microsoft / react-native-windows

A framework for building native Windows apps with React.
https://microsoft.github.io/react-native-windows/
Other
16.24k stars 1.13k forks source link

Natively driven translation transforms using Animated.Value expected results do not match JS driven if initialValue is non-zero #13452

Open slswalker opened 1 month ago

slswalker commented 1 month ago

Problem Description

There are a few bugs at play, though all possibly caused by the same issue. When attempting to use natively driven Animated.Value to transform and translate a view from a non-zero starting point, the view is rendered in the incorrect location. With work arounds we can get this to work, but there is then a further bug. If the transform value needs to be modified before or after an animation, setValue cannot be called on the Value instance. Furthermore, to fix this we can opt-out of Composition native animation, but there is no publically available way to create an Animated.Value instance starting as a natively driven non-composition value.

So to summarize the three bugs.

  1. Animated.Value cannot be create with a non-zero value and be natively driven
  2. Animated.Value cannot have setValue called when natively driven
  3. new Animated.Value(initialValue, { useNativeDriver: true, platformConfig: { useComposition: false }}) will not pass the platform config down to native. This appears to be due to React Native's AnimatedValue constructor calling __makeNative() with no params unlike other Animated variants that do pass in the platform config.

Steps To Reproduce

First bug:

  1. Create a natively driven animated value with a non-zero initialValue. const translateYValue= useAnimatedValue(100, { useNativeDriver: true };
  2. Use this value on an Animated.View style transform: <Animated.View style={{ transform: [{ translateY: translateYValue }] }}/>

Second bug:

  1. Create a natively driven animated value with a zero initialValue. const translateYValue= useAnimatedValue(0, { useNativeDriver: true };
  2. On mount, set the value to anything else useEffect(() => translateYValue.setValue(100), []);

Third bug:

1. const translateYValue = useAnimatedValue(0, { useNativeDriver: true, platformConfig: { useComposition: false }});

Expected Results

Bug #1: Value performs like JS or non-composition animation. As a developer, I can use animated values that have a non-zero initial value

Bug #2: Value performs like JS or non-composition animation. As a developer, I can set the instance's value at any point while mounted to change the value natively

Bug #3: I have a way to turn off composition animation per animated node. Instead, I have to use a hidden function __setPlatformConfig.

CLI version

12.3.6, 13.6.8

Environment

I have reproduced this in two environments.

React Native 73 App

System:
  OS: Windows 11 10.0.22631
  CPU: (24) x64 13th Gen Intel(R) Core(TM) i7-13700K
  Memory: 35.61 GB / 63.70 GB
Binaries:
  Node:
    version: 20.15.1
    path: C:\Program Files\nodejs\node.EXE
  Yarn:
    version: 1.22.19
    path: C:\Program Files (x86)\Yarn\bin\yarn.CMD
  npm:
    version: 10.7.0
    path: C:\Program Files\nodejs\npm.CMD
  Watchman: Not Found
SDKs:
  Android SDK: Not Found
  Windows SDK:
    AllowDevelopmentWithoutDevLicense: Enabled
    AllowAllTrustedApps: Enabled
    Versions:
      - 10.0.18362.0
      - 10.0.19041.0
      - 10.0.22621.0
IDEs:
  Android Studio: Not Found
  Visual Studio:
    - 17.10.35027.167 (Visual Studio Enterprise 2022)
Languages:
  Java: Not Found
  Ruby: Not Found
npmPackages:
  "@react-native-community/cli": Not Found
  react:
    installed: 18.2.0
    wanted: 18.2.0
  react-native:
    installed: 0.73.7
    wanted: 0.73.7
  react-native-windows:
    installed: 0.73.11
    wanted: 0.73.11
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: Not found
  newArchEnabled: Not found
iOS:
  hermesEnabled: Not found
  newArchEnabled: Not found

React Native 74 Default App
System:
  OS: Windows 11 10.0.22631
  CPU: (24) x64 13th Gen Intel(R) Core(TM) i7-13700K
  Memory: 35.69 GB / 63.70 GB
Binaries:
  Node:
    version: 20.15.1
    path: C:\Program Files\nodejs\node.EXE
  Yarn:
    version: 3.6.4
    path: C:\Program Files (x86)\Yarn\bin\yarn.CMD
  npm:
    version: 10.7.0
    path: C:\Program Files\nodejs\npm.CMD
  Watchman: Not Found
SDKs:
  Android SDK: Not Found
  Windows SDK:
    AllowDevelopmentWithoutDevLicense: Enabled
    AllowAllTrustedApps: Enabled
    Versions:
      - 10.0.18362.0
      - 10.0.19041.0
      - 10.0.22621.0
IDEs:
  Android Studio: Not Found
  Visual Studio:
    - 17.10.35027.167 (Visual Studio Enterprise 2022)
Languages:
  Java: Not Found
  Ruby: Not Found
npmPackages:
  "@react-native-community/cli": Not Found
  react:
    installed: 18.2.0
    wanted: 18.2.0
  react-native:
    installed: 0.74.2
    wanted: 0.74.2
  react-native-windows:
    installed: 0.74.9
    wanted: 0.74.9
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: true
  newArchEnabled: false
iOS:
  hermesEnabled: Not found
  newArchEnabled: Not found

Community Modules

No response

Target Platform Version

None

Target Device(s)

Desktop

Visual Studio Version

Visual Studio 2022

Build Configuration

Release

Snack, code example, screenshot, or link to a repository

The white box is where the content is positioned without a transform, the red box is where they are supposed to be after the transform is applied.

export function AnimationExampleSimple() {
  const transformYValueJS = useAnimatedValue(100);
  const transformYValueComposite = useAnimatedValue(100, {
    useNativeDriver: true,
  });

  return (
    <View
      style={[
        StyleSheet.absoluteFill,
        {justifyContent: 'center', alignItems: 'center'},
      ]}>
      <View
        style={{
          flexDirection: 'row',
          height: 100,
          width: 210,
          justifyContent: 'space-between',
          borderWidth: 2,
          borderColor: 'white',
        }}>
        <Animated.View
          style={{
            transform: [{translateY: transformYValueJS}],
            width: 100,
            height: 100,
            backgroundColor: 'green',
          }}
        />
        <Animated.View
          style={{
            transform: [{translateY: transformYValueComposite}],
            width: 100,
            height: 100,
            backgroundColor: 'blue',
          }}
        />
      </View>
      <View
        style={{height: 100, width: 210, borderWidth: 2, borderColor: 'red'}}
      />
    </View>
  );
}
slswalker commented 1 month ago

For anyone that needs a workaround, here is a modified version of useAnimatedValue.

type PlatformConfig = {
    useComposition?: boolean;
};

type AnimatedValueConfig = Animated.AnimatedConfig & {
    platformConfig?: PlatformConfig;
};

type AnimatedValueWithPrivateFunctions = Animated.Value & {
    __setPlatformConfig(config?: PlatformConfig): void;
};

function useAnimatedValue(
    initialValue: number,
    config?: AnimatedValueConfig,
): Animated.Value {
    const ref = useRef<null | Animated.Value>(null);
    if (ref.current == null) {
        ref.current = new Animated.Value(initialValue, config);

        if (config && "platformConfig" in config) {
            (ref.current as AnimatedValueWithPrivateFunctions).__setPlatformConfig(config.platformConfig);
        }
    }

    return ref.current;
}