From 6cfba679008523817ef7d94a340eaebd16989baa Mon Sep 17 00:00:00 2001 From: Ruslan Shestopalyuk Date: Sun, 28 Jul 2024 04:13:14 -0700 Subject: [PATCH] Kotlinify InterpolationAnimatedNode (#45759) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/45759 # Changelog: [Internal] - As in the title. Differential Revision: D60348112 --- .../animated/InterpolationAnimatedNode.java | 311 ------------------ .../animated/InterpolationAnimatedNode.kt | 282 ++++++++++++++++ 2 files changed, 282 insertions(+), 311 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/InterpolationAnimatedNode.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/InterpolationAnimatedNode.kt diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/InterpolationAnimatedNode.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/InterpolationAnimatedNode.java deleted file mode 100644 index 4307de260166ef..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/InterpolationAnimatedNode.java +++ /dev/null @@ -1,311 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.animated; - -import androidx.annotation.Nullable; -import androidx.core.graphics.ColorUtils; -import com.facebook.react.bridge.JSApplicationIllegalArgumentException; -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.ReadableType; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Animated node that corresponds to {@code AnimatedInterpolation} from AnimatedImplementation.js. - * - *

Currently only a linear interpolation is supported on an input range of an arbitrary size. - */ -/*package*/ class InterpolationAnimatedNode extends ValueAnimatedNode { - - public static final String EXTRAPOLATE_TYPE_IDENTITY = "identity"; - public static final String EXTRAPOLATE_TYPE_CLAMP = "clamp"; - public static final String EXTRAPOLATE_TYPE_EXTEND = "extend"; - - private static final Pattern sNumericPattern = - Pattern.compile("[+-]?(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?"); - - private static final String COLOR_OUTPUT_TYPE = "color"; - - private static double[] fromDoubleArray(ReadableArray ary) { - double[] res = new double[ary.size()]; - for (int i = 0; i < res.length; i++) { - res[i] = ary.getDouble(i); - } - return res; - } - - private static int[] fromIntArray(ReadableArray ary) { - int[] res = new int[ary.size()]; - for (int i = 0; i < res.length; i++) { - res[i] = ary.getInt(i); - } - return res; - } - - private static double[][] fromStringPattern(ReadableArray array) { - int size = array.size(); - double[][] outputRange = new double[size][]; - - // Match the first pattern into a List, since we don't know its length yet - Matcher m = sNumericPattern.matcher(array.getString(0)); - List firstOutputRange = new ArrayList<>(); - while (m.find()) { - firstOutputRange.add(Double.parseDouble(m.group())); - } - double[] firstOutputRangeArr = new double[firstOutputRange.size()]; - for (int i = 0; i < firstOutputRange.size(); i++) { - firstOutputRangeArr[i] = firstOutputRange.get(i).doubleValue(); - } - outputRange[0] = firstOutputRangeArr; - - for (int i = 1; i < size; i++) { - double[] outputArr = new double[firstOutputRangeArr.length]; - - int j = 0; - m = sNumericPattern.matcher(array.getString(i)); - while (m.find() && j < firstOutputRangeArr.length) { - outputArr[j++] = Double.parseDouble(m.group()); - } - outputRange[i] = outputArr; - } - - return outputRange; - } - - private static double interpolate( - double value, - double inputMin, - double inputMax, - double outputMin, - double outputMax, - String extrapolateLeft, - String extrapolateRight) { - double result = value; - - // Extrapolate - if (result < inputMin) { - switch (extrapolateLeft) { - case EXTRAPOLATE_TYPE_IDENTITY: - return result; - case EXTRAPOLATE_TYPE_CLAMP: - result = inputMin; - break; - case EXTRAPOLATE_TYPE_EXTEND: - break; - default: - throw new JSApplicationIllegalArgumentException( - "Invalid extrapolation type " + extrapolateLeft + "for left extrapolation"); - } - } - - if (result > inputMax) { - switch (extrapolateRight) { - case EXTRAPOLATE_TYPE_IDENTITY: - return result; - case EXTRAPOLATE_TYPE_CLAMP: - result = inputMax; - break; - case EXTRAPOLATE_TYPE_EXTEND: - break; - default: - throw new JSApplicationIllegalArgumentException( - "Invalid extrapolation type " + extrapolateRight + "for right extrapolation"); - } - } - - if (outputMin == outputMax) { - return outputMin; - } - - if (inputMin == inputMax) { - if (value <= inputMin) { - return outputMin; - } - return outputMax; - } - - return outputMin + (outputMax - outputMin) * (result - inputMin) / (inputMax - inputMin); - } - - /*package*/ static double interpolate( - double value, - double[] inputRange, - double[] outputRange, - String extrapolateLeft, - String extrapolateRight) { - int rangeIndex = findRangeIndex(value, inputRange); - return interpolate( - value, - inputRange[rangeIndex], - inputRange[rangeIndex + 1], - outputRange[rangeIndex], - outputRange[rangeIndex + 1], - extrapolateLeft, - extrapolateRight); - } - - /*package*/ static int interpolateColor(double value, double[] inputRange, int[] outputRange) { - int rangeIndex = findRangeIndex(value, inputRange); - int outputMin = outputRange[rangeIndex]; - int outputMax = outputRange[rangeIndex + 1]; - if (outputMin == outputMax) { - return outputMin; - } - - double inputMin = inputRange[rangeIndex]; - double inputMax = inputRange[rangeIndex + 1]; - if (inputMin == inputMax) { - if (value <= inputMin) { - return outputMin; - } - return outputMax; - } - - double ratio = (value - inputMin) / (inputMax - inputMin); - return ColorUtils.blendARGB(outputMin, outputMax, (float) ratio); - } - - /*package*/ static String interpolateString( - String pattern, - double value, - double[] inputRange, - double[][] outputRange, - String extrapolateLeft, - String extrapolateRight) { - int rangeIndex = findRangeIndex(value, inputRange); - StringBuffer sb = new StringBuffer(pattern.length()); - - Matcher m = sNumericPattern.matcher(pattern); - int i = 0; - while (m.find() && i < outputRange[rangeIndex].length) { - double val = - interpolate( - value, - inputRange[rangeIndex], - inputRange[rangeIndex + 1], - outputRange[rangeIndex][i], - outputRange[rangeIndex + 1][i], - extrapolateLeft, - extrapolateRight); - int intVal = (int) val; - m.appendReplacement(sb, intVal != val ? Double.toString(val) : Integer.toString(intVal)); - i++; - } - m.appendTail(sb); - return sb.toString(); - } - - private static int findRangeIndex(double value, double[] ranges) { - int index; - for (index = 1; index < ranges.length - 1; index++) { - if (ranges[index] >= value) { - break; - } - } - return index - 1; - } - - private enum OutputType { - Number, - Color, - String, - } - - private final double mInputRange[]; - private final Object mOutputRange; - private final OutputType mOutputType; - private final @Nullable String mPattern; - private final String mExtrapolateLeft; - private final String mExtrapolateRight; - private @Nullable ValueAnimatedNode mParent; - private Object mObjectValue; - - public InterpolationAnimatedNode(ReadableMap config) { - mInputRange = fromDoubleArray(config.getArray("inputRange")); - ReadableArray output = config.getArray("outputRange"); - - if (COLOR_OUTPUT_TYPE.equals(config.getString("outputType"))) { - mOutputType = OutputType.Color; - mOutputRange = fromIntArray(output); - mPattern = null; - } else if (output.getType(0) == ReadableType.String) { - mOutputType = OutputType.String; - mOutputRange = fromStringPattern(output); - mPattern = output.getString(0); - } else { - mOutputType = OutputType.Number; - mOutputRange = fromDoubleArray(output); - mPattern = null; - } - - mExtrapolateLeft = config.getString("extrapolateLeft"); - mExtrapolateRight = config.getString("extrapolateRight"); - } - - @Override - public void onAttachedToNode(AnimatedNode parent) { - if (mParent != null) { - throw new IllegalStateException("Parent already attached"); - } - if (!(parent instanceof ValueAnimatedNode)) { - throw new IllegalArgumentException("Parent is of an invalid type"); - } - mParent = (ValueAnimatedNode) parent; - } - - @Override - public void onDetachedFromNode(AnimatedNode parent) { - if (parent != mParent) { - throw new IllegalArgumentException("Invalid parent node provided"); - } - mParent = null; - } - - @Override - public void update() { - if (mParent == null) { - // The graph is in the middle of being created, just skip this unattached node. - return; - } - - double value = mParent.getValue(); - switch (mOutputType) { - case Number: - nodeValue = - interpolate( - value, mInputRange, (double[]) mOutputRange, mExtrapolateLeft, mExtrapolateRight); - break; - case Color: - mObjectValue = Integer.valueOf(interpolateColor(value, mInputRange, (int[]) mOutputRange)); - break; - case String: - mObjectValue = - interpolateString( - mPattern, - value, - mInputRange, - (double[][]) mOutputRange, - mExtrapolateLeft, - mExtrapolateRight); - break; - } - } - - @Override - public Object getAnimatedObject() { - return mObjectValue; - } - - @Override - public String prettyPrint() { - return "InterpolationAnimatedNode[" + tag + "] super: " + super.prettyPrint(); - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/InterpolationAnimatedNode.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/InterpolationAnimatedNode.kt new file mode 100644 index 00000000000000..2b10006e7d7741 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/InterpolationAnimatedNode.kt @@ -0,0 +1,282 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.animated + +import androidx.core.graphics.ColorUtils +import com.facebook.react.bridge.JSApplicationIllegalArgumentException +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.ReadableType +import java.util.regex.Pattern + +/** + * Animated node that corresponds to `AnimatedInterpolation` from AnimatedImplementation.js. + * + * Currently only a linear interpolation is supported on an input range of an arbitrary size. + */ +internal class InterpolationAnimatedNode(config: ReadableMap) : ValueAnimatedNode() { + private enum class OutputType { + Number, + Color, + String + } + + private val inputRange: DoubleArray + private var outputRange: Any? = null + private var outputType: OutputType? = null + private var pattern: String? = null + private val extrapolateLeft: String? + private val extrapolateRight: String? + private var parent: ValueAnimatedNode? = null + private var objectValue: Any? = null + + init { + inputRange = fromDoubleArray(config.getArray("inputRange")) + val output = config.getArray("outputRange") + if (COLOR_OUTPUT_TYPE == config.getString("outputType")) { + outputType = OutputType.Color + outputRange = fromIntArray(output) + pattern = null + } else if (output?.getType(0) == ReadableType.String) { + outputType = OutputType.String + outputRange = fromStringPattern(output) + pattern = output.getString(0) + } else { + outputType = OutputType.Number + outputRange = fromDoubleArray(output) + pattern = null + } + extrapolateLeft = config.getString("extrapolateLeft") + extrapolateRight = config.getString("extrapolateRight") + } + + override fun onAttachedToNode(parent: AnimatedNode) { + check(this.parent == null) { "Parent already attached" } + require(parent is ValueAnimatedNode) { "Parent is of an invalid type" } + this.parent = parent + } + + override fun onDetachedFromNode(parent: AnimatedNode) { + require(parent === this.parent) { "Invalid parent node provided" } + this.parent = null + } + + override fun update() { + // If the graph is in the middle of being created, just skip this unattached node. + val parentValue = parent?.getValue() ?: return + when (outputType) { + OutputType.Number -> + nodeValue = + interpolate( + parentValue, + inputRange, + outputRange as DoubleArray, + extrapolateLeft, + extrapolateRight) + OutputType.Color -> + objectValue = + Integer.valueOf(interpolateColor(parentValue, inputRange, outputRange as IntArray)) + OutputType.String -> + pattern?.let { + @Suppress("UNCHECKED_CAST") + objectValue = + interpolateString( + it, + parentValue, + inputRange, + outputRange as Array, + extrapolateLeft, + extrapolateRight) + } + + else -> {} + } + } + + override fun getAnimatedObject(): Any? = objectValue + + override fun prettyPrint(): String = + "InterpolationAnimatedNode[$tag] super: {super.prettyPrint()}" + + internal companion object { + private const val EXTRAPOLATE_TYPE_IDENTITY: String = "identity" + private const val EXTRAPOLATE_TYPE_CLAMP: String = "clamp" + private const val EXTRAPOLATE_TYPE_EXTEND: String = "extend" + private val numericPattern: Pattern = + Pattern.compile("[+-]?(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?") + private const val COLOR_OUTPUT_TYPE: String = "color" + + private fun fromDoubleArray(array: ReadableArray?): DoubleArray { + val size = array?.size() ?: return DoubleArray(0) + val res = DoubleArray(size) + for (i in res.indices) { + res[i] = array.getDouble(i) + } + return res + } + + private fun fromIntArray(array: ReadableArray?): IntArray { + val size = array?.size() ?: return IntArray(0) + val res = IntArray(size) + for (i in res.indices) { + res[i] = array.getInt(i) + } + return res + } + + private fun fromStringPattern(array: ReadableArray): Array { + val size = array.size() + val outputRange = arrayOfNulls(size) + + // Match the first pattern into a List, since we don't know its length yet + var m = numericPattern.matcher(array.getString(0)) + val firstOutputRange: MutableList = ArrayList() + while (m.find()) { + firstOutputRange.add(m.group().toDouble()) + } + val firstOutputRangeArr = DoubleArray(firstOutputRange.size) + for (i in firstOutputRange.indices) { + firstOutputRangeArr[i] = firstOutputRange[i] + } + outputRange[0] = firstOutputRangeArr + for (i in 1 until size) { + val outputArr = DoubleArray(firstOutputRangeArr.size) + var j = 0 + m = numericPattern.matcher(array.getString(i)) + while (m.find() && j < firstOutputRangeArr.size) { + outputArr[j++] = m.group().toDouble() + } + outputRange[i] = outputArr + } + return outputRange + } + + private fun interpolate( + value: Double, + inputMin: Double, + inputMax: Double, + outputMin: Double, + outputMax: Double, + extrapolateLeft: String?, + extrapolateRight: String? + ): Double { + var result = value + + // Extrapolate + if (result < inputMin) { + when (extrapolateLeft) { + EXTRAPOLATE_TYPE_IDENTITY -> return result + EXTRAPOLATE_TYPE_CLAMP -> result = inputMin + EXTRAPOLATE_TYPE_EXTEND -> {} + else -> + throw JSApplicationIllegalArgumentException( + "Invalid extrapolation type " + extrapolateLeft + "for left extrapolation") + } + } + if (result > inputMax) { + when (extrapolateRight) { + EXTRAPOLATE_TYPE_IDENTITY -> return result + EXTRAPOLATE_TYPE_CLAMP -> result = inputMax + EXTRAPOLATE_TYPE_EXTEND -> {} + else -> + throw JSApplicationIllegalArgumentException( + "Invalid extrapolation type " + extrapolateRight + "for right extrapolation") + } + } + if (outputMin == outputMax) { + return outputMin + } + return if (inputMin == inputMax) { + if (value <= inputMin) { + outputMin + } else outputMax + } else outputMin + (outputMax - outputMin) * (result - inputMin) / (inputMax - inputMin) + } + + internal fun interpolate( + value: Double, + inputRange: DoubleArray, + outputRange: DoubleArray, + extrapolateLeft: String?, + extrapolateRight: String? + ): Double { + val rangeIndex = findRangeIndex(value, inputRange) + return interpolate( + value, + inputRange[rangeIndex], + inputRange[rangeIndex + 1], + outputRange[rangeIndex], + outputRange[rangeIndex + 1], + extrapolateLeft, + extrapolateRight) + } + + internal fun interpolateColor( + value: Double, + inputRange: DoubleArray, + outputRange: IntArray + ): Int { + val rangeIndex = findRangeIndex(value, inputRange) + val outputMin = outputRange[rangeIndex] + val outputMax = outputRange[rangeIndex + 1] + if (outputMin == outputMax) { + return outputMin + } + val inputMin = inputRange[rangeIndex] + val inputMax = inputRange[rangeIndex + 1] + if (inputMin == inputMax) { + return if (value <= inputMin) { + outputMin + } else outputMax + } + val ratio = (value - inputMin) / (inputMax - inputMin) + return ColorUtils.blendARGB(outputMin, outputMax, ratio.toFloat()) + } + + internal fun interpolateString( + pattern: String, + value: Double, + inputRange: DoubleArray, + outputRange: Array, + extrapolateLeft: String?, + extrapolateRight: String? + ): String { + val rangeIndex = findRangeIndex(value, inputRange) + val sb = StringBuffer(pattern.length) + val m = numericPattern.matcher(pattern) + var i = 0 + while (m.find() && i < outputRange[rangeIndex].size) { + val v = + interpolate( + value, + inputRange[rangeIndex], + inputRange[rangeIndex + 1], + outputRange[rangeIndex][i], + outputRange[rangeIndex + 1][i], + extrapolateLeft, + extrapolateRight) + val intVal = v.toInt() + m.appendReplacement(sb, if (intVal.toDouble() != v) v.toString() else intVal.toString()) + i++ + } + m.appendTail(sb) + return sb.toString() + } + + private fun findRangeIndex(value: Double, ranges: DoubleArray): Int { + var index: Int = 1 + while (index < ranges.size - 1) { + if (ranges[index] >= value) { + break + } + index++ + } + return index - 1 + } + } +}