React-Native Animated lags behind in low end Android phones

Viewed 1476

I am trying to implement an animation that is supposed to be run on iOS and Android devices.

In iOS the performance seems satisfactory (tested with iPhone 6 Plus and up).

On the other hand for some Android devices the animations lag behind.

The question is, what kind of actions can be taken to avoid the performance problem (apart from the usage of useNativeDriver={true} directive, which is already in the code)?

The code is like that:

import * as React from 'react';
import {
  Animated,
  ImageBackground,
  StyleSheet,
  Image,
  Easing,
  TextInput,
  View,
  Text,
} from 'react-native';
import { PanGestureHandler, State } from 'react-native-gesture-handler';
import backImg from './background.png';

const c_initial_coordinate_left = 100;
const c_initial_coordinate_top = 100;

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {};
    this.spaceAnimatedTranslations = new Animated.ValueXY();
    this.spaceAnimatedTranslations2 = new Animated.ValueXY();
    this.spaceAnimatedTranslations3 = new Animated.ValueXY();
    this.spaceAnimatedTranslations4 = new Animated.ValueXY();
    this.spaceAnimatedTranslations5 = new Animated.ValueXY();
    this.spaceAnimatedTranslations.addListener(value => (this.spaceAnimatedTranslations_value = value));
    this.spaceAnimatedTranslations2.addListener(value => (this.spaceAnimatedTranslations_value2 = value));
    this.spaceAnimatedTranslations3.addListener(value => (this.spaceAnimatedTranslations_value3 = value));
    this.spaceAnimatedTranslations4.addListener(value => (this.spaceAnimatedTranslations_value4 = value));
    this.spaceAnimatedTranslations5.addListener(value => (this.spaceAnimatedTranslations_value5 = value));
    this._animatedStyle = {transform: [{ translateX: this.spaceAnimatedTranslations.x }, { translateY: this.spaceAnimatedTranslations.y },],};
    this._animatedStyle2 = {transform: [{ translateX: this.spaceAnimatedTranslations2.x }, { translateY: this.spaceAnimatedTranslations2.y },],};
    this._animatedStyle3 = {transform: [{ translateX: this.spaceAnimatedTranslations3.x }, { translateY: this.spaceAnimatedTranslations3.y },],};
    this._animatedStyle4 = {transform: [{ translateX: this.spaceAnimatedTranslations4.x }, { translateY: this.spaceAnimatedTranslations4.y },],};
    this._animatedStyle5 = {transform: [{ translateX: this.spaceAnimatedTranslations5.x }, { translateY: this.spaceAnimatedTranslations5.y },],};
  }

  onSpaceMove(event) {
    let l_panTranslateX = event.nativeEvent.translationX;
    let l_panTranslateY = event.nativeEvent.translationY;
    let l_panStartX = event.nativeEvent.x - event.nativeEvent.translationX;
    let l_panStartY = event.nativeEvent.y - event.nativeEvent.translationY;

    let l_animationsArray = new Array();
    l_animationsArray.push(Animated.timing(this.spaceAnimatedTranslations.y, {toValue: event.nativeEvent.translationY,duration: 0,easing: Easing.linear,}));
    l_animationsArray.push(Animated.timing(this.spaceAnimatedTranslations2.y, {toValue: event.nativeEvent.translationY,duration: 0,easing: Easing.linear,}));
    l_animationsArray.push(Animated.timing(this.spaceAnimatedTranslations3.y, {toValue: event.nativeEvent.translationY,duration: 0,easing: Easing.linear,}));
    l_animationsArray.push(Animated.timing(this.spaceAnimatedTranslations4.y, {toValue: event.nativeEvent.translationY,duration: 0,easing: Easing.linear,}));
    l_animationsArray.push(Animated.timing(this.spaceAnimatedTranslations5.y, {toValue: event.nativeEvent.translationY,duration: 0,easing: Easing.linear,}));
    l_animationsArray.push(Animated.timing(this.spaceAnimatedTranslations.x, {toValue: event.nativeEvent.translationX,duration: 0,easing: Easing.linear,}));
    l_animationsArray.push(Animated.timing(this.spaceAnimatedTranslations2.x, {toValue: event.nativeEvent.translationX,duration: 0,easing: Easing.linear,}));
    l_animationsArray.push(Animated.timing(this.spaceAnimatedTranslations3.x, {toValue: event.nativeEvent.translationX,duration: 0,easing: Easing.linear,}));
    l_animationsArray.push(Animated.timing(this.spaceAnimatedTranslations4.x, {toValue: event.nativeEvent.translationX,duration: 0,easing: Easing.linear,}));
    l_animationsArray.push(Animated.timing(this.spaceAnimatedTranslations5.x, {toValue: event.nativeEvent.translationX,duration: 0,easing: Easing.linear,}));
    Animated.parallel(l_animationsArray).start();

    this.debug_message = `\n
      event.nativeEvent.translationX: ${Math.floor(
        event.nativeEvent.translationX
      )}
      event.nativeEvent.translationY: ${Math.floor(
        event.nativeEvent.translationY
      )}
      event.nativeEvent.absoluteX: ${Math.floor(event.nativeEvent.absoluteX)}
      event.nativeEvent.absoluteY: ${Math.floor(event.nativeEvent.absoluteY)}
      event.nativeEvent.x: ${Math.floor(event.nativeEvent.x)}
      event.nativeEvent.y: ${Math.floor(event.nativeEvent.y)}
      this.spaceAnimatedTranslations_value.x: ${Math.floor(
        this.spaceAnimatedTranslations_value.x
      )}
      this.spaceAnimatedTranslations_value.y: ${Math.floor(
        this.spaceAnimatedTranslations_value.y
      )}
      this.gestureStartedX: ${Math.floor(this.gestureStartedX)}
      this.gestureStartedY: ${Math.floor(this.gestureStartedY)}
        `;
    this.forceUpdate();
  }

  onSpaceMoveCompleted(event) {
    if (event.nativeEvent.state === State.BEGAN) {
      this.spaceAnimatedTranslations.flattenOffset();
      this.spaceAnimatedTranslations2.flattenOffset();
      this.spaceAnimatedTranslations3.flattenOffset();
      this.spaceAnimatedTranslations4.flattenOffset();
      this.spaceAnimatedTranslations5.flattenOffset();
      this.spaceAnimatedTranslations.setOffset({x: Math.floor(event.nativeEvent.absoluteX - c_initial_coordinate_left),y: Math.floor(event.nativeEvent.absoluteY - c_initial_coordinate_top),});
      this.spaceAnimatedTranslations2.setOffset({x: Math.floor(event.nativeEvent.absoluteX - c_initial_coordinate_left),y: Math.floor(event.nativeEvent.absoluteY - c_initial_coordinate_top),});
      this.spaceAnimatedTranslations3.setOffset({x: Math.floor(event.nativeEvent.absoluteX - c_initial_coordinate_left),y: Math.floor(event.nativeEvent.absoluteY - c_initial_coordinate_top),});
      this.spaceAnimatedTranslations4.setOffset({x: Math.floor(event.nativeEvent.absoluteX - c_initial_coordinate_left),y: Math.floor(event.nativeEvent.absoluteY - c_initial_coordinate_top),});
      this.spaceAnimatedTranslations5.setOffset({x: Math.floor(event.nativeEvent.absoluteX - c_initial_coordinate_left),y: Math.floor(event.nativeEvent.absoluteY - c_initial_coordinate_top ),});
      this.gestureStartedX = event.nativeEvent.absoluteX;
      this.gestureStartedY = event.nativeEvent.absoluteY;
    }
    if (event.nativeEvent.state === State.END) {
      this.onSpaceMove(event);
    }
  }

  render() {
    return (
      <ImageBackground source={backImg} style={{ flex: 1 }}>
        {this.debug_message ? (
          <Text style={{ color: 'white' }}>{this.debug_message}</Text>
        ) : (
          undefined
        )}

        <PanGestureHandler
          key={`test`}
          onGestureEvent={e => this.onSpaceMove(e)}
          onHandlerStateChange={e => this.onSpaceMoveCompleted(e)}>
          <Animated.View
            ref={ref => {
              this.testAnimatedView = ref;
            }}
            style={[styles._animatable_view, this._animatedStyle]}
            useNativeDriver={true}>
            <View style={styles._box_content}>
              <Text
                style={{
                  width: '100%',
                  height: '100%',
                  fontSize: 15,
                  textAlign: 'center',
                  textAlignVertical: 'center',
                  borderRadius: 20,
                }}
                editable={false}
                ref={ref => {
                  this.textInputRef = ref;
                }}
              >
              {'Master (1) (DRAG THIS ONE)'}
              </Text>
            </View>
          </Animated.View>
        </PanGestureHandler>
          <Animated.View
            ref={ref => {
              this.testAnimatedView2 = ref;
            }}
            style={[styles._animatable_view2, this._animatedStyle2]}
            useNativeDriver={true}>
            <View style={styles._box_content}>
              <Text
                style={{
                  width: '100%',
                  height: '100%',
                  fontSize: 15,
                  textAlign: 'center',
                  textAlignVertical: 'center',
                  borderRadius: 20,
                }}
                editable={false}
                ref={ref => {
                  this.textInputRef = ref;
                }}
              >
              {'Slave (2) '}
              </Text>
            </View>
          </Animated.View>
          <Animated.View
            ref={ref => {
              this.testAnimatedView3 = ref;
            }}
            style={[styles._animatable_view3, this._animatedStyle3]}
            useNativeDriver={true}>
            <View style={styles._box_content}>
              <Text
                style={{
                  width: '100%',
                  height: '100%',
                  fontSize: 15,
                  textAlign: 'center',
                  textAlignVertical: 'center',
                  borderRadius: 20,
                }}
                editable={false}
                ref={ref => {
                  this.textInputRef = ref;
                }}
              >
              {'Slave (3) '}
              </Text>
            </View>
          </Animated.View>
          <Animated.View
            ref={ref => {
              this.testAnimatedView4 = ref;
            }}
            style={[styles._animatable_view4, this._animatedStyle4]}
            useNativeDriver={true}>
            <View style={styles._box_content}>
              <Text
                style={{
                  width: '100%',
                  height: '100%',
                  fontSize: 15,
                  textAlign: 'center',
                  textAlignVertical: 'center',
                  borderRadius: 20,
                }}
                editable={false}
                ref={ref => {
                  this.textInputRef = ref;
                }}
              >
              {'Slave (4) '}
              </Text>
            </View>
          </Animated.View>
          <Animated.View
            ref={ref => {
              this.testAnimatedView5 = ref;
            }}
            style={[styles._animatable_view5, this._animatedStyle5]}
            useNativeDriver={true}>
            <View style={styles._box_content}>
              <Text
                style={{
                  width: '100%',
                  height: '100%',
                  fontSize: 15,
                  textAlign: 'center',
                  textAlignVertical: 'center',
                  borderRadius: 20,
                }}
                editable={false}
                ref={ref => {
                  this.textInputRef = ref;
                }}
              >
              {'Slave (5) '}
              </Text>
            </View>
          </Animated.View>
      </ImageBackground>
    );
  }
}

const styles = StyleSheet.create({
  _animatable_view: {
    flex: 1,
    width: 250,
    height: 50,
    top: c_initial_coordinate_top,
    left: c_initial_coordinate_left,
    position: 'absolute',
    backgroundColor: '#ABC',
    alignItems: 'center',
    justifyContent: 'center',
    borderColor: 'gainsboro',
    borderWidth: 2,
  },
  _animatable_view2: {
    flex: 1,
    width: 250,
    height: 50,
    top: c_initial_coordinate_top + 100,
    left: c_initial_coordinate_left,
    position: 'absolute',
    backgroundColor: '#ABC',
    alignItems: 'center',
    justifyContent: 'center',
    borderColor: 'gainsboro',
    borderWidth: 2,
  },
  _animatable_view3: {
    flex: 1,
    width: 250,
    height: 50,
    top: c_initial_coordinate_top + 200,
    left: c_initial_coordinate_left,
    position: 'absolute',
    backgroundColor: '#ABC',
    alignItems: 'center',
    justifyContent: 'center',
    borderColor: 'gainsboro',
    borderWidth: 2,
  },
  _animatable_view4: {
    flex: 1,
    width: 250,
    height: 50,
    top: c_initial_coordinate_top + 300,
    left: c_initial_coordinate_left,
    position: 'absolute',
    backgroundColor: '#ABC',
    alignItems: 'center',
    justifyContent: 'center',
    borderColor: 'gainsboro',
    borderWidth: 2,
  },
  _animatable_view5: {
    flex: 1,
    width: 250,
    height: 50,
    top: c_initial_coordinate_top + 400,
    left: c_initial_coordinate_left,
    position: 'absolute',
    backgroundColor: '#ABC',
    alignItems: 'center',
    justifyContent: 'center',
    borderColor: 'gainsboro',
    borderWidth: 2,
  },
  _box_content: {
    flex: 1,
    height: '100%',
    width: '100%',
    borderRadius: Math.min(this.rectangleHeight, this.rectangleWidth) / 2,
    alignItems: 'center',
    justifyContent: 'center',
    borderColor: 'gainsboro',
    borderWidth: 2,
    opacity: 0.9,
  },
});

This can be seen in action in this snack: https://snack.expo.io/@mehmetkaplan/movetextwithgesturesingle

Additionally, in order to feel the performance problem I generated another snack, which simply animates 5 objects simultaneously. If you run this through low end Android devices, you can feel the performance problem: https://snack.expo.io/@mehmetkaplan/movetextwithgesturemulti

2 Answers

I would also like to add that useNativeDriver is NOT a prop you add to components! The usage of useNativeDriver={true} here does absolute nothing! From the docs itself, the proper usage of useNativeDriver are in the Animated.[function]s. Using the code above, the correct usage would be located here:

Animated.timing(this.spaceAnimatedTranslations5.x, {
    toValue: event.nativeEvent.translationX,
    duration: 0,
    easing: Easing.linear
})

You would need to add it as another option in the second argument as a property of the options object.

Animated.timing([Animated Value], {options})

Animated.timing(this.spaceAnimatedTranslations5.x, {
    toValue: event.nativeEvent.translationX,
    duration: 0,
    easing: Easing.linear,
    useNativeDriver: true
})

Other Animated functions you can use it with are: Animated.event or Animated.interpolate.

Animations are very processing heavy, so please make sure you use useNativeDriver whenever possible and wherever applicable.

For the future visitors:

There are 2 methods that I came up with but still for the low end phones the performance is not that satisfactory:

  1. There is practically no need to have 5 different animations for this code so only this.spaceAnimatedTranslations pushed to the animations array.

  2. Another approach is, if you want the objects to follow the drag immediately, using animations is actually unnecessary. Rather directly updating the value through .setValue is more logical. This code can be seen in action in this snack.

For item 2, the change is here:

//let l_animationsArray = new Array();
//l_animationsArray.push(Animated.timing(this.spaceAnimatedTranslations.x, {toValue: event.nativeEvent.translationX,duration: 0,easing: Easing.linear,}));
//l_animationsArray.push(Animated.timing(this.spaceAnimatedTranslations.y, {toValue: event.nativeEvent.translationY,duration: 0,easing: Easing.linear,}));
//Animated.parallel(l_animationsArray).start();
this.spaceAnimatedTranslations.setValue({x: l_panTranslateX, y: l_panTranslateY});
Related