Skip to content

Commit 01ac9c6

Browse files
zeyapfacebook-github-bot
authored andcommitted
Add support for PlatformColor type of toValue and interpolation outputRange (facebook#54450)
Summary: ## Changelog: [Android] [Added] Add support for PlatformColor type of toValue and interpolation outputRange Making this change in order to support animation from and to PlatformColor type, in use cases like below Example 1: ``` const animatedColor = useRef( new Animated.Color(PlatformColor('android:color/white')), ); ... Animated.timing(animatedColor.current, { toValue: PlatformColor('android:color/darker_gray'), duration: 500, useNativeDriver: true, }).start(); ``` Example 2: ``` const animatedValue = useRef(new Animated.Value(0)); ... backgroundColor: animatedValue.current.interpolate({ inputRange: [0, 1], outputRange: [ PlatformColor('android:color/white'), PlatformColor('android:color/darker_gray'), ], }), ``` A nuance here: in the framework js code, usually `AnimatedColor` class will resolve color for each channel (RGBA) and create multiple `AnimatedValue` instances. However for platform color, instead of directly resolving the PlatformColor by calling native module, we could pass down the color string + channel name to native, and defer to AnimatedNode/Driver on the native side to resolve the actual color value when it's necessary. But this strategy means PlatformColor toValue/outputRange won't work if `useNativeDriver` is turned off. Note that `fromValue` is already supported (in ColorAnimatedNode.kt) Differential Revision: D86351284
1 parent 7c91517 commit 01ac9c6

File tree

5 files changed

+132
-28
lines changed

5 files changed

+132
-28
lines changed

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/AnimatedNode.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
package com.facebook.react.animated
99

10+
import android.content.Context
1011
import java.util.ArrayList
1112

1213
/** 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 {
1516
internal companion object {
1617
internal const val INITIAL_BFS_COLOR: Int = 0
1718
internal const val DEFAULT_ANIMATED_NODE_CHILD_COUNT: Int = 1
19+
20+
internal fun getContextHelper(node: AnimatedNode): Context? {
21+
// Search children depth-first until we get to a PropsAnimatedNode, from which we can
22+
// get the view and its context
23+
node.children?.let { children ->
24+
for (child in children) {
25+
return if (child is PropsAnimatedNode) {
26+
val view = child.connectedView
27+
view?.context
28+
} else {
29+
getContextHelper(child)
30+
}
31+
}
32+
}
33+
return null
34+
}
1835
}
1936

2037
// TODO: T196787278 Reduce the visibility of these fields to package once we have

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/ColorAnimatedNode.kt

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -90,22 +90,4 @@ internal class ColorAnimatedNode(
9090
// case we will search for a view associated with a PropsAnimatedNode to get the context.
9191
return reactApplicationContext.currentActivity ?: getContextHelper(this)
9292
}
93-
94-
companion object {
95-
private fun getContextHelper(node: AnimatedNode): Context? {
96-
// Search children depth-first until we get to a PropsAnimatedNode, from which we can
97-
// get the view and its context
98-
node.children?.let { children ->
99-
for (child in children) {
100-
return if (child is PropsAnimatedNode) {
101-
val view = child.connectedView
102-
view?.context
103-
} else {
104-
getContextHelper(child)
105-
}
106-
}
107-
}
108-
return null
109-
}
110-
}
11193
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/FrameBasedAnimationDriver.kt

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77

88
package com.facebook.react.animated
99

10+
import android.content.Context
11+
import android.graphics.Color
1012
import com.facebook.common.logging.FLog
13+
import com.facebook.react.bridge.ColorPropConverter
14+
import com.facebook.react.bridge.ReactApplicationContext
1115
import com.facebook.react.bridge.ReadableMap
1216
import com.facebook.react.bridge.ReadableType
1317
import com.facebook.react.common.ReactConstants
@@ -18,31 +22,68 @@ import com.facebook.react.common.build.ReactBuildConfig
1822
* that are pre-calculate on the JS side. For each animation frame JS provides a value from 0 to 1
1923
* that indicates a progress of the animation at that frame.
2024
*/
21-
internal class FrameBasedAnimationDriver(config: ReadableMap) : AnimationDriver() {
25+
internal class FrameBasedAnimationDriver(
26+
config: ReadableMap,
27+
private val reactApplicationContext: ReactApplicationContext,
28+
) : AnimationDriver() {
2229
private var startFrameTimeNanos: Long = -1
2330
private var frames: DoubleArray = DoubleArray(0)
24-
private var toValue = 0.0
31+
private var toValue: Double = 0.0
2532
private var fromValue = 0.0
2633
private var iterations = 1
2734
private var currentLoop = 1
2835
private var logCount = 0
2936

37+
private val context: Context?
38+
get() {
39+
// There are cases where the activity may not exist (such as for VRShell panel apps). In this
40+
// case we will search for a view associated with a PropsAnimatedNode to get the context.
41+
return reactApplicationContext.currentActivity
42+
?: AnimatedNode.getContextHelper(requireNotNull(animatedValue))
43+
}
44+
3045
init {
3146
resetConfig(config)
3247
}
3348

3449
override fun resetConfig(config: ReadableMap) {
35-
val framesConfig = config.getArray("frames")
36-
if (framesConfig != null) {
50+
config.getArray("frames")?.let { framesConfig ->
3751
val numberOfFrames = framesConfig.size()
3852
if (frames.size != numberOfFrames) {
3953
frames = DoubleArray(numberOfFrames) { i -> framesConfig.getDouble(i) }
4054
}
4155
}
56+
4257
toValue =
43-
if (config.hasKey("toValue") && config.getType("toValue") == ReadableType.Number)
44-
config.getDouble("toValue")
45-
else 0.0
58+
when {
59+
!config.hasKey("toValue") -> 0.0
60+
config.getType("toValue") == ReadableType.Number -> config.getDouble("toValue")
61+
config.getType("toValue") == ReadableType.Map -> {
62+
val toValueMap = config.getMap("toValue")
63+
if (
64+
toValueMap != null &&
65+
toValueMap.hasKey("nativeColor") &&
66+
toValueMap.hasKey("channel")
67+
) {
68+
// Handle platform color with channel information
69+
val nativeColorMap = toValueMap.getMap("nativeColor")
70+
val channel = toValueMap.getString("channel")
71+
val resolvedColor = context?.let { ColorPropConverter.getColor(nativeColorMap, it) }
72+
when {
73+
resolvedColor == null || channel == null -> 0.0
74+
channel == "r" -> Color.red(resolvedColor).toDouble()
75+
channel == "g" -> Color.green(resolvedColor).toDouble()
76+
channel == "b" -> Color.blue(resolvedColor).toDouble()
77+
channel == "a" -> Color.alpha(resolvedColor) / 255.0
78+
else -> 0.0
79+
}
80+
} else {
81+
0.0
82+
}
83+
}
84+
else -> 0.0
85+
}
86+
4687
iterations =
4788
if (config.hasKey("iterations") && config.getType("iterations") == ReadableType.Number)
4889
config.getInt("iterations")

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/InterpolationAnimatedNode.kt

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77

88
package com.facebook.react.animated
99

10+
import android.content.Context
1011
import androidx.core.graphics.ColorUtils
12+
import com.facebook.react.bridge.ColorPropConverter
1113
import com.facebook.react.bridge.JSApplicationIllegalArgumentException
14+
import com.facebook.react.bridge.ReactApplicationContext
1215
import com.facebook.react.bridge.ReadableArray
1316
import com.facebook.react.bridge.ReadableMap
1417
import com.facebook.react.bridge.ReadableType
@@ -19,10 +22,14 @@ import java.util.regex.Pattern
1922
*
2023
* Currently only a linear interpolation is supported on an input range of an arbitrary size.
2124
*/
22-
internal class InterpolationAnimatedNode(config: ReadableMap) : ValueAnimatedNode() {
25+
internal class InterpolationAnimatedNode(
26+
config: ReadableMap,
27+
private val reactApplicationContext: ReactApplicationContext,
28+
) : ValueAnimatedNode() {
2329
private enum class OutputType {
2430
Number,
2531
Color,
32+
PlatformColor,
2633
String,
2734
}
2835

@@ -35,11 +42,21 @@ internal class InterpolationAnimatedNode(config: ReadableMap) : ValueAnimatedNod
3542
private var parent: ValueAnimatedNode? = null
3643
private var objectValue: Any? = null
3744

45+
private val context: Context?
46+
get() {
47+
// There are cases where the activity may not exist (such as for VRShell panel apps). In this
48+
// case we will search for a view associated with a PropsAnimatedNode to get the context.
49+
return reactApplicationContext.currentActivity ?: getContextHelper(this)
50+
}
51+
3852
init {
3953
val output = config.getArray("outputRange")
4054
if (COLOR_OUTPUT_TYPE == config.getString("outputType")) {
4155
outputType = OutputType.Color
4256
outputRange = fromIntArray(output)
57+
} else if (PLATFORM_COLOR_OUTPUT_TYPE == config.getString("outputType")) {
58+
outputType = OutputType.PlatformColor
59+
outputRange = fromMapArray(output)
4360
} else if (output?.getType(0) == ReadableType.String) {
4461
outputType = OutputType.String
4562
outputRange = fromStringPattern(output)
@@ -77,6 +94,17 @@ internal class InterpolationAnimatedNode(config: ReadableMap) : ValueAnimatedNod
7794
OutputType.Color ->
7895
objectValue =
7996
Integer.valueOf(interpolateColor(parentValue, inputRange, outputRange as IntArray))
97+
OutputType.PlatformColor -> {
98+
@Suppress("UNCHECKED_CAST")
99+
objectValue =
100+
Integer.valueOf(
101+
interpolatePlatformColor(
102+
parentValue,
103+
inputRange,
104+
outputRange as List<ReadableMap>,
105+
)
106+
)
107+
}
80108
OutputType.String ->
81109
pattern?.let {
82110
@Suppress("UNCHECKED_CAST")
@@ -100,6 +128,30 @@ internal class InterpolationAnimatedNode(config: ReadableMap) : ValueAnimatedNod
100128
override fun prettyPrint(): String =
101129
"InterpolationAnimatedNode[$tag] super: ${super.prettyPrint()}"
102130

131+
private fun interpolatePlatformColor(
132+
value: Double,
133+
inputRange: DoubleArray,
134+
outputRange: List<ReadableMap>,
135+
): Int {
136+
val rangeIndex = findRangeIndex(value, inputRange)
137+
val outputMin =
138+
context?.let { ColorPropConverter.getColor(outputRange[rangeIndex], it) ?: 0 } ?: 0
139+
val outputMax =
140+
context?.let { ColorPropConverter.getColor(outputRange[rangeIndex + 1], it) ?: 0 } ?: 0
141+
if (outputMin == outputMax) {
142+
return outputMin
143+
}
144+
val inputMin = inputRange[rangeIndex]
145+
val inputMax = inputRange[rangeIndex + 1]
146+
if (inputMin == inputMax) {
147+
return if (value <= inputMin) {
148+
outputMin
149+
} else outputMax
150+
}
151+
val ratio = (value - inputMin) / (inputMax - inputMin)
152+
return ColorUtils.blendARGB(outputMin, outputMax, ratio.toFloat())
153+
}
154+
103155
companion object {
104156
const val EXTRAPOLATE_TYPE_IDENTITY: String = "identity"
105157
const val EXTRAPOLATE_TYPE_CLAMP: String = "clamp"
@@ -108,6 +160,7 @@ internal class InterpolationAnimatedNode(config: ReadableMap) : ValueAnimatedNod
108160
private val numericPattern: Pattern =
109161
Pattern.compile("[+-]?(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?")
110162
private const val COLOR_OUTPUT_TYPE: String = "color"
163+
private const val PLATFORM_COLOR_OUTPUT_TYPE: String = "platform_color"
111164

112165
private fun fromDoubleArray(array: ReadableArray?): DoubleArray {
113166
val size = array?.size() ?: return DoubleArray(0)
@@ -127,6 +180,15 @@ internal class InterpolationAnimatedNode(config: ReadableMap) : ValueAnimatedNod
127180
return res
128181
}
129182

183+
private fun fromMapArray(array: ReadableArray?): List<ReadableMap> {
184+
val size = array?.size() ?: return ArrayList()
185+
val res = ArrayList<ReadableMap>(size)
186+
for (i in 0 until size) {
187+
res.add(checkNotNull(array.getMap(i)))
188+
}
189+
return res
190+
}
191+
130192
private fun fromStringPattern(array: ReadableArray): Array<DoubleArray?> {
131193
val size = array.size()
132194
val outputRange = arrayOfNulls<DoubleArray>(size)

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ public class NativeAnimatedNodesManager(
112112
"value" -> ValueAnimatedNode(config)
113113
"color" -> ColorAnimatedNode(config, this, checkNotNull(reactApplicationContext))
114114
"props" -> PropsAnimatedNode(config, this)
115-
"interpolation" -> InterpolationAnimatedNode(config)
115+
"interpolation" ->
116+
InterpolationAnimatedNode(config, checkNotNull(reactApplicationContext))
116117
"addition" -> AdditionAnimatedNode(config, this)
117118
"subtraction" -> SubtractionAnimatedNode(config, this)
118119
"division" -> DivisionAnimatedNode(config, this)
@@ -247,7 +248,8 @@ public class NativeAnimatedNodesManager(
247248

248249
val animation =
249250
when (val type = animationConfig.getString("type")) {
250-
"frames" -> FrameBasedAnimationDriver(animationConfig)
251+
"frames" ->
252+
FrameBasedAnimationDriver(animationConfig, checkNotNull(reactApplicationContext))
251253
"spring" -> SpringAnimation(animationConfig)
252254
"decay" -> DecayAnimation(animationConfig)
253255
else -> {

0 commit comments

Comments
 (0)