Skip to content

Commit a81e94a

Browse files
javachefacebook-github-bot
authored andcommitted
Fix useNativeTransformHelper behaviour when frame size is 0 (#53978)
Summary: Pull Request resolved: #53978 Inconsistency between the previous and old version of `processTransform` - if frameSize is 0, the transform was being ignored, which is not correct when considering a fixed transform origin and a rotation animation for example. Instead, always apply the transform origin if it's set. Changelog: [Android][Fixed] Fixed representation of transforms when view is originally zero-sized Reviewed By: mdvacca Differential Revision: D83469083 fbshipit-source-id: e9ae1500f64c700708edb00b2d5871e3f224fb07
1 parent fa465c6 commit a81e94a

File tree

3 files changed

+380
-4
lines changed

3 files changed

+380
-4
lines changed

packages/react-native/ReactAndroid/src/main/jni/react/jni/TransformHelper.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ void processTransform(
3939
}
4040

4141
auto result = BaseViewProps::resolveTransform(
42-
Size(viewWidth, viewHeight), transform, transformOrigin);
42+
Size{.width = viewWidth, .height = viewHeight},
43+
transform,
44+
transformOrigin);
4345

4446
// Convert from matrix of floats to double matrix
4547
constexpr size_t MatrixSize = std::tuple_size_v<decltype(result.matrix)>;

packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -558,9 +558,6 @@ Transform BaseViewProps::resolveTransform(
558558
const Transform& transform,
559559
const TransformOrigin& transformOrigin) {
560560
auto transformMatrix = Transform{};
561-
if (frameSize.width == 0 && frameSize.height == 0) {
562-
return transformMatrix;
563-
}
564561

565562
// transform is matrix
566563
if (transform.operations.size() == 1 &&
Lines changed: 377 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
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+
#include <gtest/gtest.h>
9+
10+
#include <react/renderer/components/view/BaseViewProps.h>
11+
12+
namespace facebook::react {
13+
14+
namespace {
15+
16+
// For transforms involving rotations, use this helper to fix floating point
17+
// accuracies
18+
void expectTransformsEqual(const Transform& t1, const Transform& t2) {
19+
for (int i = 0; i < 16; i++) {
20+
EXPECT_NEAR(t1.matrix[i], t2.matrix[i], 0.0001);
21+
}
22+
}
23+
24+
} // namespace
25+
26+
class ResolveTransformTest : public ::testing::Test {
27+
protected:
28+
TransformOrigin createTransformOriginPoints(float x, float y, float z = 0) {
29+
TransformOrigin origin;
30+
origin.xy[0] = ValueUnit(x, UnitType::Point);
31+
origin.xy[1] = ValueUnit(y, UnitType::Point);
32+
origin.z = z;
33+
return origin;
34+
}
35+
36+
TransformOrigin createTransformOriginPercent(float x, float y, float z = 0) {
37+
TransformOrigin origin;
38+
origin.xy[0] = ValueUnit(x, UnitType::Percent);
39+
origin.xy[1] = ValueUnit(y, UnitType::Percent);
40+
origin.z = z;
41+
return origin;
42+
}
43+
};
44+
45+
TEST_F(ResolveTransformTest, EmptyFrameNoTransformOrigin) {
46+
Size frameSize{.width = 0, .height = 0};
47+
Transform transform = Transform::Translate(10.0, 20.0, 0.0);
48+
TransformOrigin transformOrigin; // Default (not set)
49+
50+
auto result =
51+
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);
52+
53+
// With empty frame size and no transform origin, should just apply the
54+
// transform directly
55+
EXPECT_EQ(result.matrix, transform.matrix);
56+
}
57+
58+
TEST_F(ResolveTransformTest, EmptyFrameTransformOriginPoints) {
59+
Size frameSize{.width = 0, .height = 0};
60+
Transform transform = Transform::Translate(10.0, 20.0, 0.0);
61+
TransformOrigin transformOrigin = createTransformOriginPoints(5, 8);
62+
63+
auto result =
64+
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);
65+
66+
// Should handle transform origin even with empty frame size
67+
EXPECT_EQ(result.matrix, Transform::Translate(10.0, 20.0, 0.0).matrix);
68+
}
69+
70+
TEST_F(ResolveTransformTest, EmptyFrameTransformOriginPercent) {
71+
Size frameSize{.width = 0, .height = 0};
72+
Transform transform = Transform::Translate(10.0, 20.0, 0.0);
73+
TransformOrigin transformOrigin = createTransformOriginPercent(50, 50);
74+
75+
auto result =
76+
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);
77+
78+
// Transform origin does not affect translate transform
79+
EXPECT_EQ(result.matrix, Transform::Translate(10.0, 20.0, 0.0).matrix);
80+
}
81+
82+
TEST_F(ResolveTransformTest, NonEmptyFrameNoTransformOrigin) {
83+
Size frameSize{.width = 100, .height = 200};
84+
Transform transform = Transform::Translate(10.0, 20.0, 0.0);
85+
TransformOrigin transformOrigin; // Default (not set)
86+
87+
auto result =
88+
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);
89+
90+
// Transform origin does not affect translate transform
91+
EXPECT_EQ(result.matrix, Transform::Translate(10.0, 20.0, 0.0).matrix);
92+
}
93+
94+
TEST_F(ResolveTransformTest, NonEmptyFrameTransformOriginPoints) {
95+
Size frameSize{.width = 100, .height = 200};
96+
Transform transform = Transform::Scale(2.0, 1.5, 0.);
97+
TransformOrigin transformOrigin = createTransformOriginPoints(25, 50);
98+
99+
auto result =
100+
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);
101+
102+
auto expected = Transform::Translate(25.0, 25.0, 0.0) * transform;
103+
EXPECT_EQ(result.matrix, expected.matrix);
104+
}
105+
106+
TEST_F(ResolveTransformTest, NonEmptyFrameTransformOriginPercent) {
107+
Size frameSize{.width = 100, .height = 200};
108+
Transform transform = Transform::Scale(2.0, 1.5, 0.);
109+
TransformOrigin transformOrigin =
110+
createTransformOriginPercent(25, 75); // 25% width, 75% height
111+
112+
auto result =
113+
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);
114+
115+
// Should resolve percentages: 25% of 100 = 25, 75% of 200 = 150
116+
auto expected = Transform::Translate(25.0, -25.0, 0.0) * transform;
117+
EXPECT_EQ(result.matrix, expected.matrix);
118+
}
119+
120+
TEST_F(ResolveTransformTest, IdentityTransformWithOrigin) {
121+
Size frameSize{.width = 100, .height = 200};
122+
Transform transform = Transform::Identity();
123+
TransformOrigin transformOrigin = createTransformOriginPoints(25, 50);
124+
125+
auto result =
126+
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);
127+
128+
// Even with identity transform, transform origin should still apply
129+
// translations but they should cancel out, resulting in identity
130+
EXPECT_EQ(result.matrix, transform.matrix);
131+
}
132+
133+
TEST_F(ResolveTransformTest, MultipleTransformOperations) {
134+
Size frameSize{.width = 100, .height = 200};
135+
136+
Transform transform = Transform::Identity();
137+
transform = transform * Transform::Translate(10.0, 20.0, 0.0);
138+
transform = transform * Transform::Scale(2.0, 1.5, 0.0);
139+
140+
TransformOrigin transformOrigin = createTransformOriginPercent(50, 50);
141+
142+
auto result =
143+
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);
144+
145+
EXPECT_EQ(result.matrix, transform.matrix);
146+
}
147+
148+
TEST_F(ResolveTransformTest, VariousTransformOriginPositions) {
149+
Size frameSize{.width = 100, .height = 200};
150+
Transform transform = Transform::Scale(2.0, 2.0, 0.);
151+
152+
// Test origin at top-left (0, 0)
153+
TransformOrigin topLeft = createTransformOriginPoints(0, 0);
154+
auto resultTopLeft =
155+
BaseViewProps::resolveTransform(frameSize, transform, topLeft);
156+
auto expected = Transform::Translate(50.0, 100.0, 0.0) * transform;
157+
EXPECT_EQ(resultTopLeft.matrix, expected.matrix);
158+
159+
// Test origin at center (50%, 50%)
160+
TransformOrigin center = createTransformOriginPercent(50, 50);
161+
auto resultCenter =
162+
BaseViewProps::resolveTransform(frameSize, transform, center);
163+
EXPECT_EQ(resultCenter.matrix, transform.matrix);
164+
165+
// Test origin at bottom-right (100%, 100%)
166+
TransformOrigin bottomRight = createTransformOriginPercent(100, 100);
167+
auto resultBottomRight =
168+
BaseViewProps::resolveTransform(frameSize, transform, bottomRight);
169+
expected = Transform::Translate(-50.0, -100.0, 0.0) * transform;
170+
EXPECT_EQ(resultBottomRight.matrix, expected.matrix);
171+
}
172+
173+
// Test with z-component in transform origin
174+
TEST_F(ResolveTransformTest, TransformOriginWithZComponent) {
175+
Size frameSize{.width = 100, .height = 200};
176+
Transform transform = Transform::Scale(1.5, 1.5, 0.);
177+
178+
TransformOrigin transformOrigin;
179+
transformOrigin.xy[0] = ValueUnit(50, UnitType::Point);
180+
transformOrigin.xy[1] = ValueUnit(100, UnitType::Point);
181+
transformOrigin.z = 10.0f;
182+
183+
auto result =
184+
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);
185+
auto expected = Transform::Translate(0.0, 0.0, 10.0) * transform;
186+
EXPECT_EQ(result.matrix, expected.matrix);
187+
}
188+
189+
TEST_F(ResolveTransformTest, ArbitraryTransformMatrix) {
190+
Size frameSize{.width = 100, .height = 200};
191+
192+
Transform transform;
193+
transform.operations.push_back({
194+
.type = TransformOperationType::Arbitrary,
195+
.x = ValueUnit(0, UnitType::Point),
196+
.y = ValueUnit(0, UnitType::Point),
197+
.z = ValueUnit(0, UnitType::Point),
198+
});
199+
// Set custom matrix
200+
transform.matrix = {{2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1, 0, 10, 20, 0, 1}};
201+
202+
TransformOrigin transformOrigin = createTransformOriginPoints(25, 50);
203+
204+
auto result =
205+
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);
206+
207+
auto expected = Transform::Translate(25.0, 50.0, 0.0) * transform;
208+
EXPECT_EQ(result.matrix, expected.matrix);
209+
}
210+
211+
// Test rotation with empty frame size and no transform origin
212+
TEST_F(ResolveTransformTest, RotationEmptyFrameNoTransformOrigin) {
213+
Size frameSize{.width = 0, .height = 0};
214+
Transform transform = Transform::RotateZ(M_PI / 4.0); // 45 degrees
215+
TransformOrigin transformOrigin; // Default (not set)
216+
217+
auto result =
218+
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);
219+
220+
// With empty frame size and no transform origin, should just apply the
221+
// rotation directly
222+
expectTransformsEqual(result, transform);
223+
}
224+
225+
// Test rotation with empty frame size and transform origin in points
226+
TEST_F(ResolveTransformTest, RotationEmptyFrameTransformOriginPoints) {
227+
Size frameSize{.width = 0, .height = 0};
228+
Transform transform = Transform::RotateZ(M_PI / 4.0); // 45 degrees
229+
TransformOrigin transformOrigin = createTransformOriginPoints(10, 20);
230+
231+
auto result =
232+
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);
233+
234+
// With empty frame size, center is (0, 0), so origin offset is (10, 20)
235+
auto expected = Transform::Translate(10.0, 20.0, 0.0) * transform *
236+
Transform::Translate(-10.0, -20.0, 0.0);
237+
expectTransformsEqual(result, expected);
238+
}
239+
240+
// Test rotation with empty frame size and transform origin in percentages
241+
TEST_F(ResolveTransformTest, RotationEmptyFrameTransformOriginPercent) {
242+
Size frameSize{.width = 0, .height = 0};
243+
Transform transform = Transform::RotateZ(M_PI / 6.0); // 30 degrees
244+
TransformOrigin transformOrigin = createTransformOriginPercent(50, 50);
245+
246+
auto result =
247+
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);
248+
249+
// With 0 frame size, percentages resolve to 0, so no origin offset
250+
expectTransformsEqual(result, transform);
251+
}
252+
253+
// Test rotation with non-empty frame size and no transform origin
254+
TEST_F(ResolveTransformTest, RotationNonEmptyFrameNoTransformOrigin) {
255+
Size frameSize{.width = 100, .height = 200};
256+
Transform transform = Transform::RotateZ(M_PI / 3.0); // 60 degrees
257+
TransformOrigin transformOrigin; // Default (not set)
258+
259+
auto result =
260+
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);
261+
262+
// Without transform origin, rotation should happen around default center
263+
expectTransformsEqual(result, transform);
264+
}
265+
266+
// Test rotation with non-empty frame size and transform origin in points
267+
TEST_F(ResolveTransformTest, RotationNonEmptyFrameTransformOriginPoints) {
268+
Size frameSize{.width = 100, .height = 200};
269+
Transform transform = Transform::RotateZ(M_PI / 4.0); // 45 degrees
270+
TransformOrigin transformOrigin = createTransformOriginPoints(25, 50);
271+
272+
auto result =
273+
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);
274+
275+
// Center of 100x200 frame is (50, 100), origin at (25, 50) means offset of
276+
// (-25, -50)
277+
auto expected = Transform::Translate(-25.0, -50.0, 0.0) * transform *
278+
Transform::Translate(25.0, 50.0, 0.0);
279+
expectTransformsEqual(result, expected);
280+
}
281+
282+
// Test rotation with non-empty frame size and transform origin in percentages
283+
TEST_F(ResolveTransformTest, RotationNonEmptyFrameTransformOriginPercent) {
284+
Size frameSize{.width = 100, .height = 200};
285+
Transform transform = Transform::RotateZ(M_PI / 2.0); // 90 degrees
286+
TransformOrigin transformOrigin =
287+
createTransformOriginPercent(25, 75); // 25% width, 75% height
288+
289+
auto result =
290+
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);
291+
292+
// Should resolve percentages: 25% of 100 = 25, 75% of 200 = 150
293+
// Center is (50, 100), so origin offset is (25-50, 150-100) = (-25, 50)
294+
auto expected = Transform::Translate(-25.0, 50.0, 0.0) * transform *
295+
Transform::Translate(25.0, -50.0, 0.0);
296+
expectTransformsEqual(result, expected);
297+
}
298+
299+
// Test rotation with mixed transform origin units
300+
TEST_F(ResolveTransformTest, RotationMixedTransformOriginUnits) {
301+
Size frameSize{.width = 100, .height = 200};
302+
Transform transform = Transform::RotateZ(M_PI); // 180 degrees
303+
304+
TransformOrigin transformOrigin;
305+
transformOrigin.xy[0] = ValueUnit(30, UnitType::Point); // 30 points
306+
transformOrigin.xy[1] = ValueUnit(25, UnitType::Percent); // 25% of 200 = 50
307+
transformOrigin.z = 0;
308+
309+
auto result =
310+
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);
311+
312+
// Center is (50, 100), origin is (30, 50), so offset is (-20, -50)
313+
auto expected = Transform::Translate(-20.0, -50.0, 0.0) * transform *
314+
Transform::Translate(20.0, 50.0, 0.0);
315+
expectTransformsEqual(result, expected);
316+
}
317+
318+
// Test multiple rotations (RotateX, RotateY, RotateZ)
319+
TEST_F(ResolveTransformTest, MultipleRotationsWithTransformOrigin) {
320+
Size frameSize{.width = 100, .height = 100};
321+
322+
Transform transform = Transform::Rotate(M_PI / 6.0, M_PI / 4.0, M_PI / 3.0);
323+
TransformOrigin transformOrigin = createTransformOriginPercent(50, 50);
324+
325+
auto result =
326+
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);
327+
expectTransformsEqual(result, transform);
328+
}
329+
330+
// Test rotation with z-component in transform origin
331+
TEST_F(ResolveTransformTest, RotationWithZTransformOrigin) {
332+
Size frameSize{.width = 100, .height = 200};
333+
Transform transform = Transform::RotateZ(M_PI / 4.0); // 45 degrees
334+
335+
TransformOrigin transformOrigin;
336+
transformOrigin.xy[0] = ValueUnit(50, UnitType::Point);
337+
transformOrigin.xy[1] = ValueUnit(100, UnitType::Point);
338+
transformOrigin.z = 15.0f;
339+
340+
auto result =
341+
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);
342+
343+
// Center is (50, 100), origin is (50, 100, 15), so offset is (0, 0, 15)
344+
auto expected = Transform::Translate(0.0, 0.0, 15.0) * transform *
345+
Transform::Translate(0.0, 0.0, -15.0);
346+
expectTransformsEqual(result, expected);
347+
}
348+
349+
// Test rotation at different origin positions (corners vs center)
350+
TEST_F(ResolveTransformTest, RotationDifferentOriginPositions) {
351+
Size frameSize{.width = 100, .height = 100};
352+
Transform transform = Transform::RotateZ(M_PI / 2.0); // 90 degrees
353+
354+
// Test rotation around top-left corner (0, 0)
355+
TransformOrigin topLeft = createTransformOriginPoints(0, 0);
356+
auto resultTopLeft =
357+
BaseViewProps::resolveTransform(frameSize, transform, topLeft);
358+
auto expectedTopLeft = Transform::Translate(-50.0, -50.0, 0.0) * transform *
359+
Transform::Translate(50.0, 50.0, 0.0);
360+
expectTransformsEqual(resultTopLeft, expectedTopLeft);
361+
362+
// Test rotation around center (50%, 50%)
363+
TransformOrigin center = createTransformOriginPercent(50, 50);
364+
auto resultCenter =
365+
BaseViewProps::resolveTransform(frameSize, transform, center);
366+
expectTransformsEqual(resultCenter, transform);
367+
368+
// Test rotation around bottom-right corner (100%, 100%)
369+
TransformOrigin bottomRight = createTransformOriginPercent(100, 100);
370+
auto resultBottomRight =
371+
BaseViewProps::resolveTransform(frameSize, transform, bottomRight);
372+
auto expectedBottomRight = Transform::Translate(50.0, 50.0, 0.0) * transform *
373+
Transform::Translate(-50.0, -50.0, 0.0);
374+
expectTransformsEqual(resultBottomRight, expectedBottomRight);
375+
}
376+
377+
} // namespace facebook::react

0 commit comments

Comments
 (0)