react-native
react-native copied to clipboard
feat: transform-origin
Checklist
- [x] Number/Percentage values.
- [x]
top,bottom,center,left,rightenum support. - [x] Old architecture support.
- [x] Add examples
Summary
Why?
By default, rotate/scale/skew transforms occur around the center of the View. If we want to perform them around the top-left of the view, we need to first translate to the top/left point, perform the transform, and then translate back to the center. This can be achieved with the following steps:
transform: [
{ translateX: -viewWidth / 2 },
{ translateY: -viewHeight / 2 },
{ scale: animatedV.value },
{ translateX: viewWidth / 2 },
{ translateY: viewHeight / 2 }
]
The above approach requires View dimensions, that need to be calculated using onLayout, measure, or using constants. Transform origin can simplify it and also bring it closer to the web feature parity.
Approach
Scale/Rotation/Skew are affected by transform-origin. Transform origin is affected by the dimension of the view (as it supports % values). So, we use layoutSubviews on iOS and layoutListener on Android. These callbacks retrigger the transformation with a new transform-origin. Checkout the applyTransformWithTransformOrigin method in both platforms.
Changelog:
[GENERAL] [ADDED] - transform-origin.
Test Plan:
- Run pod install/rebuild android RN tester app and test transform origin example in Transform examples.
- Test on fabric/paper architecture.
| Platform | Engine | Arch | Size (bytes) | Diff |
|---|---|---|---|---|
| android | hermes | arm64-v8a | 9,043,848 | -1,379 |
| android | hermes | armeabi-v7a | 8,292,445 | -2,054 |
| android | hermes | x86 | 9,561,193 | -227 |
| android | hermes | x86_64 | 9,401,751 | -2,002 |
| android | jsc | arm64-v8a | 9,603,220 | -1,199 |
| android | jsc | armeabi-v7a | 8,729,244 | -1,890 |
| android | jsc | x86 | 9,691,421 | -44 |
| android | jsc | x86_64 | 9,935,977 | -1,831 |
Base commit: c803a5bfa12a4305daa846c94e8112a7661a8dfc Branch: main
High-level thought on the approach here: Could we applytransformOrigin as we calculate the transform matrix on the ShadowNode/ShadowView instead, and avoid having to mutate the transform in the mounting layer?
An example of what's going to be incorrect here now is any of the measure callbacks which respect transforms: https://github.com/facebook/react-native/blob/0e83c53122f871f54c562b945e3cb9e2c27c406f/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp#L101
High-level thought on the approach here: Could we applytransformOrigin as we calculate the transform matrix on the ShadowNode/ShadowView instead, and avoid having to mutate the transform in the mounting layer?
i am currently mutating it from the mounting layer because we need the View dimensions for transform origin. From high level i can think of two approaches, either get the transform in shadow node calculation from the native view or create a state for transforms in shadow node and reset it from the mounting layer. Does this make any sense?
i am currently mutating it from the mounting layer because we need the View dimensions for transform origin.
ShadowNode / ShadowView have the full layout information available, so ideally getTransform correctly represents the transform, accounting for the transformOrigin. I'd recommend making the change at that layer.
Please also add a test that shows this works correctly when animations of the transform property are used.
@javache can you check this approach? https://github.com/facebook/react-native/compare/main...intergalacticspacehighway:react-native:f/transform-origin-resolve?expand=1
It adds a resolveTransform function that returns a new transform based on the transform-origin and layoutMetrics. This makes sure we get the updated transform in shadow node's getTransform. This won't require us to mutate the transform prop. (only tested on fabric iOS. If this looks good, I can try to add it for Android/old Arch)
@intergalacticspacehighway That looks very promising! Can you add it here, and I'll add some comments?
@javache sorry for the delay, been out for a trip. I just merged the new approach here so you can review. I'll add the support for old arch and android.
@javache I've updated the code so it works on old/new arch + android/ios. However, I think there is a fundamental issue with the approach I have taken to make it work on Android and iOS old arch. Ideally, I want to implement a solution where it triggers transform updates from cpp/shadow node realm, so I don't have to repeat things for respective platforms. i.e. once we have the layout info in shadow nodes we can trigger an imperative update call to native side to update the view transforms and not use the existing setter prop approach. wdyt? I'll have to dig more for this. Let me know what you think! The current approach works with native driver as well but has some redundancy/not very clean.
@javache Thank you for reviewing. I have made separate PRs and incorporated the changes you suggested. We can discuss it there. iOS old arch - https://github.com/facebook/react-native/pull/38626 Android - https://github.com/facebook/react-native/pull/38558 iOS fabric arch - https://github.com/facebook/react-native/pull/38559
Sorry for the delayed reviews, I was on parental leave. I'll get to this this week!
Closing this PR as reviews are happening on the split PR's.