Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Having a rendering issue with React-Native Animation

I am having trouble with an animation.I attempting to flip the card with two different views. I am also trying to create a scrolling effect when the user scrolls between two different cards. When the code is combined in the manner down below, it creating a bug that I cannot squash. I included an image to give a visual representation of my issue.

I appreciate any help.

:

My Life Cycle method:

componentWillMount() {
    this.animatedValue = new Animated.Value(0);
    this.value = 0;
    this.animatedValue.addListener(({ value }) => {
      this.value = value;
      this.setState({ value });
    });
    this.frontInterpolate = this.animatedValue.interpolate({
      inputRange: [0, 180],
      outputRange: ['0deg', '180deg']
    });
    this.backInterpolate = this.animatedValue.interpolate({
      inputRange: [0, 180],
      outputRange: ['180deg', '360deg']
    });
  }
}

This animation that is used to produce the flip animation:

  flipCard() { 
    if (this.value >= 90) {
      this.setState({
        isWaiting: true
      });
      Animated.spring(this.animatedValue, {
        toValue: 0,
        friction: 8,
        tension: 10
      }).start(() => {
        this.setState({
          isWaiting: false
        });
      });
    } else {
      this.setState({
        isWaiting: true
      });
      Animated.spring(this.animatedValue, {
        toValue: 180,
        friction: 8,
        tension: 10
      }).start(() => {
        this.setState({ isWaiting: false });
      });
    }
  }

This is View that is being flipped via the flipCard function. If you see in one of the views, there is a function called transitionAnimation. That is used to produce the scrolling effect.

 <View style={styles.scrollPage}>
        <View>
          <Animated.View
              style={[
                 frontAnimatedStyle,
                   styles.screen,
                    this.transitionAnimation(index)
                     ]}
                   >
                   <Text style={styles.text}>{question.question}</Text>
         </Animated.View>
             <Animated.View
               style={[
                  styles.screen,
                  backAnimatedStyle,
                    styles.back,
                     this.transitionAnimation(index)
                    ]}
                    >
                    <Text style={styles.text}>{question.answer}</Text>
                 </Animated.View>

The transitionAnimation:

transitionAnimation = index => {
    if (!this.state.isWaiting) {
      return {
        transform: [
          { perspective: 800 },
          {
            scale: xOffset.interpolate({
              inputRange: [
                (index - 1) * SCREEN_WIDTH,
                index * SCREEN_WIDTH,
                (index + 1) * SCREEN_WIDTH
              ],
              outputRange: [0.25, 1, 0.25]
            })
          },
          {
            rotateX: xOffset.interpolate({
              inputRange: [
                (index - 1) * SCREEN_WIDTH,
                index * SCREEN_WIDTH,
                (index + 1) * SCREEN_WIDTH
              ],
              outputRange: ['45deg', '0deg', '45deg']
            })
          },
          {
            rotateY: xOffset.interpolate({
              inputRange: [
                (index - 1) * SCREEN_WIDTH,
                index * SCREEN_WIDTH,
                (index + 1) * SCREEN_WIDTH
              ],
              outputRange: ['-45deg', '0deg', '45deg']
            })
          }
        ]
      };
    }
  };

My render function:

render() {
    const { flashcards } = this.state;

    return (
      <View style={styles.container}>
        <View
          style={{
            alignItems: 'flex-end',
            marginTop: 10
          }}
        >
          <Progress.Circle
            size={70}
            showsText
            progress={this.state.timer}
            formatText={text => {
              return (this.state.timer * 100).toFixed(0);
            }}
          />
        </View>
        <Animated.ScrollView
          scrollEventThrottle={16}
          onScroll={Animated.event(
            [{ nativeEvent: { contentOffset: { x: xOffset } } }],
            { useNativeDriver: true }
          )}
          horizontal
          pagingEnabled
          style={styles.scrollView}
        >
          {this.state.flashcards && this.renderCard()}
        </Animated.ScrollView>
      </View>
    );
  }
}

Animation not working properly

I also created a snack bar where you can look at the problem. https://snack.expo.io/@louis345/flaschards

like image 344
Louis345 Avatar asked May 02 '18 03:05

Louis345


People also ask

How do you check if animation is finished React Native?

The updated answer should be: . start(({finished}) => { if (finished) { console. log('animation ended!) } })

How animation works in React Native?

The animated api helps to provide time based animation based on the input/output.In this example, we will dynamically change the width and the height of the box using animated timing api. The Animated. timing() function makes use of easing functions and the value given is animated on the time.

Is React Native 60 fps?

The views in React Navigation use native components and the Animated library to deliver 60 FPS animations that are run on the native thread.

What is the use of React Native animation?

React Native provides two animations: LayoutAnimation: It is used for animated global layout transactions and, Animated: It is used to control specific values at a tiny level very interactively. React-Native provides the best animation API, which provides the ability to make different animations.

How to deal with sketchy React Native components?

The only solution to this React Native problem is patience, meticulousness, and experience. Research the modules you like, try to find applications that use them as well. Moreover, an experienced React Native developer will know the first signs of a sketchy component, so try to get one on your team.

How does the animated API work with native?

The Animated API is designed to be serializable. By using the native driver, we send everything about the animation to native before starting the animation, allowing native code to perform the animation on the UI thread without having to go through the bridge on every frame.

What are the different types of animations in React-Native?

There are two animations provided by React Native: LayoutAnimation: It is used for animated global layout transactions and, Animated: It is used to control specific values at a tiny level very interactively. React-Native provides the best animation API which provides the ability to make different animations.


1 Answers

You have lots of issues:

  1. The main problem is because you don't store the state of each card properly (if it is flipped or not). For example you can add flippedCards Array or Set to your state and update it each time you flip a card so that it could render properly after setState is called when animation ends, and for proper rendering of other cards that weren't flipped.

  2. You render and animate (flip and transition) all cards at a time, but you should render only three cards (current and neighbours), and you should flip only current card.

  3. Performance issues: you create transition styles and other functions on each render, that makes your render very slow.

  4. Other code that should be refactored.

I fixed 1 and 3 problems and refactored a bit. 2 is up to you:

import React, { Component } from 'react';
import { Animated, Dimensions, StyleSheet, Text, View, TouchableOpacity, TouchableWithoutFeedback } from 'react-native';
import { EvilIcons, MaterialIcons } from '@expo/vector-icons';

const SCREEN_WIDTH = Dimensions.get('window').width;

export default class App extends Component {
  constructor(props) {
    super(props);

    const flashcards = ['konichiwa','hi','genki desu','how are you'];

    this.state = {
      flashcards,
      flipped: flashcards.map(() => false),
      flipping: false
    };

    this.flipValue = new Animated.Value(0);

    this.frontAnimatedStyle = {
      transform: [{
        rotateY: this.flipValue.interpolate({
          inputRange: [0, 1],
          outputRange: ['0deg', '180deg']
        })
      }]
    };

    this.backAnimatedStyle = {
      transform: [{
        rotateY: this.flipValue.interpolate({
          inputRange: [0, 1],
          outputRange: ['180deg', '360deg']
        })
      }]
    };

    let xOffset = new Animated.Value(0);
    this.onScroll = Animated.event(
      [{ nativeEvent: { contentOffset: { x: xOffset } } }],
      { useNativeDriver: false }
    );

    this.transitionAnimations = this.state.flashcards.map((card, index) => ({
      transform: [
        { perspective: 800 },
        {
          scale: xOffset.interpolate({
            inputRange: [
              (index - 1) * SCREEN_WIDTH,
              index * SCREEN_WIDTH,
              (index + 1) * SCREEN_WIDTH
            ],
            outputRange: [0.25, 1, 0.25]
          })
        },
        {
          rotateX: xOffset.interpolate({
            inputRange: [
              (index - 1) * SCREEN_WIDTH,
              index * SCREEN_WIDTH,
              (index + 1) * SCREEN_WIDTH
            ],
            outputRange: ['45deg', '0deg', '45deg']
          })
        },
        {
          rotateY: xOffset.interpolate({
            inputRange: [
              (index - 1) * SCREEN_WIDTH,
              index * SCREEN_WIDTH,
              (index + 1) * SCREEN_WIDTH
            ],
            outputRange: ['-45deg', '0deg', '45deg']
          })
        }
      ]
    }));
  }

  render() {
    return (
      <View style={styles.container}>
        <Animated.ScrollView
          scrollEnabled={!this.state.flipping}
          scrollEventThrottle={16}
          onScroll={this.onScroll}
          horizontal
          pagingEnabled
          style={styles.scrollView}>
          {this.state.flashcards.map(this.renderCard)}
        </Animated.ScrollView>
      </View>
    );
  }

  renderCard = (question, index) => {
    const isFlipped = this.state.flipped[index];

    return (
      <TouchableWithoutFeedback key={index} onPress={() => this.flipCard(index)}>
        <View>

          <View style={styles.scrollPage}>
            <View>
              {(this.state.flipping || !isFlipped) && <Animated.View
                style={[
                  this.state.flipping ? this.frontAnimatedStyle : this.transitionAnimations[index],
                  styles.screen
                ]}
              >
                <Text style={styles.text}>{this.state.flashcards[index]}</Text>
              </Animated.View>}

              {(this.state.flipping || isFlipped) && <Animated.View
                style={[
                  styles.screen,
                  this.state.flipping ? this.backAnimatedStyle : this.transitionAnimations[index],
                  this.state.flipping && styles.back
                ]}
              >
                <Text style={styles.text}>{this.state.flashcards[index+1]}</Text>
              </Animated.View>}
            </View>
          </View>

          <View style={styles.iconStyle}>
            <TouchableOpacity>
              <EvilIcons name="check" size={80} color={'#5CAF25'} />
            </TouchableOpacity>
            <TouchableOpacity>
              <MaterialIcons name="cancel" size={70} color={'#b71621'} />
            </TouchableOpacity>
          </View>

        </View>
      </TouchableWithoutFeedback>
    );
  }

  flipCard = index => {
    if (this.state.flipping) return;

    let isFlipped = this.state.flipped[index];
    let flipped = [...this.state.flipped];
    flipped[index] = !isFlipped;

    this.setState({
      flipping: true,
      flipped
    });

    this.flipValue.setValue(isFlipped ? 1: 0);
    Animated.spring(this.flipValue, {
      toValue: isFlipped ? 0 : 1,
      friction: 8,
      tension: 10
    }).start(() => {
      this.setState({ flipping: false });
    });
  }
}

const styles = StyleSheet.create({
  container: {
    backgroundColor:'red',
    flex: 1,
    flexDirection: 'column',
    justifyContent: 'space-between'
  },
  scrollView: {
    flexDirection: 'row',
    backgroundColor: 'black'
  },
  scrollPage: {
    width: SCREEN_WIDTH,
    padding: 20
  },
  screen: {
    height: 400,
    justifyContent: 'center',
    alignItems: 'center',
    borderRadius: 25,
    backgroundColor: 'white',
    width: SCREEN_WIDTH - 20 * 2,
    backfaceVisibility: 'hidden'
  },
  text: {
    fontSize: 45,
    fontWeight: 'bold'
  },
  iconStyle: {
    flexDirection: 'row',
    justifyContent: 'center'
  },
  back: {
    position: 'absolute',
    top: 0,
    backfaceVisibility: 'hidden'
  }
});

At least now it works fine.

like image 110
Alexander Danilov Avatar answered Oct 11 '22 12:10

Alexander Danilov