javascriptreact-nativereact-native-animatable

How to handle animations on individual icons rendered dynamically?


Summary

I have an 'arrow-dropdown' expo icon in an accordion header that I'm trying to animate to flip 180 degrees when pressed, so the user knows it can be closed, or opened. These headers are rendered dynamically; I do not know how many are going to come in as it's based on how many objects a user has open in other software through a web socket API.

I got the animation to work, but it animates all the icons and not just the one I click on.

Also, to be clear of how the code works, I'm using an accordion component from the library shown in the import statement in the code section.

What I've Tried

I've tried to emulate this approach: React Native: Only Animate One of Several Elements

It appears slightly different than my problem (they are only working with font size), so I wasn't able to successfully apply this solution to my code.

I notice in their code they have the tag connected to the font size in the state. I'm certain that's why their individual icons animate on click, and not all of them. However, I'm unsure of how to apply that concept to my code.

The state contains the animation value


    this.state={
      activeSections: [],
      animValue: new Animated.Value(250),
    }

  }

This is the function called when a header is pressed.

  handleSelect = () => {
    this.state.animValue._value > 250
      ? Animated.timing(this.state.animValue, {
          toValue: 250,
          duration: 500
        }).start()
      : Animated.timing(this.state.animValue, {
          toValue: 450,
          duration: 500
        }).start();
  };

This is the function the accordion calls to render each header individually. I.E. if component mounts with 10 headers, this function gets called 10 times.

  _renderHeader = section => {

      let rotateAnimation = this.state.animValue.interpolate({
          inputRange: [250, 450],
          outputRange: ['0deg', '180deg']
      });
    const customStyle = {
      transform:[{rotate:rotateAnimation}]
    };

    return (
        <View style={styles.header}>
          <View style={styles.headerTextContainer}>
            <Text style={styles.headerText}>{section['title']}</Text>
              <Animated.View style={[{marginLeft: 10}, customStyle]}>
                <TouchableWithoutFeedback >
                  <Ionicons name="md-arrow-dropdown" size={20} color={EStyleSheet.value('$textColor')} />
                </TouchableWithoutFeedback>
              </Animated.View>
          </View>

        </View>
    );
  };

Below is the accordion function that fires off whenever a header is clicked. This is where I fire off the animation function.

 _updateSections = activeSections => {
    console.log(activeSections)
    this.handleSelect()


   //unrelated code...


Additional Info

I did try to model an attempt similar to the snack link. It involved changing the state to :

    this.state={
      activeSections: [],
      animValue: [new Animated.Value(250),new Animated.Value(250),new Animated.Value(250),new Animated.Value(250),new Animated.Value(250),]
    }

And adjusting all the other functions to handle input with an array value. I also set the style of the icon to have fontSize of this.state.animValue, to try to individualize each component.

I did encounter this error during my attempt:

TypeError: TypeError: undefined is not an object (evaluating '_this.state.animValue[i].interpolate')


Solution

  • Solution!

    After a couple long hours, I was able to find a solution! Posting for those who may have similar issues.

    First you need to edit your state. This allows each icon to have their own Animated.Value to use. I hard coded many new animated.values in for visibility, but for dynamic allocation, I will be pushing a new value, then assigning it in the next steps below.

          animValue: [
            new Animated.Value(250),
            new Animated.Value(250),
            new Animated.Value(250),
            new Animated.Value(250),
            new Animated.Value(250),
            new Animated.Value(250),
            new Animated.Value(250),
          ],
    

    Now I can assign a transformation to a unique value in state. That way, each icon acts independently of one another.

    <Animated.View key={1} style={[{marginLeft: 10}, {transform:[{rotate: this.state.animValue[i].interpolate({
            inputRange: [250, 450],
            outputRange: ['0deg', '180deg']})}]}]}>
    

    I made one simple change to my HandleSelect function. I added an argument to take in an index, so it can choose the right unique animated value to animate.

    handleSelect = (i) => {
        this.state.animValue[i]._value > 250
          ? Animated.timing(this.state.animValue[i], {
              toValue: 250,
              duration: 500
            }).start()
          : Animated.timing(this.state.animValue[i], {
              toValue: 450,
              duration: 500
            }).start();
      };
    

    Hope this helps any lost soul who may have the same problem as me!