react-native icon indicating copy to clipboard operation
react-native copied to clipboard

Simulate skew on Android

Open piaskowyk opened this issue 5 months ago • 5 comments

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

Screenshot 2024-01-26 at 10 22 17

3D rotation without perspective

Screenshot 2024-01-26 at 10 24 06

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. image image

Rotation transformation 3D

image image

Scale transformation 3D

image

Matrix multiplication

It is possible to use only Rotation and Scale transformation to achieve Skew 2D transformation. image image

image image

Skew is only 2D transformation so we only need to consider the 2D matrix when simulating the skew effect.

SkewX
image
SkewY
image

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
image
SkewY
image

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. image

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]

piaskowyk avatar Jan 26 '24 10:01 piaskowyk

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

analysis-bot avatar Jan 26 '24 11:01 analysis-bot

This is dope! Nice work @piaskowyk 🔥

mrousavy avatar Jan 26 '24 12:01 mrousavy

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.

NickGerleman avatar Jan 27 '24 22:01 NickGerleman

This would be an alternative solution to https://github.com/facebook/react-native/pull/38494

javache avatar Mar 14 '24 15:03 javache

@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 😅)

piaskowyk avatar Apr 23 '24 12:04 piaskowyk