Search code examples
reactjsreact-nativereact-animated

React Native: How to animate a particular component?


I am making a quiz. And all options will render in for loop.

Expected Behaviour:

When I click on an option, if it is the wrong answer then it should change the background color to red and it should shake.

Below is the code I am trying.

import React, { Component } from "react";
import {
  View,
  Text,
  TouchableWithoutFeedback,
  Animated,
  Easing
} from "react-native";

class MCQOptions extends Component {
  state = {
    optionSelectedStatus: 0 // 0: unselected, 1: correct, -1: wrong
  };
  constructor() {
    super();
    this.animatedValue = new Animated.Value(0);
    this.shakeAnimValue = new Animated.Value(0);
  }

  onOptionSelected(i) {

    // this.props.showNextQuestion();

    var answer = this.props.answer;
    if (answer == i) {
      this.setState({ optionSelectedStatus: 1 });
      this.showCorrectAnimation();
    } else {
      this.setState({ optionSelectedStatus: -1 });
      this.showErrorAnimation();
    }
  }

  showErrorAnimation() {
    this.shakeAnimValue.setValue(0);
    Animated.timing(this.shakeAnimValue, {
      toValue: 1,
      duration: 300,
      easing: Easing.linear
    }).start();
  }
  showCorrectAnimation() {}

  getOptions() {
    var options = [];
    var optionSelectedStyle = styles.optionUnselected;
    var optionShadowStyle = styles.optionShadow;
    if (this.state.optionSelectedStatus == 1) {
      optionSelectedStyle = styles.optionCorrect;
      optionShadowStyle = null;
    } else if (this.state.optionSelectedStatus == -1) {
      optionSelectedStyle = styles.optionWrong;
      optionShadowStyle = null;
    }

    const marginLeft = this.shakeAnimValue.interpolate({
      inputRange: [0, 0.2, 0.4, 0.6, 0.8, 0.9, 1],
      outputRange: [0, -10, 10, -10, 10, -10, 0]
    });

    for (var i = 0; i < this.props.options.length; i++) {
      options.push(
        <TouchableWithoutFeedback
          onPress={this.onOptionSelected.bind(this, this.props.indexes[i])}
          key={"options_" + i}
        >
          <View style={styles.optionBox}>
            <View style={optionShadowStyle} />
            <Animated.Text
              style={[
                styles.option,
                optionSelectedStyle,
                { marginLeft: marginLeft }
              ]}
              key={"option" + i}
            >
              {this.props.options[i]}
            </Animated.Text>
          </View>
        </TouchableWithoutFeedback>
      );
    }
    return options;
  }

  render() {
    const marginTop = this.animatedValue.interpolate({
      inputRange: [0, 1],
      outputRange: [100, 0]
    });
    const opacity = this.animatedValue.interpolate({
      inputRange: [0, 1],
      outputRange: [0, 1]
    });

    return (
      <Animated.View style={{ marginTop: marginTop, opacity: opacity }}>
        {this.getOptions()}
      </Animated.View>
    );
  }

  // Animations
  componentDidMount() {
    this.slideUpOptionsContainer();
  }
  componentWillReceiveProps() {
    this.slideUpOptionsContainer();
    this.setState({ optionSelectedStatus: 0 });
  }
  slideUpOptionsContainer() {
    this.animatedValue.setValue(0);
    Animated.timing(this.animatedValue, {
      toValue: 1,
      duration: 300,
      easing: Easing.linear
    }).start();
  }
}

const styles = {
  optionBox: {
    margin: 5
  },
  optionsContainer: {
    marginTop: 100
  },
  option: {
    padding: 10,
    textAlign: "center",
    borderRadius: 10,
    overflow: "hidden",
    width: "100%"
  },
  optionUnselected: {
    backgroundColor: "#FFF"
  },
  optionWrong: {
    backgroundColor: "red"
  },
  optionCorrect: {
    backgroundColor: "green"
  },
  optionShadow: {
    backgroundColor: "rgba(255,255,255,0.85)",
    position: "absolute",
    width: "100%",
    height: "100%",
    left: -5,
    top: 5,
    borderRadius: 10
  }
};

export default MCQOptions;

The above code animating(shake) all the options (Which is proper according to the login written), and I am stuck how to make only the clicked option get animated instead all?

Edited:

Parent component with props feed:

class MCQ extends Component<{}> {

render() {
var options = ["yes", "no", "can't define"];
var indexes = [1,2,3];
var answer = 1;

optionsObj = <MCQOptions
        options={options}
        indexes={indexes}
        answer={answer}/>;

return (
      <View style={styles.container} >
        <View style={styles.optionsContainer}>
          {optionsObj}
        </View>
      </View>
    );
}
}
const styles = {
  container: {
    flex: 1,
    backgroundColor: "blue",
    paddingTop: 20,
    justifyContent: 'flex-start',
    padding: 20
  },

};

export default MCQ;

Second EDIT: Trying to simplify problem.

Below is the simplified code with zero props. I want to animate clicked element only.

import React, { Component } from "react";
import {
  View,
  Text,
  TouchableWithoutFeedback,
  Animated,
  Easing
} from "react-native";

class MCQOptions extends Component {
  constructor() {
    super();
    this.shakeAnimValue = new Animated.Value(0);
  }
  showErrorAnimation() {
    this.shakeAnimValue.setValue(0);
    Animated.timing(this.shakeAnimValue, {
      toValue: 1,
      duration: 300,
      easing: Easing.linear
    }).start();
  }

  getOptions() {

    const marginLeft = this.shakeAnimValue.interpolate({
      inputRange: [0, 0.2, 0.4, 0.6, 0.8, 0.9, 1],
      outputRange: [0, -10, 10, -10, 10, -10, 0]
    });
    var options = [];
    for (var i = 0; i < 4; i++) {
      options.push(
        <TouchableWithoutFeedback
          onPress={this.showErrorAnimation.bind(this)}
          key={"options_" + i}
        >
          <View style={styles.optionBox}>
            <Animated.Text style={[
                styles.option,
                { marginLeft: marginLeft }
              ]}
              key={"option" + i}
            >
              {"Option "+i}
            </Animated.Text>
          </View>
        </TouchableWithoutFeedback>
      );
    }
    return options;
  }

  render() {
    return (
      <View style={{ marginTop: 100}}>
        {this.getOptions()}
      </View>
    );
  }

}

const styles = {
  optionBox: {
    margin: 5
  },
  optionsContainer: {
    marginTop: 100
  },
  option: {
    padding: 10,
    textAlign: "center",
    borderRadius: 10,
    overflow: "hidden",
    width: "100%"
  },
  optionUnselected: {
    backgroundColor: "#FFF"
  },
  optionWrong: {
    backgroundColor: "red"
  },
};

export default MCQOptions;

Solution

  • Since you want to animate them separately, they cannot bind to the same Animated object. You have to make them multiple, for example:

    Example:

    export class App extends Component {
        constructor() {
          super();
          this.getOptions = this.getOptions.bind(this);
          this.originalOptions = [0,1,2,3];
          this.shakeAnimations = this.originalOptions.map( (i) => new Animated.Value(0) );
        }
        showErrorAnimation(i) {
          this.shakeAnimations[i].setValue(0);
          Animated.timing(this.shakeAnimations[i], {
            toValue: 1,
            duration: 300,
            easing: Easing.linear
          }).start();
        }
    
        getOptions() {
    
          var options = this.originalOptions.map( (i) => {
                const marginLeft = this.shakeAnimations[i].interpolate({
                    inputRange: [0, 0.2, 0.4, 0.6, 0.8, 0.9, 1],
                    outputRange: [0, -10, 10, -10, 10, -10, 0]
                });
                return (
                <TouchableWithoutFeedback
                    onPress={() => this.showErrorAnimation(i)}
                    key={"options_" + i}
                >
                    <View style={styles.optionBox}>
                    <Animated.Text style={[
                        styles.option,
                        { marginLeft: marginLeft }
                        ]}
                        key={"option" + i}
                    >
                        {"Option "+i}
                    </Animated.Text>
                    </View>
                </TouchableWithoutFeedback>              
              )
          });
          return options;
        }
    
        render() {
          return (
            <View style={{ marginTop: 100}}>
              {this.getOptions()}
            </View>
          );
        }
    
      }
    
      const styles = {
        optionBox: {
          margin: 5
        },
        optionsContainer: {
          marginTop: 100
        },
        option: {
          padding: 10,
          textAlign: "center",
          borderRadius: 10,
          overflow: "hidden",
          width: "100%"
        },
        optionUnselected: {
          backgroundColor: "#FFF"
        },
        optionWrong: {
          backgroundColor: "red"
        },
      };
    

    Result:

    enter image description here