diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/AnimatedNode.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/AnimatedNode.kt index ccc17069ef0fce..e535a2d9a81848 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/AnimatedNode.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/AnimatedNode.kt @@ -7,6 +7,7 @@ package com.facebook.react.animated +import android.content.Context import java.util.ArrayList /** Base class for all Animated.js library node types that can be created on the "native" side. */ @@ -15,6 +16,22 @@ public abstract class AnimatedNode { internal companion object { internal const val INITIAL_BFS_COLOR: Int = 0 internal const val DEFAULT_ANIMATED_NODE_CHILD_COUNT: Int = 1 + + internal fun getContextHelper(node: AnimatedNode): Context? { + // Search children depth-first until we get to a PropsAnimatedNode, from which we can + // get the view and its context + node.children?.let { children -> + for (child in children) { + return if (child is PropsAnimatedNode) { + val view = child.connectedView + view?.context + } else { + getContextHelper(child) + } + } + } + return null + } } // TODO: T196787278 Reduce the visibility of these fields to package once we have diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/ColorAnimatedNode.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/ColorAnimatedNode.kt index d971d8fd2d30f3..944420a5a1e536 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/ColorAnimatedNode.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/ColorAnimatedNode.kt @@ -90,22 +90,4 @@ internal class ColorAnimatedNode( // case we will search for a view associated with a PropsAnimatedNode to get the context. return reactApplicationContext.currentActivity ?: getContextHelper(this) } - - companion object { - private fun getContextHelper(node: AnimatedNode): Context? { - // Search children depth-first until we get to a PropsAnimatedNode, from which we can - // get the view and its context - node.children?.let { children -> - for (child in children) { - return if (child is PropsAnimatedNode) { - val view = child.connectedView - view?.context - } else { - getContextHelper(child) - } - } - } - return null - } - } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/FrameBasedAnimationDriver.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/FrameBasedAnimationDriver.kt index ab772fb69d2bd9..75e0bb99ea3280 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/FrameBasedAnimationDriver.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/FrameBasedAnimationDriver.kt @@ -7,7 +7,11 @@ package com.facebook.react.animated +import android.content.Context +import android.graphics.Color import com.facebook.common.logging.FLog +import com.facebook.react.bridge.ColorPropConverter +import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.ReadableType import com.facebook.react.common.ReactConstants @@ -18,31 +22,68 @@ import com.facebook.react.common.build.ReactBuildConfig * that are pre-calculate on the JS side. For each animation frame JS provides a value from 0 to 1 * that indicates a progress of the animation at that frame. */ -internal class FrameBasedAnimationDriver(config: ReadableMap) : AnimationDriver() { +internal class FrameBasedAnimationDriver( + config: ReadableMap, + private val reactApplicationContext: ReactApplicationContext, +) : AnimationDriver() { private var startFrameTimeNanos: Long = -1 private var frames: DoubleArray = DoubleArray(0) - private var toValue = 0.0 + private var toValue: Double = 0.0 private var fromValue = 0.0 private var iterations = 1 private var currentLoop = 1 private var logCount = 0 + private val context: Context? + get() { + // There are cases where the activity may not exist (such as for VRShell panel apps). In this + // case we will search for a view associated with a PropsAnimatedNode to get the context. + return reactApplicationContext.currentActivity + ?: AnimatedNode.getContextHelper(requireNotNull(animatedValue)) + } + init { resetConfig(config) } override fun resetConfig(config: ReadableMap) { - val framesConfig = config.getArray("frames") - if (framesConfig != null) { + config.getArray("frames")?.let { framesConfig -> val numberOfFrames = framesConfig.size() if (frames.size != numberOfFrames) { frames = DoubleArray(numberOfFrames) { i -> framesConfig.getDouble(i) } } } + toValue = - if (config.hasKey("toValue") && config.getType("toValue") == ReadableType.Number) - config.getDouble("toValue") - else 0.0 + when { + !config.hasKey("toValue") -> 0.0 + config.getType("toValue") == ReadableType.Number -> config.getDouble("toValue") + config.getType("toValue") == ReadableType.Map -> { + val toValueMap = config.getMap("toValue") + if ( + toValueMap != null && + toValueMap.hasKey("nativeColor") && + toValueMap.hasKey("channel") + ) { + // Handle platform color with channel information + val nativeColorMap = toValueMap.getMap("nativeColor") + val channel = toValueMap.getString("channel") + val resolvedColor = context?.let { ColorPropConverter.getColor(nativeColorMap, it) } + when { + resolvedColor == null || channel == null -> 0.0 + channel == "r" -> Color.red(resolvedColor).toDouble() + channel == "g" -> Color.green(resolvedColor).toDouble() + channel == "b" -> Color.blue(resolvedColor).toDouble() + channel == "a" -> Color.alpha(resolvedColor) / 255.0 + else -> 0.0 + } + } else { + 0.0 + } + } + else -> 0.0 + } + iterations = if (config.hasKey("iterations") && config.getType("iterations") == ReadableType.Number) config.getInt("iterations") 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 index be57f0d95d8023..27c504a24eeca6 100644 --- 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 @@ -7,8 +7,11 @@ package com.facebook.react.animated +import android.content.Context import androidx.core.graphics.ColorUtils +import com.facebook.react.bridge.ColorPropConverter import com.facebook.react.bridge.JSApplicationIllegalArgumentException +import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.ReadableType @@ -19,10 +22,14 @@ import java.util.regex.Pattern * * Currently only a linear interpolation is supported on an input range of an arbitrary size. */ -internal class InterpolationAnimatedNode(config: ReadableMap) : ValueAnimatedNode() { +internal class InterpolationAnimatedNode( + config: ReadableMap, + private val reactApplicationContext: ReactApplicationContext, +) : ValueAnimatedNode() { private enum class OutputType { Number, Color, + PlatformColor, String, } @@ -35,11 +42,21 @@ internal class InterpolationAnimatedNode(config: ReadableMap) : ValueAnimatedNod private var parent: ValueAnimatedNode? = null private var objectValue: Any? = null + private val context: Context? + get() { + // There are cases where the activity may not exist (such as for VRShell panel apps). In this + // case we will search for a view associated with a PropsAnimatedNode to get the context. + return reactApplicationContext.currentActivity ?: getContextHelper(this) + } + init { val output = config.getArray("outputRange") if (COLOR_OUTPUT_TYPE == config.getString("outputType")) { outputType = OutputType.Color outputRange = fromIntArray(output) + } else if (PLATFORM_COLOR_OUTPUT_TYPE == config.getString("outputType")) { + outputType = OutputType.PlatformColor + outputRange = fromMapArray(output) } else if (output?.getType(0) == ReadableType.String) { outputType = OutputType.String outputRange = fromStringPattern(output) @@ -77,6 +94,17 @@ internal class InterpolationAnimatedNode(config: ReadableMap) : ValueAnimatedNod OutputType.Color -> objectValue = Integer.valueOf(interpolateColor(parentValue, inputRange, outputRange as IntArray)) + OutputType.PlatformColor -> { + @Suppress("UNCHECKED_CAST") + objectValue = + Integer.valueOf( + interpolatePlatformColor( + parentValue, + inputRange, + outputRange as List, + ) + ) + } OutputType.String -> pattern?.let { @Suppress("UNCHECKED_CAST") @@ -100,6 +128,30 @@ internal class InterpolationAnimatedNode(config: ReadableMap) : ValueAnimatedNod override fun prettyPrint(): String = "InterpolationAnimatedNode[$tag] super: ${super.prettyPrint()}" + private fun interpolatePlatformColor( + value: Double, + inputRange: DoubleArray, + outputRange: List, + ): Int { + val rangeIndex = findRangeIndex(value, inputRange) + val outputMin = + context?.let { ColorPropConverter.getColor(outputRange[rangeIndex], it) ?: 0 } ?: 0 + val outputMax = + context?.let { ColorPropConverter.getColor(outputRange[rangeIndex + 1], it) ?: 0 } ?: 0 + 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()) + } + companion object { const val EXTRAPOLATE_TYPE_IDENTITY: String = "identity" const val EXTRAPOLATE_TYPE_CLAMP: String = "clamp" @@ -108,6 +160,7 @@ internal class InterpolationAnimatedNode(config: ReadableMap) : ValueAnimatedNod private val numericPattern: Pattern = Pattern.compile("[+-]?(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?") private const val COLOR_OUTPUT_TYPE: String = "color" + private const val PLATFORM_COLOR_OUTPUT_TYPE: String = "platform_color" private fun fromDoubleArray(array: ReadableArray?): DoubleArray { val size = array?.size() ?: return DoubleArray(0) @@ -127,6 +180,15 @@ internal class InterpolationAnimatedNode(config: ReadableMap) : ValueAnimatedNod return res } + private fun fromMapArray(array: ReadableArray?): List { + val size = array?.size() ?: return ArrayList() + val res = ArrayList(size) + for (i in 0 until size) { + res.add(checkNotNull(array.getMap(i))) + } + return res + } + private fun fromStringPattern(array: ReadableArray): Array { val size = array.size() val outputRange = arrayOfNulls(size) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.kt index f7237025ab3b02..301929e2134017 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.kt @@ -112,7 +112,8 @@ public class NativeAnimatedNodesManager( "value" -> ValueAnimatedNode(config) "color" -> ColorAnimatedNode(config, this, checkNotNull(reactApplicationContext)) "props" -> PropsAnimatedNode(config, this) - "interpolation" -> InterpolationAnimatedNode(config) + "interpolation" -> + InterpolationAnimatedNode(config, checkNotNull(reactApplicationContext)) "addition" -> AdditionAnimatedNode(config, this) "subtraction" -> SubtractionAnimatedNode(config, this) "division" -> DivisionAnimatedNode(config, this) @@ -247,7 +248,8 @@ public class NativeAnimatedNodesManager( val animation = when (val type = animationConfig.getString("type")) { - "frames" -> FrameBasedAnimationDriver(animationConfig) + "frames" -> + FrameBasedAnimationDriver(animationConfig, checkNotNull(reactApplicationContext)) "spring" -> SpringAnimation(animationConfig) "decay" -> DecayAnimation(animationConfig) else -> { diff --git a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp index 355e69ac70107c..3d6a9d9169aa96 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp @@ -568,10 +568,10 @@ void NativeAnimatedNodesManager::stopRenderCallbackIfNeeded( if (isRenderCallbackStarted) { if (stopOnRenderCallback_) { stopOnRenderCallback_(isAsync); - } - if (frameRateListenerCallback_) { - frameRateListenerCallback_(false); + if (frameRateListenerCallback_) { + frameRateListenerCallback_(false); + } } } } diff --git a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp index ed571e15019be1..d318aca8b7961b 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp @@ -96,7 +96,8 @@ NativeAnimatedNodesManagerProvider::getOrCreate( std::move(directManipulationCallback), std::move(fabricCommitCallback), std::move(startOnRenderCallback_), - std::move(stopOnRenderCallback_)); + std::move(stopOnRenderCallback_), + std::move(frameRateListenerCallback_)); nativeAnimatedDelegate_ = std::make_shared(