react-native
react-native copied to clipboard
Simulate skew on Android
Summary:
This PR aims to address the issue of lacking a direct API for setting skew on native views in Android.
Why does this problem exist?
The lack of an API on Android to directly set skew on native views is the reason behind this problem. It is not a problem specific to React Native itself, but rather an Android platform limitation. Android restricts direct manipulation of transformations. Skew can only be applied via public methods like setRotation
, setScale
, or setTranslation
.
How is this problem solved?
While we cannot directly set the fields in the transformation matrix responsible for skew, we can employ an alternative matrix that produces an equivalent result. The idea behind this PR is to utilize a 3D rotation without perspective to simulate the skew effect. Let's examine an example involving a wall's block with the number 3.
3D rotation with perspective
3D rotation without perspective
By applying the appropriate 3D rotation without perspective, it is possible to achieve the desired skew effect.
Mathematical background
Skew transformation 2D
The skew transformation is an affine 2D matrix.
Rotation transformation 3D
Scale transformation 3D
Matrix multiplication
It is possible to use only Rotation and Scale transformation to achieve Skew 2D transformation.
Skew is only 2D transformation so we only need to consider the 2D matrix when simulating the skew effect.
SkewX
SkewY
The only thing you need to do is solve the formulas and calculate the rotation and scale values.
Solution
The matrix has an infinite number of solutions, so we can assume that one of the rotation axes has an arbitrary value to simplify the calculations.
SkewX
SkewY
Limitations
It is worth noting, that this transformation is not completely equivalent to skew. The matrix that is created by those 3d rotations (and scaling) is equal to the skew matrix only on its 2x2 sub-matrix. The other values in this matrix are however not the same as if they would be in the affine matrix for the skew transformation. This implies that this transformation will not interact well with other 3d transformations, since those additional values in the matrix, will be taken into account when multiplying with other matrices, creating a completely different matrix for 3d vectors. However when 2d transformations are used, this will behave correctly, since 2d vectors won't interact with those values during multiplication. We think that this behaviour is beneficial in comparison to the current situation.
Changelog:
[ANDROID][FIXED] Skew transformation
Test Plan:
skewX before | skewX after |
---|---|
skewY before | skewY after |
---|---|
Example code:
code
import React from 'react';
import { Text, View, StyleSheet, TextInput } from 'react-native';
import Slider from '@react-native-community/slider';
import Animated, {
useAnimatedProps,
useAnimatedStyle,
useSharedValue
} from 'react-native-reanimated';
Animated.addWhitelistedNativeProps({ text: true });
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
function App() {
const skew = useSharedValue(0);
const style = useAnimatedStyle(() => ({
transform: [
{ skewX: `${skew.value}deg` },
],
}));
const animatedProps = useAnimatedProps(() => {
const skewStr = Math.round(skew.value).toString();
return { text: skewStr + '°', defaultValue: skewStr + '°' };
});
return (
<View style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fafafa',
}}>
<Animated.View style={[{
width: 200,
height: 200,
margin: 50,
backgroundColor: '#78c9af',
alignItems: 'center',
justifyContent: 'center',
}, style]}>
<Text style={{fontSize: 45, color: '#001a72'}}>
SkewX
</Text>
<AnimatedTextInput
style={{fontSize: 45, color: '#001a72'}}
defaultValue={skew.value.toString() + '°'}
value={skew.value.toString() + '°'}
animatedProps={animatedProps}
/>
</Animated.View>
<Slider
style={styles.slider}
minimumValue={-45}
maximumValue={45}
value={0}
onValueChange={(value) => {
skew.value = value;
}}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
backgroundColor: '#232736',
padding: 20,
},
slider: {
height: 40,
width: 300,
},
});
export default App;
Co-authored with @bartlomiejbloniarz
Co-authored-by: Bartłomiej Błoniarz [email protected]
Platform | Engine | Arch | Size (bytes) | Diff |
---|---|---|---|---|
android | hermes | arm64-v8a | 16,972,237 | +8,460 |
android | hermes | armeabi-v7a | n/a | -- |
android | hermes | x86 | n/a | -- |
android | hermes | x86_64 | n/a | -- |
android | jsc | arm64-v8a | 20,356,021 | +8,471 |
android | jsc | armeabi-v7a | n/a | -- |
android | jsc | x86 | n/a | -- |
android | jsc | x86_64 | n/a | -- |
Base commit: 5f75e9b90d4f998403101ae92924778df31d36fb Branch: main
This is dope! Nice work @piaskowyk 🔥
This is really neat 🙂.
Not having played much around this area before, something I noticed when reading was that Android Canvas
exposed by overriding onDraw
gives more flexibility here, allowing setting arbitrary matrix/skew in the drawing list (unlike RenderNode
or View
).
This seems kind of appealing, to not need to be constrained to the higher level view APIs, to simulate skew as different transforms, but I don't know if the API works, or results in any de-optimizations.
Curious if this is an approach that has been tried before.
This would be an alternative solution to https://github.com/facebook/react-native/pull/38494
@javache Do you require any additional information regarding this implementation from me? Or any way that I can help you. I aim to facilitate the merging process for you (of course if you want to merge it 😅)