Search code examples
javascriptreactjsreact-nativereact-animated

How to animate mapped elements one at a time in react-native?


I mapped an object array to create a tag element with the details being mapped onto the element. And then I created an animation so on render, the tags zoom in to full scale. However, I was wanting to take it to the next step and wanted to animate each tag individually, so that each tag is animated in order one after the other. To me, this seems like a common use of animations, so how could I do it from my example? Is there any common way to do this that I am missing?

import {LeftIconsRightText} from '@atoms/LeftIconsRightText';
import {LeftTextRightCircle} from '@atoms/LeftTextRightCircle';
import {Text, TextTypes} from '@atoms/Text';
import VectorIcon, {vectorIconTypes} from '@atoms/VectorIcon';
import styled from '@styled-components';
import * as React from 'react';
import {useEffect, useRef} from 'react';
import {Animated, ScrollView} from 'react-native';

export interface ICustomerFeedbackCard {
  title: string;
  titleIconName: string[];
  tagInfo?: {feedback: string; rating: number}[];
}

export const CustomerFeedbackCard: React.FC<ICustomerFeedbackCard> = ({
  title,
  titleIconName,
  tagInfo,
  ...props
}) => {
  const FAST_ZOOM = 800;
  const START_ZOOM_SCALE = 0.25;
  const FINAL_ZOOM_SCALE = 1;
  const zoomAnim = useRef(new Animated.Value(START_ZOOM_SCALE)).current;

  /**
   * Creates an animation with a
   * set duration and scales the
   * size by a set factor to create
   * a small zoom effect
   */
  useEffect(() => {
    const zoomIn = () => {
      Animated.timing(zoomAnim, {
        toValue: FINAL_ZOOM_SCALE,
        duration: FAST_ZOOM,
        useNativeDriver: true,
      }).start();
    };
    zoomIn();
  }, [zoomAnim]);

  /**
   * Sorts all tags from highest
   * to lowest rating numbers
   * @returns void
   */

  const sortTags = () => {
    tagInfo?.sort((a, b) => b.rating - a.rating);
  };

  /**
   * Displays the all the created tags with
   * the feedback text and rating number
   * @returns JSX.Element
   */
  const displayTags = () =>
    tagInfo?.map((tag) => (
      <TagContainer
        style={[
          {
            transform: [{scale: zoomAnim}],
          },
        ]}>
        <LeftTextRightCircle feedback={tag.feedback} rating={tag.rating} />
      </TagContainer>
    ));

  return (
    <CardContainer {...props}>
      <HeaderContainer>
        <LeftIconsRightText icons={titleIconName} textDescription={title} />
        <Icon name="chevron-right" type={vectorIconTypes.SMALL} />
      </HeaderContainer>
      <ScrollOutline>
        <ScrollContainer>
          {sortTags()}
          {displayTags()}
        </ScrollContainer>
      </ScrollOutline>
      <FooterContainer>
        <TextFooter>Most recent customer compliments</TextFooter>
      </FooterContainer>
    </CardContainer>
  );
};

And here is the object array for reference:

export const FEEDBACKS = [
  {feedback: 'Good Service', rating: 5},
  {feedback: 'Friendly', rating: 2},
  {feedback: 'Very Polite', rating: 2},
  {feedback: 'Above & Beyond', rating: 1},
  {feedback: 'Followed Instructions', rating: 1},
  {feedback: 'Speedy Service', rating: 3},
  {feedback: 'Clean', rating: 4},
  {feedback: 'Accommodating', rating: 0},
  {feedback: 'Enjoyable Experience', rating: 10},
  {feedback: 'Great', rating: 8},
];

Edit: I solved it by replacing React-Native-Animated and using an Animated View and instead using Animatable and using an Animatable which has built in delay. Final solution:

const displayTags = () =>
    tagInfo?.map((tag, index) => (
      <TagContainer animation="zoomIn" duration={1000} delay={index * 1000}>
        <LeftTextRightCircle feedback={tag.feedback} rating={tag.rating} />
      </TagContainer>
    ));

Here is a gif of the animation


Solution

  • This is an interesting problem. A clean way you could approach this problem is to develop a wrapper component, DelayedZoom that will render its child component with a delayed zoom. This component would take a delay prop that you can control to add a delay for when the component should begin animation.

    function DelayedZoom({delay, speed, endScale, startScale, children}) {
      const zoomAnim = useRef(new Animated.Value(startScale)).current;
      useEffect(() => {
        const zoomIn = () => {
          Animated.timing(zoomAnim, {
            delay: delay,
            toValue: endScale,
            duration: speed,
            useNativeDriver: true,
          }).start();
        };
        zoomIn();
      }, [zoomAnim]);
    
      return (
        <Animated.View
          style={[
            {
              transform: [{scale: zoomAnim}],
            },
          ]}>
          {children}
        </Animated.View>
      );
    }
    

    After this, you can use this component as follows:

    function OtherScreen() {
      const tags = FEEDBACKS;
      const FAST_ZOOM = 800;
      const START_ZOOM_SCALE = 0.25;
      const FINAL_ZOOM_SCALE = 1;
    
      function renderTags() {
        return tags.map((tag, idx) => {
          const delay = idx * 10; // play around with this. Main thing is that you get a sense for when something should start to animate based on its index, idx.
    
          return (
            <DelayedZoom
              delay={delay}
              endScale={FINAL_ZOOM_SCALE}
              startScale={START_ZOOM_SCALE}
              speed={FAST_ZOOM}>
              {/** whatever you want to render with a delayed zoom would go here. In your case it may be TagContainer */}
              <TagContainer>
                <LeftTextRightCircle feedback={tag.feedback} rating={tag.rating} />
              </TagContainer>
            </DelayedZoom>
          );
        });
      }
    
      return <View>{renderTags()}</View>;
    }
    

    I hope this helps to point you in the right direction!

    Also some helpful resources:

    Demo

    enter image description here