Skip to content

Commit a6bf42a

Browse files
Bartlomiej Bloniarzfacebook-github-bot
authored andcommitted
Add batched animated prop update path on Android
Summary: Implements the Android batched animated prop update path on top of the cross-platform delegate chain introduced in the previous commit. Gated behind the `optimizedAnimatedPropUpdates` feature flag, so this change has no behavioural impact when the flag is off. - `BatchedAnimatedPropsMountItem.kt`: decodes the int/double buffer protocol and applies updates to views. - `AnimatedPropBufferEncoder` + `AnimatedPropCommands`: C++ side that encodes `AnimatedProps` into the buffer protocol. - `FabricMountingManager::synchronouslyUpdateAnimatedPropsOnUIThread`: builds the buffers and JNI-calls into Java. - `FabricUIManagerBinding`: wires the `SchedulerDelegate` override into `FabricMountingManager`. - `FabricUIManager.synchronouslyUpdateViewBatch`: Java entry point invoked from JNI that constructs and executes a `BatchedAnimatedPropsMountItem`. Changelog: [Android][Added] - Add a batched animated prop update path that applies all pending C++ animation backend updates via a single mount item (gated by `optimizedAnimatedPropUpdates`) Differential Revision: D101157453
1 parent 655ee05 commit a6bf42a

11 files changed

Lines changed: 998 additions & 3 deletions

File tree

packages/react-native/ReactAndroid/api/ReactAndroid.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2267,6 +2267,7 @@ public class com/facebook/react/fabric/FabricUIManager : com/facebook/react/brid
22672267
public fun stopSurface (I)V
22682268
public fun stopSurface (Lcom/facebook/react/fabric/SurfaceHandlerBinding;)V
22692269
public fun sweepActiveTouchForTag (II)V
2270+
public fun synchronouslyUpdateViewBatch ([I[D)V
22702271
public fun synchronouslyUpdateViewOnUIThread (ILcom/facebook/react/bridge/ReadableMap;)V
22712272
public fun updateRootLayoutSpecs (IIIII)V
22722273
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import com.facebook.react.fabric.mounting.MountingManager;
6262
import com.facebook.react.fabric.mounting.SurfaceMountingManager;
6363
import com.facebook.react.fabric.mounting.mountitems.BatchMountItem;
64+
import com.facebook.react.fabric.mounting.mountitems.BatchedAnimatedPropsMountItem;
6465
import com.facebook.react.fabric.mounting.mountitems.DispatchCommandMountItem;
6566
import com.facebook.react.fabric.mounting.mountitems.MountItem;
6667
import com.facebook.react.fabric.mounting.mountitems.MountItemFactory;
@@ -847,6 +848,16 @@ private synchronized ViewTransitionSnapshotManager getViewTransitionSnapshotMana
847848
return mViewTransitionSnapshotManager;
848849
}
849850

851+
@SuppressWarnings("unused")
852+
@UiThread
853+
@ThreadConfined(UI)
854+
public void synchronouslyUpdateViewBatch(final int[] intBuffer, final double[] doubleBuffer) {
855+
UiThreadUtil.assertOnUiThread();
856+
857+
MountItem mountItem = new BatchedAnimatedPropsMountItem(intBuffer, doubleBuffer);
858+
mountItem.execute(mMountingManager);
859+
}
860+
850861
@SuppressLint("NotInvokedPrivateMethod")
851862
@SuppressWarnings("unused")
852863
@AnyThread
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.fabric.mounting.mountitems
9+
10+
import android.view.View
11+
import com.facebook.react.bridge.JavaOnlyArray
12+
import com.facebook.react.bridge.JavaOnlyMap
13+
import com.facebook.react.fabric.mounting.MountingManager
14+
import com.facebook.react.uimanager.ViewProps
15+
16+
/**
17+
* A [MountItem] that decodes a batched buffer of animated prop updates and applies them
18+
* synchronously. The buffer protocol encodes multiple per-view prop updates into compact int/double
19+
* arrays, which this mount item decodes into [JavaOnlyMap] props and applies via
20+
* [MountingManager.updatePropsSynchronously].
21+
*/
22+
internal class BatchedAnimatedPropsMountItem(
23+
private val intBuffer: IntArray,
24+
private val doubleBuffer: DoubleArray,
25+
) : MountItem {
26+
27+
override fun execute(mountingManager: MountingManager) {
28+
var intIdx = 0
29+
var doubleIdx = 0
30+
while (intIdx < intBuffer.size) {
31+
val command = intBuffer[intIdx++]
32+
if (command != CMD_START_OF_VIEW) {
33+
break
34+
}
35+
val viewTag = intBuffer[intIdx++]
36+
val props = JavaOnlyMap()
37+
38+
while (intIdx < intBuffer.size) {
39+
val cmd = intBuffer[intIdx++]
40+
if (cmd == CMD_END_OF_VIEW) {
41+
break
42+
}
43+
44+
when (cmd) {
45+
in CMD_OPACITY..CMD_SHADOW_RADIUS ->
46+
props.putDouble(commandToString(cmd), doubleBuffer[doubleIdx++])
47+
in CMD_BACKGROUND_COLOR..CMD_TINT_COLOR,
48+
in CMD_BORDER_COLOR..CMD_BORDER_END_COLOR ->
49+
props.putInt(commandToString(cmd), intBuffer[intIdx++])
50+
in CMD_BORDER_RADIUS..CMD_BORDER_END_END_RADIUS -> {
51+
// Border radius: value in doubleBuffer, unit in intBuffer
52+
val value = doubleBuffer[doubleIdx++]
53+
val unit = intBuffer[intIdx++]
54+
if (unit == CMD_UNIT_PX) {
55+
props.putDouble(commandToString(cmd), value)
56+
} else if (unit == CMD_UNIT_PERCENT) {
57+
props.putString(commandToString(cmd), "$value%")
58+
}
59+
}
60+
CMD_START_OF_TRANSFORM -> {
61+
val transform = JavaOnlyArray()
62+
while (intIdx < intBuffer.size) {
63+
val transformCmd = intBuffer[intIdx++]
64+
if (transformCmd == CMD_END_OF_TRANSFORM) {
65+
props.putArray(ViewProps.TRANSFORM, transform)
66+
break
67+
}
68+
val name = transformCommandToString(transformCmd)
69+
when (transformCmd) {
70+
in CMD_SCALE..CMD_SCALE_Y,
71+
CMD_PERSPECTIVE -> {
72+
val entry = JavaOnlyMap()
73+
entry.putDouble(name, doubleBuffer[doubleIdx++])
74+
transform.pushMap(entry)
75+
}
76+
in CMD_TRANSLATE_X..CMD_TRANSLATE_Y -> {
77+
val value = doubleBuffer[doubleIdx++]
78+
val unitCmd = intBuffer[intIdx++]
79+
val entry = JavaOnlyMap()
80+
if (unitCmd == CMD_UNIT_PX) {
81+
entry.putDouble(name, value)
82+
} else {
83+
entry.putString(name, "$value%")
84+
}
85+
transform.pushMap(entry)
86+
}
87+
in CMD_ROTATE..CMD_SKEW_Y -> {
88+
val angle = doubleBuffer[doubleIdx++]
89+
val unitCmd = intBuffer[intIdx++]
90+
val unitStr = if (unitCmd == CMD_UNIT_DEG) "deg" else "rad"
91+
val entry = JavaOnlyMap()
92+
entry.putString(name, "$angle$unitStr")
93+
transform.pushMap(entry)
94+
}
95+
CMD_MATRIX -> {
96+
// matrix
97+
val size = intBuffer[intIdx++]
98+
val matrix = JavaOnlyArray()
99+
for (m in 0 until size) {
100+
matrix.pushDouble(doubleBuffer[doubleIdx++])
101+
}
102+
val entry = JavaOnlyMap()
103+
entry.putArray(name, matrix)
104+
transform.pushMap(entry)
105+
}
106+
}
107+
}
108+
}
109+
}
110+
}
111+
112+
try {
113+
mountingManager.updatePropsSynchronously(viewTag, props)
114+
} catch (ex: Exception) {
115+
// Same surface-teardown race as in SynchronousMountItem.
116+
}
117+
}
118+
}
119+
120+
override fun toString(): String {
121+
val sb = StringBuilder("BATCHED UPDATE PROPS ")
122+
var intIdx = 0
123+
var doubleIdx = 0
124+
try {
125+
while (intIdx < intBuffer.size) {
126+
if (intBuffer[intIdx++] != CMD_START_OF_VIEW) break
127+
val viewTag = intBuffer[intIdx++]
128+
sb.append('[').append(viewTag).append("]: {")
129+
var firstProp = true
130+
131+
view@ while (true) {
132+
val cmd = intBuffer[intIdx++]
133+
if (cmd == CMD_END_OF_VIEW) break@view
134+
135+
if (!firstProp) sb.append(", ")
136+
firstProp = false
137+
138+
when (cmd) {
139+
in CMD_OPACITY..CMD_SHADOW_RADIUS ->
140+
sb.append(commandToString(cmd)).append('=').append(doubleBuffer[doubleIdx++])
141+
in CMD_BACKGROUND_COLOR..CMD_TINT_COLOR,
142+
in CMD_BORDER_COLOR..CMD_BORDER_END_COLOR ->
143+
sb.append(commandToString(cmd)).append('=').append(intBuffer[intIdx++])
144+
in CMD_BORDER_RADIUS..CMD_BORDER_END_END_RADIUS -> {
145+
val value = doubleBuffer[doubleIdx++]
146+
val unit = intBuffer[intIdx++]
147+
sb.append(commandToString(cmd)).append('=').append(value)
148+
if (unit == CMD_UNIT_PERCENT) sb.append('%')
149+
}
150+
CMD_START_OF_TRANSFORM -> {
151+
sb.append(ViewProps.TRANSFORM).append("=[")
152+
var firstEntry = true
153+
while (true) {
154+
val transformCmd = intBuffer[intIdx++]
155+
if (transformCmd == CMD_END_OF_TRANSFORM) break
156+
if (!firstEntry) sb.append(", ")
157+
firstEntry = false
158+
sb.append(transformCommandToString(transformCmd)).append('=')
159+
when (transformCmd) {
160+
in CMD_SCALE..CMD_SCALE_Y,
161+
CMD_PERSPECTIVE -> sb.append(doubleBuffer[doubleIdx++])
162+
in CMD_TRANSLATE_X..CMD_TRANSLATE_Y -> {
163+
sb.append(doubleBuffer[doubleIdx++])
164+
if (intBuffer[intIdx++] == CMD_UNIT_PERCENT) sb.append('%')
165+
}
166+
in CMD_ROTATE..CMD_SKEW_Y -> {
167+
sb.append(doubleBuffer[doubleIdx++])
168+
sb.append(if (intBuffer[intIdx++] == CMD_UNIT_DEG) "deg" else "rad")
169+
}
170+
CMD_MATRIX -> {
171+
val size = intBuffer[intIdx++]
172+
sb.append('[')
173+
for (i in 0 until size) {
174+
if (i > 0) sb.append(", ")
175+
sb.append(doubleBuffer[doubleIdx++])
176+
}
177+
sb.append(']')
178+
}
179+
}
180+
}
181+
sb.append(']')
182+
}
183+
}
184+
}
185+
sb.append("}; ")
186+
}
187+
} catch (t: Throwable) {
188+
sb.append("<decode failed: ").append(t.javaClass.simpleName).append('>')
189+
}
190+
return sb.toString()
191+
}
192+
193+
override fun getSurfaceId(): Int = View.NO_ID
194+
195+
companion object {
196+
// Buffer protocol commands
197+
private const val CMD_START_OF_VIEW = 1
198+
private const val CMD_START_OF_TRANSFORM = 2
199+
private const val CMD_END_OF_TRANSFORM = 3
200+
private const val CMD_END_OF_VIEW = 4
201+
private const val CMD_OPACITY = 10
202+
private const val CMD_ELEVATION = 11
203+
private const val CMD_Z_INDEX = 12
204+
private const val CMD_SHADOW_OPACITY = 13
205+
private const val CMD_SHADOW_RADIUS = 14
206+
private const val CMD_BACKGROUND_COLOR = 15
207+
private const val CMD_COLOR = 16
208+
private const val CMD_TINT_COLOR = 17
209+
private const val CMD_BORDER_RADIUS = 20
210+
private const val CMD_BORDER_TOP_LEFT_RADIUS = 21
211+
private const val CMD_BORDER_TOP_RIGHT_RADIUS = 22
212+
private const val CMD_BORDER_TOP_START_RADIUS = 23
213+
private const val CMD_BORDER_TOP_END_RADIUS = 24
214+
private const val CMD_BORDER_BOTTOM_LEFT_RADIUS = 25
215+
private const val CMD_BORDER_BOTTOM_RIGHT_RADIUS = 26
216+
private const val CMD_BORDER_BOTTOM_START_RADIUS = 27
217+
private const val CMD_BORDER_BOTTOM_END_RADIUS = 28
218+
private const val CMD_BORDER_START_START_RADIUS = 29
219+
private const val CMD_BORDER_START_END_RADIUS = 30
220+
private const val CMD_BORDER_END_START_RADIUS = 31
221+
private const val CMD_BORDER_END_END_RADIUS = 32
222+
private const val CMD_BORDER_COLOR = 40
223+
private const val CMD_BORDER_TOP_COLOR = 41
224+
private const val CMD_BORDER_BOTTOM_COLOR = 42
225+
private const val CMD_BORDER_LEFT_COLOR = 43
226+
private const val CMD_BORDER_RIGHT_COLOR = 44
227+
private const val CMD_BORDER_START_COLOR = 45
228+
private const val CMD_BORDER_END_COLOR = 46
229+
private const val CMD_TRANSLATE_X = 100
230+
private const val CMD_TRANSLATE_Y = 101
231+
private const val CMD_SCALE = 102
232+
private const val CMD_SCALE_X = 103
233+
private const val CMD_SCALE_Y = 104
234+
private const val CMD_ROTATE = 105
235+
private const val CMD_ROTATE_X = 106
236+
private const val CMD_ROTATE_Y = 107
237+
private const val CMD_ROTATE_Z = 108
238+
private const val CMD_SKEW_X = 109
239+
private const val CMD_SKEW_Y = 110
240+
private const val CMD_MATRIX = 111
241+
private const val CMD_PERSPECTIVE = 112
242+
private const val CMD_UNIT_DEG = 200
243+
private const val CMD_UNIT_PX = 202
244+
private const val CMD_UNIT_PERCENT = 203
245+
246+
@JvmStatic
247+
fun commandToString(command: Int): String =
248+
when (command) {
249+
CMD_OPACITY -> ViewProps.OPACITY
250+
CMD_ELEVATION -> ViewProps.ELEVATION
251+
CMD_Z_INDEX -> ViewProps.Z_INDEX
252+
CMD_SHADOW_OPACITY -> "shadowOpacity"
253+
CMD_SHADOW_RADIUS -> "shadowRadius"
254+
CMD_BACKGROUND_COLOR -> ViewProps.BACKGROUND_COLOR
255+
CMD_COLOR -> ViewProps.COLOR
256+
CMD_TINT_COLOR -> "tintColor"
257+
CMD_BORDER_RADIUS -> ViewProps.BORDER_RADIUS
258+
CMD_BORDER_TOP_LEFT_RADIUS -> ViewProps.BORDER_TOP_LEFT_RADIUS
259+
CMD_BORDER_TOP_RIGHT_RADIUS -> ViewProps.BORDER_TOP_RIGHT_RADIUS
260+
CMD_BORDER_TOP_START_RADIUS -> ViewProps.BORDER_TOP_START_RADIUS
261+
CMD_BORDER_TOP_END_RADIUS -> ViewProps.BORDER_TOP_END_RADIUS
262+
CMD_BORDER_BOTTOM_LEFT_RADIUS -> ViewProps.BORDER_BOTTOM_LEFT_RADIUS
263+
CMD_BORDER_BOTTOM_RIGHT_RADIUS -> ViewProps.BORDER_BOTTOM_RIGHT_RADIUS
264+
CMD_BORDER_BOTTOM_START_RADIUS -> ViewProps.BORDER_BOTTOM_START_RADIUS
265+
CMD_BORDER_BOTTOM_END_RADIUS -> ViewProps.BORDER_BOTTOM_END_RADIUS
266+
CMD_BORDER_START_START_RADIUS -> ViewProps.BORDER_START_START_RADIUS
267+
CMD_BORDER_START_END_RADIUS -> ViewProps.BORDER_START_END_RADIUS
268+
CMD_BORDER_END_START_RADIUS -> ViewProps.BORDER_END_START_RADIUS
269+
CMD_BORDER_END_END_RADIUS -> ViewProps.BORDER_END_END_RADIUS
270+
CMD_BORDER_COLOR -> ViewProps.BORDER_COLOR
271+
CMD_BORDER_TOP_COLOR -> ViewProps.BORDER_TOP_COLOR
272+
CMD_BORDER_BOTTOM_COLOR -> ViewProps.BORDER_BOTTOM_COLOR
273+
CMD_BORDER_LEFT_COLOR -> ViewProps.BORDER_LEFT_COLOR
274+
CMD_BORDER_RIGHT_COLOR -> ViewProps.BORDER_RIGHT_COLOR
275+
CMD_BORDER_START_COLOR -> ViewProps.BORDER_START_COLOR
276+
CMD_BORDER_END_COLOR -> ViewProps.BORDER_END_COLOR
277+
else -> "unknown"
278+
}
279+
280+
@JvmStatic
281+
fun transformCommandToString(command: Int): String =
282+
when (command) {
283+
CMD_TRANSLATE_X -> "translateX"
284+
CMD_TRANSLATE_Y -> "translateY"
285+
CMD_SCALE -> "scale"
286+
CMD_SCALE_X -> ViewProps.SCALE_X
287+
CMD_SCALE_Y -> ViewProps.SCALE_Y
288+
CMD_ROTATE -> "rotate"
289+
CMD_ROTATE_X -> "rotateX"
290+
CMD_ROTATE_Y -> "rotateY"
291+
CMD_ROTATE_Z -> "rotateZ"
292+
CMD_SKEW_X -> "skewX"
293+
CMD_SKEW_Y -> "skewY"
294+
CMD_MATRIX -> "matrix"
295+
CMD_PERSPECTIVE -> "perspective"
296+
else -> "unknown"
297+
}
298+
}
299+
}

0 commit comments

Comments
 (0)