Skip to content

Commit 2dfe673

Browse files
Batch Android animated prop updates into one JNI call per surface: tests and scaffolding
Tests and C++ API snapshots for the change in the parent commit.
1 parent 47df773 commit 2dfe673

13 files changed

Lines changed: 914 additions & 0 deletions

File tree

‎packages/react-native/ReactAndroid/src/main/jni/react/fabric/test/AnimatedPropBufferEncoderTest.cpp‎

Lines changed: 401 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
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
9+
10+
import com.facebook.react.bridge.JavaOnlyMap
11+
import com.facebook.react.bridge.ReactNativeJNISoLoader
12+
import com.facebook.react.bridge.ReadableMap
13+
import com.facebook.react.fabric.mounting.MountItemDispatcher
14+
import com.facebook.react.fabric.mounting.MountingManager
15+
import com.facebook.react.fabric.mounting.mountitems.BatchedAnimatedPropsMountItem
16+
import com.facebook.react.uimanager.MatrixMathHelper
17+
import com.facebook.react.uimanager.TransformHelper
18+
import org.assertj.core.api.Assertions.assertThat
19+
import org.assertj.core.api.Assertions.within
20+
import org.junit.Test
21+
import org.mockito.Mockito.mockStatic
22+
import org.mockito.kotlin.mock
23+
import org.mockito.kotlin.verify
24+
import org.mockito.kotlin.whenever
25+
26+
class BatchedAnimatedPropsMountItemTest {
27+
private fun decode(
28+
ints: IntArray,
29+
doubles: DoubleArray,
30+
rawProps: Array<Any?> = emptyArray(),
31+
): List<Pair<Int, ReadableMap>> {
32+
val updates = mutableListOf<Pair<Int, ReadableMap>>()
33+
BatchedAnimatedPropsMountItem(ints, doubles, rawProps, mock()).decode { tag, props ->
34+
updates.add(tag to props)
35+
}
36+
return updates
37+
}
38+
39+
@Test
40+
fun decodesOpacityPerView() {
41+
val updates =
42+
decode(
43+
intArrayOf(
44+
CMD_START_OF_VIEW,
45+
11,
46+
CMD_OPACITY,
47+
CMD_END_OF_VIEW,
48+
CMD_START_OF_VIEW,
49+
12,
50+
CMD_OPACITY,
51+
CMD_END_OF_VIEW),
52+
doubleArrayOf(0.25, 0.75))
53+
54+
assertThat(updates.map { it.first }).containsExactly(11, 12)
55+
assertThat(updates[0].second.getDouble("opacity")).isEqualTo(0.25)
56+
assertThat(updates[1].second.getDouble("opacity")).isEqualTo(0.75)
57+
}
58+
59+
@Test
60+
fun decodesAdditionalColorProps() {
61+
val color = 0xff112233.toInt()
62+
for ((command, name) in
63+
listOf(
64+
CMD_PLACEHOLDER_TEXT_COLOR to "placeholderTextColor",
65+
CMD_SHADOW_COLOR to "shadowColor",
66+
CMD_BORDER_BLOCK_COLOR to "borderBlockColor",
67+
CMD_BORDER_BLOCK_START_COLOR to "borderBlockStartColor",
68+
CMD_BORDER_BLOCK_END_COLOR to "borderBlockEndColor",
69+
CMD_OUTLINE_COLOR to "outlineColor",
70+
)) {
71+
val props =
72+
decode(intArrayOf(CMD_START_OF_VIEW, 7, command, color, CMD_END_OF_VIEW), doubleArrayOf())
73+
.single()
74+
.second
75+
assertThat(props.getInt(name)).describedAs(name).isEqualTo(color)
76+
}
77+
}
78+
79+
@Test
80+
fun decodesOutlinePropsAcrossViews() {
81+
val updates =
82+
decode(
83+
intArrayOf(
84+
CMD_START_OF_VIEW,
85+
7,
86+
CMD_OUTLINE_OFFSET,
87+
CMD_OUTLINE_WIDTH,
88+
CMD_END_OF_VIEW,
89+
CMD_START_OF_VIEW,
90+
8,
91+
CMD_OPACITY,
92+
CMD_END_OF_VIEW),
93+
doubleArrayOf(-2.0, 1.25, 0.5))
94+
95+
assertThat(updates.map { it.first }).containsExactly(7, 8)
96+
val props = updates[0].second
97+
assertThat(props.getDouble("outlineOffset")).isEqualTo(-2.0)
98+
assertThat(props.getDouble("outlineWidth")).isEqualTo(1.25)
99+
assertThat(updates[1].second.getDouble("opacity")).isEqualTo(0.5)
100+
}
101+
102+
@Test
103+
fun decodesTransformOpsInOrder() {
104+
val updates =
105+
decode(
106+
intArrayOf(
107+
CMD_START_OF_VIEW,
108+
7,
109+
CMD_START_OF_TRANSFORM,
110+
CMD_TRANSLATE_X,
111+
CMD_UNIT_PX,
112+
CMD_ROTATE,
113+
CMD_SCALE,
114+
CMD_END_OF_TRANSFORM,
115+
CMD_END_OF_VIEW),
116+
doubleArrayOf(10.0, 1.5, 2.0))
117+
118+
val transforms = checkNotNull(updates.single().second.getArray("transform"))
119+
assertThat(transforms.size()).isEqualTo(3)
120+
assertThat(transforms.getMap(0)?.getDouble("translateX")).isEqualTo(10.0)
121+
assertThat(transforms.getMap(1)?.getDouble("rotate")).isEqualTo(1.5)
122+
assertThat(transforms.getMap(2)?.getDouble("scale")).isEqualTo(2.0)
123+
}
124+
125+
@Test
126+
fun decodesMatrixOpWithSixteenValues() {
127+
val matrix = DoubleArray(16) { it.toDouble() }
128+
val updates =
129+
decode(
130+
intArrayOf(
131+
CMD_START_OF_VIEW,
132+
7,
133+
CMD_START_OF_TRANSFORM,
134+
CMD_MATRIX,
135+
CMD_END_OF_TRANSFORM,
136+
CMD_END_OF_VIEW),
137+
matrix)
138+
139+
val transforms = checkNotNull(updates.single().second.getArray("transform"))
140+
val decoded = checkNotNull(transforms.getMap(0)?.getArray("matrix"))
141+
assertThat(decoded.size()).isEqualTo(16)
142+
assertThat(decoded.getDouble(15)).isEqualTo(15.0)
143+
}
144+
145+
@Test
146+
fun preservesScaleWithThreeDimensionalRotations() {
147+
mockStatic(ReactNativeJNISoLoader::class.java).use {
148+
for ((rotation, axis) in listOf(CMD_ROTATE_X to 0, CMD_ROTATE_Y to 1)) {
149+
val props =
150+
decode(
151+
intArrayOf(
152+
CMD_START_OF_VIEW,
153+
7,
154+
CMD_START_OF_TRANSFORM,
155+
CMD_PERSPECTIVE,
156+
CMD_SCALE,
157+
rotation,
158+
CMD_END_OF_TRANSFORM,
159+
CMD_END_OF_VIEW,
160+
),
161+
doubleArrayOf(200.0, 2.0, Math.PI / 4),
162+
)
163+
.single()
164+
.second
165+
val matrix = DoubleArray(16)
166+
TransformHelper.processTransform(
167+
checkNotNull(props.getArray("transform")),
168+
matrix,
169+
0f,
170+
0f,
171+
null,
172+
)
173+
val decomposition = MatrixMathHelper.MatrixDecompositionContext()
174+
MatrixMathHelper.decomposeMatrix(matrix, decomposition)
175+
176+
for (scale in decomposition.scale) {
177+
assertThat(scale).isCloseTo(2.0, within(0.000001))
178+
}
179+
assertThat(decomposition.rotationDegrees[axis]).isEqualTo(45.0)
180+
}
181+
}
182+
}
183+
184+
@Test
185+
fun passesRawPropsThroughAsTheWholeView() {
186+
val raw = object : ReadableMap by JavaOnlyMap.of("translateX", 4.0, "opacity", null) {}
187+
val updates =
188+
decode(
189+
intArrayOf(
190+
CMD_START_OF_VIEW,
191+
7,
192+
CMD_RAW_PROPS,
193+
CMD_END_OF_VIEW,
194+
CMD_START_OF_VIEW,
195+
8,
196+
CMD_OPACITY,
197+
CMD_END_OF_VIEW),
198+
doubleArrayOf(0.5),
199+
arrayOf(raw))
200+
201+
assertThat(updates[0]).isEqualTo(7 to raw)
202+
assertThat(updates[1].second.getDouble("opacity")).isEqualTo(0.5)
203+
}
204+
205+
@Test
206+
fun rejectsUnknownEntries() {
207+
org.junit.Assert.assertThrows(IllegalStateException::class.java) {
208+
decode(intArrayOf(CMD_START_OF_VIEW, 7, UNKNOWN_COMMAND, CMD_END_OF_VIEW), doubleArrayOf())
209+
}
210+
}
211+
212+
@Test
213+
fun decodesRadiusAndTranslationUnits() {
214+
val props =
215+
decode(
216+
intArrayOf(
217+
CMD_START_OF_VIEW,
218+
7,
219+
CMD_BORDER_RADIUS,
220+
CMD_UNIT_PX,
221+
CMD_BORDER_TOP_LEFT_RADIUS,
222+
CMD_UNIT_PERCENT,
223+
CMD_START_OF_TRANSFORM,
224+
CMD_TRANSLATE_X,
225+
CMD_UNIT_PX,
226+
CMD_TRANSLATE_Y,
227+
CMD_UNIT_PERCENT,
228+
CMD_END_OF_TRANSFORM,
229+
CMD_END_OF_VIEW),
230+
doubleArrayOf(4.0, 12.5, 10.0, 25.0),
231+
)
232+
.single()
233+
.second
234+
assertThat(props.getDouble("borderRadius")).isEqualTo(4.0)
235+
assertThat(props.getString("borderTopLeftRadius")).isEqualTo("12.5%")
236+
val transform = checkNotNull(props.getArray("transform"))
237+
assertThat(transform.getMap(0)?.getDouble("translateX")).isEqualTo(10.0)
238+
assertThat(transform.getMap(1)?.getString("translateY")).isEqualTo("25.0%")
239+
}
240+
241+
@Test
242+
fun rejectsMalformedCommandsAndUnits() {
243+
for (ints in
244+
listOf(
245+
intArrayOf(UNKNOWN_COMMAND),
246+
intArrayOf(CMD_START_OF_VIEW, 7, CMD_BORDER_RADIUS, UNKNOWN_UNIT, CMD_END_OF_VIEW),
247+
intArrayOf(
248+
CMD_START_OF_VIEW,
249+
7,
250+
CMD_START_OF_TRANSFORM,
251+
CMD_TRANSLATE_X,
252+
UNKNOWN_UNIT,
253+
CMD_END_OF_TRANSFORM,
254+
CMD_END_OF_VIEW),
255+
intArrayOf(
256+
CMD_START_OF_VIEW,
257+
7,
258+
CMD_START_OF_TRANSFORM,
259+
UNKNOWN_TRANSFORM_COMMAND,
260+
CMD_END_OF_TRANSFORM,
261+
CMD_END_OF_VIEW),
262+
intArrayOf(
263+
CMD_START_OF_VIEW,
264+
7,
265+
CMD_START_OF_TRANSFORM,
266+
CMD_MATRIX,
267+
CMD_END_OF_TRANSFORM,
268+
CMD_END_OF_VIEW),
269+
intArrayOf(CMD_START_OF_VIEW, 7, CMD_RAW_PROPS, CMD_OPACITY, CMD_END_OF_VIEW),
270+
)) {
271+
org.junit.Assert.assertThrows(RuntimeException::class.java) {
272+
decode(ints, doubleArrayOf(1.0))
273+
}
274+
}
275+
}
276+
277+
@Test
278+
fun queuesMissingViewsAndContinuesTheBatch() {
279+
val manager = mock<MountingManager>()
280+
val dispatcher = mock<MountItemDispatcher>()
281+
val second = JavaOnlyMap.of("opacity", 0.75)
282+
whenever(manager.updateAnimatedPropsSynchronously(12, second)).thenReturn(true)
283+
BatchedAnimatedPropsMountItem(
284+
intArrayOf(
285+
CMD_START_OF_VIEW,
286+
11,
287+
CMD_OPACITY,
288+
CMD_END_OF_VIEW,
289+
CMD_START_OF_VIEW,
290+
12,
291+
CMD_OPACITY,
292+
CMD_END_OF_VIEW),
293+
doubleArrayOf(0.25, 0.75),
294+
emptyArray(),
295+
dispatcher,
296+
)
297+
.execute(manager)
298+
verify(dispatcher).addMountItem(org.mockito.kotlin.any())
299+
verify(manager).updateAnimatedPropsSynchronously(12, second)
300+
}
301+
302+
@Test
303+
fun continuesAfterOneViewManagerFails() {
304+
val manager = mock<MountingManager>()
305+
val dispatcher = mock<MountItemDispatcher>()
306+
whenever(manager.updateAnimatedPropsSynchronously(11, JavaOnlyMap.of("opacity", 0.25)))
307+
.thenThrow(IllegalStateException("view failure"))
308+
BatchedAnimatedPropsMountItem(
309+
intArrayOf(
310+
CMD_START_OF_VIEW,
311+
11,
312+
CMD_OPACITY,
313+
CMD_END_OF_VIEW,
314+
CMD_START_OF_VIEW,
315+
12,
316+
CMD_OPACITY,
317+
CMD_END_OF_VIEW),
318+
doubleArrayOf(0.25, 0.75),
319+
emptyArray(),
320+
dispatcher,
321+
)
322+
.execute(manager)
323+
verify(manager).updateAnimatedPropsSynchronously(12, JavaOnlyMap.of("opacity", 0.75))
324+
}
325+
326+
private companion object {
327+
const val CMD_START_OF_VIEW = 1
328+
const val CMD_START_OF_TRANSFORM = 2
329+
const val CMD_END_OF_TRANSFORM = 3
330+
const val CMD_END_OF_VIEW = 4
331+
const val CMD_RAW_PROPS = 5
332+
const val CMD_OPACITY = 10
333+
const val CMD_PLACEHOLDER_TEXT_COLOR = 18
334+
const val CMD_SHADOW_COLOR = 19
335+
const val CMD_BORDER_RADIUS = 20
336+
const val CMD_BORDER_TOP_LEFT_RADIUS = 21
337+
const val CMD_BORDER_BLOCK_COLOR = 47
338+
const val CMD_BORDER_BLOCK_START_COLOR = 48
339+
const val CMD_BORDER_BLOCK_END_COLOR = 49
340+
const val CMD_OUTLINE_COLOR = 50
341+
const val CMD_OUTLINE_OFFSET = 51
342+
const val CMD_OUTLINE_WIDTH = 52
343+
const val CMD_TRANSLATE_X = 100
344+
const val CMD_TRANSLATE_Y = 101
345+
const val CMD_SCALE = 102
346+
const val CMD_ROTATE = 105
347+
const val CMD_ROTATE_X = 106
348+
const val CMD_ROTATE_Y = 107
349+
const val CMD_MATRIX = 111
350+
const val CMD_PERSPECTIVE = 112
351+
const val CMD_UNIT_PX = 202
352+
const val CMD_UNIT_PERCENT = 203
353+
354+
const val UNKNOWN_COMMAND = 9
355+
const val UNKNOWN_TRANSFORM_COMMAND = 999
356+
const val UNKNOWN_UNIT = 999
357+
}
358+
}

0 commit comments

Comments
 (0)