Search code examples
react-nativereact-native-animatable

Add animatable Items to Flatlist horizontal items


I have flat list horizontal like below , I am trying to add bounce effect when I swipe right and when new item gets visible to the view.

const Item = ({ title, image, index }) => (
  <Animatable.View>
    <View style={styles.item}>
      <Text>{title}</Text>
    </View>
  </Animatable.View>
);

const renderItem = ({ item, index }) => (
  <Item title={item.title} image={item.image} index={index} />
);

const [viewableItemsIndices, setViewableItemsIndices] = useState([]);

const handleVieweableItemsChanged = useCallback(
  ({ viewableItems, changed }) => {
    setViewableItemsIndices(viewableItems.map((item) => item.index));
  },
  []
);

const viewabilityConfig = useRef({
  itemVisiblePercentThreshold: 80,
}).current;
return (
  <SafeAreaView style={style.container}>
    <View style={style.flatlistContainer}>
      <FlatList
        snapToInterval={120}
        horizontal={true}
        data={DATA.map((item, i) => {
          item.isViewable = viewableItemsIndices.find((ix) => ix == i);
          return item;
        })}
        renderItem={renderItem}
        keyExtractor={(item) => item.id}
        onViewableItemsChanged={handleVieweableItemsChanged}
        viewabilityConfig={viewabilityConfig}
        extraData={viewableItemsIndices}
      />
    </View>
    <Text>Items that should be visible:</Text>
    {viewableItemsIndices.map((i) => (
      <Text> {DATA[i].title}</Text>
    ))}
  </SafeAreaView>
);

I have all the visible items in flatlist in viewableItemsIndices , now how do I following animatable animation to individual item based on its index..

 const AnimationRef = useRef(null);
  const _onPress = () => {
    if(AnimationRef) {
      AnimationRef.current?.bounce();
    }
  }
  return (
    <TouchableWithoutFeedback onPress={_onPress}>
      <Animatable.View ref={AnimationRef}>
        <Text>Bounce me!</Text>
      </Animatable.View>
    </TouchableWithoutFeedback>
  );

Edit : Below is complete code

import React , { useState, useRef,useCallback } from 'react';
import { SafeAreaView, Image , View, FlatList, StyleSheet, Text, StatusBar } from 'react-native';
import { Ionicons, MaterialCommunityIcons , FontAwesome5 } from "@expo/vector-icons";
import * as Animatable from 'react-native-animatable';
import FlatListWrapper from "../anim/flatwrapper";

const FlatListAnimation = () => {
  const DATA = [
    {
      id: 'bd7acbea-c1b1-46c2-aed5-3ad53abb28ba',
      title: 'First  Item Item Item',
      image : 'https://res.cloudinary.com/Sony-liv/image/fetch/c_fill,e_brightness:10,f_auto,fl_lossy,h_494,q_auto:low,w_344/https://origin-staticv2.sonyliv.com/portrait_thumb/scam_Lang_Protrait_Thumb.jpg'
    },
    {
      id: '3ac68afc-c605-48d3-a4f8-fbd91aa97f63',
      title: 'Second Item',
      image : 'https://res.cloudinary.com/Sony-liv/image/fetch/c_fill,e_brightness:10,f_auto,fl_lossy,h_494,q_auto:low,w_344/https://origin-staticv2.sonyliv.com/portrait_thumb/Ramsingh-CharlieA_07062021_Lang_Protrait_Thumb.jpg'
    },
    {
      id: '58694a0f-3da1-471f-bd96-145571e29d72',
      title: 'Third Item',
      image : 'https://res.cloudinary.com/Sony-liv/image/fetch/c_fill,e_brightness:10,f_auto,fl_lossy,h_494,q_auto:low,w_344/https://origin-staticv2.sonyliv.com/portrait_thumb/6028647473001.jpg'
  
    },
    {
      id: '58694a0sdsf-3da1-471f-bd96-145571e29d72',
      title: 'Third Item',
      image : 'https://res.cloudinary.com/Sony-liv/image/fetch/c_fill,e_brightness:10,f_auto,fl_lossy,h_494,q_auto:low,w_344/https://origin-staticv2.sonyliv.com/portrait_thumb/6033313370001.jpg'
  
    },
    {
      id: '58694a0f-3ddsda1-471f-bd96-145571e29d72',
      title: 'Third Item',
      image : 'https://res.cloudinary.com/Sony-liv/image/fetch/c_fill,e_brightness:10,f_auto,fl_lossy,h_494,q_auto:low,w_344/https://origin-staticv2.sonyliv.com/portrait_thumb/6033325319001_v2.jpg'
  
    },
    {
      id: 'bd7acbea-c1b1-46c2-aed5-3ad531abb28ba',
      title: 'First  Item Item Item',
      image : 'https://res.cloudinary.com/Sony-liv/image/fetch/c_fill,e_brightness:10,f_auto,fl_lossy,h_494,q_auto:low,w_344/https://origin-staticv2.sonyliv.com/portrait_thumb/scam_Lang_Protrait_Thumb.jpg'
    },
    {
      id: '3ac68afc-c605-48d3-a4f8-fbd912aa97f63',
      title: 'Second Item',
      image : 'https://res.cloudinary.com/Sony-liv/image/fetch/c_fill,e_brightness:10,f_auto,fl_lossy,h_494,q_auto:low,w_344/https://origin-staticv2.sonyliv.com/portrait_thumb/Ramsingh-CharlieA_07062021_Lang_Protrait_Thumb.jpg'
    },
    {
      id: '58694a0f-3da1-471f-bd96-1455371e29d72',
      title: 'Third Item',
      image : 'https://res.cloudinary.com/Sony-liv/image/fetch/c_fill,e_brightness:10,f_auto,fl_lossy,h_494,q_auto:low,w_344/https://origin-staticv2.sonyliv.com/portrait_thumb/6028647473001.jpg'
  
    },
    {
      id: '58694a0sdsf-3da1-471f-bd96-1445571e29d72',
      title: 'Third Item',
      image : 'https://res.cloudinary.com/Sony-liv/image/fetch/c_fill,e_brightness:10,f_auto,fl_lossy,h_494,q_auto:low,w_344/https://origin-staticv2.sonyliv.com/portrait_thumb/6033313370001.jpg'
  
    },
  ];

      const maxlimit = 20;

// Item.js
const Item = (props) => { 
  const {
    item:{image , title, isViewable},
    index
  } = props
  let animation = null
  // set your animation type base on isViewable
  if(isViewable || isViewable == 0){
    animation = ""
  }
  else{
    animation = ""
  }
  
  return (
    //add animation to Animated.View


<Animatable.View style={style.itemContainer} >


    <Image
      resizeMode="contain"
      style={styles.tinyLogo}
      source={{
        uri: image,
      }}
    />
    
    </Animatable.View>



  );
}

      
      const renderItem = ({ item , index }) => (
        <Item title={item.title} image = {item.image} index= {index} />
      );

    // store the indices of the viewableItmes
    const [ viewableItemsIndices, setViewableItemsIndices ] = useState([]);

    const handleVieweableItemsChanged = useCallback(({viewableItems, changed }) => {   
        setViewableItemsIndices(viewableItems.map(item=>item.index))
    }, []);

    // config that decides when an item is viewable
    const viewabilityConfig = useRef({
      // useRef to try to counter the view rerender thing
      itemVisiblePercentThreshold:80
    }).current;
    // wrapped handleViewChange in useCallback to try to handle the re-render error
    return (
      <SafeAreaView style={style.container}>
        <FlatListWrapper
          horizontal={true}
          //{/*give each data item an isViewable prop*/}
          data={DATA.map((item,i)=>{
            item.isViewable=viewableItemsIndices.find(ix=>ix == i)
            return item
          })}
          renderItem={item=><Item {...item}/>}
          keyExtractor={item => item.id}
          onViewableItemsChanged={({viewableItems, changed})=>{
            // set viewableItemIndices to the indices when view change
            setViewableItemsIndices(viewableItems.map(item=>item.index))
          }}
          //{/*config that decides when an item is viewable*/} 
          viewabilityConfig={{itemVisiblePercentThreshold:80}}
          extraData={viewableItemsIndices}
        />
       {/* Extra stuff that just tells you what items should be visible*/}
        <Text>Items that should be visible:</Text>
        {viewableItemsIndices.map(i=><Text key={'text-'+i}>  {DATA[i].title}</Text>)}
      </SafeAreaView>
    );
  }
  const style = StyleSheet.create({
    container:{
      padding:10,
      alignItems:'center'
    },
    
    item:{
      borderWidth:1,
      padding:5,
      borderColor:'green',
    },
    itemContainer:{
      flex:1,
      backgroundColor: 'transparent',
      marginVertical: 8,
      width:120,
      alignItems:'center',
      marginHorizontal: 3,


    }
  })

  const styles = StyleSheet.create({
    container: {
      flex: 1,
      marginTop: StatusBar.currentHeight || 0,
    },
    item: {
      flex:1,
      backgroundColor: 'transparent',
      marginVertical: 8,
      width:120,
      alignItems:'center',
      marginHorizontal: 3,
    },
    title: {
      fontSize: 32,
    },
    tinyLogo: {
      borderRadius : 4,
      width: 150,
      height:150
    },
  });

  export default FlatListAnimation; 

FlatWrapper.js

//FlatListWrapper
import React, {useRef, useState, useCallback } from 'react';
import { 
    View, StyleSheet, FlatList , 
} from 'react-native';

const FlatListWrapper = (props) => {

  // useRef to avoid onViewableItemsChange on fly error
  const viewabilityConfig = useRef({
    // useRef to try to counter the view rerender thing
    itemVisiblePercentThreshold:80
  }).current;
  // wrapped handleViewChange in useCallback to try to handle the onViewableItemsChange on fly error
  const onViewChange = useCallback(props.onViewableItemsChanged,[])
  return (
      <View style={style.flatlistContainer}>
        <FlatList
          {...props}
          horizontal={true}
          onViewableItemsChanged={onViewChange}
        />
      </View>
  );
}
const style = StyleSheet.create({
 
  flatlistContainer:{
    borderWidth:1,
    borderColor:'red',
    width:'100%',
  },
 
})
export default FlatListWrapper

Solution

  • Okay so I think the flickering was caused by the extraData prop causing reloads of the Flatlist. After re-reading the Animatable docs, I found that there was a way to access Animatable View refs and call animations from that ref. Knowing that, it was no longer necessary to force the flatlist to re-render whenever the items on screen change, and that removed all the flickering I found

    import React , { useState, useRef,useCallback } from 'react';
    import { SafeAreaView, Image , View, FlatList, StyleSheet, Text, StatusBar } from 'react-native';
    import { Ionicons, MaterialCommunityIcons , FontAwesome5 } from "@expo/vector-icons";
    import * as Animatable from 'react-native-animatable';
    import FlatListWrapper from "../components/FlatListWrapper";
    
    
    
    const FlatListAnimation = () => {
      const DATA = [
        {
          id: 'bd7acbea-c1b1-46c2-aed5-3ad53abb28ba',
          title: 'First  Item Item Item',
          image : 'https://res.cloudinary.com/Sony-liv/image/fetch/c_fill,e_brightness:10,f_auto,fl_lossy,h_494,q_auto:low,w_344/https://origin-staticv2.sonyliv.com/portrait_thumb/scam_Lang_Protrait_Thumb.jpg'
        },
        {
          id: '3ac68afc-c605-48d3-a4f8-fbd91aa97f63',
          title: 'Second Item',
          image : 'https://res.cloudinary.com/Sony-liv/image/fetch/c_fill,e_brightness:10,f_auto,fl_lossy,h_494,q_auto:low,w_344/https://origin-staticv2.sonyliv.com/portrait_thumb/Ramsingh-CharlieA_07062021_Lang_Protrait_Thumb.jpg'
        },
        {
          id: '58694a0f-3da1-471f-bd96-145571e29d72-3-3',
          title: 'Third Item',
          image : 'https://res.cloudinary.com/Sony-liv/image/fetch/c_fill,e_brightness:10,f_auto,fl_lossy,h_494,q_auto:low,w_344/https://origin-staticv2.sonyliv.com/portrait_thumb/6028647473001.jpg'
      
        },
        {
          id: '58694a0sdsf-3da1-471f-bd96-145571e29d72-4-4',
          title: 'Fourth Item',
          image : 'https://res.cloudinary.com/Sony-liv/image/fetch/c_fill,e_brightness:10,f_auto,fl_lossy,h_494,q_auto:low,w_344/https://origin-staticv2.sonyliv.com/portrait_thumb/6033313370001.jpg'
      
        },
        {
          id: '58694a0f-3ddsda1-471f-bd96-145571e29d72-5-5',
          title: 'Fifth Item',
          image : 'https://res.cloudinary.com/Sony-liv/image/fetch/c_fill,e_brightness:10,f_auto,fl_lossy,h_494,q_auto:low,w_344/https://origin-staticv2.sonyliv.com/portrait_thumb/6033325319001_v2.jpg'
      
        },
        {
          id: 'bd7acbea-c1b1-46c2-aed5-3ad531abb28ba-6-6',
          title: 'Sixth  Item Item Item',
          image : 'https://res.cloudinary.com/Sony-liv/image/fetch/c_fill,e_brightness:10,f_auto,fl_lossy,h_494,q_auto:low,w_344/https://origin-staticv2.sonyliv.com/portrait_thumb/scam_Lang_Protrait_Thumb.jpg'
        },
        {
          id: '3ac68afc-c605-48d3-a4f8-fbd912aa97f63-7-7',
          title: 'Seventh Item',
          image : 'https://res.cloudinary.com/Sony-liv/image/fetch/c_fill,e_brightness:10,f_auto,fl_lossy,h_494,q_auto:low,w_344/https://origin-staticv2.sonyliv.com/portrait_thumb/Ramsingh-CharlieA_07062021_Lang_Protrait_Thumb.jpg'
        },
        {
          id: '58694a0f-3da1-471f-bd96-1455371e29d72-8-8',
          title: 'Eighth Item',
          image : 'https://res.cloudinary.com/Sony-liv/image/fetch/c_fill,e_brightness:10,f_auto,fl_lossy,h_494,q_auto:low,w_344/https://origin-staticv2.sonyliv.com/portrait_thumb/6028647473001.jpg'
      
        },
        {
          id: '58694a0sdsf-3da1-471f-bd96-1445571e29d72-9-9',
          title: 'Ninth Item',
          image : 'https://res.cloudinary.com/Sony-liv/image/fetch/c_fill,e_brightness:10,f_auto,fl_lossy,h_494,q_auto:low,w_344/https://origin-staticv2.sonyliv.com/portrait_thumb/6033313370001.jpg'
      
        },
      ];
      const maxlimit = 20;
        // store the indices of the viewableItmes
      const [ viewableItemsIndices, setViewableItemsIndices ] = useState([]);
      const itemRefs = useRef(
        DATA.map( item=> {})
      );
      
      const isScrollingForward = useRef(true);
      // use last and current scroll position to determine scroll direction
      const lastScrollPosition = useRef(0);
      const handleScroll = ({nativeEvent})=>{
        let currentPosition = nativeEvent.contentOffset.x
        isScrollingForward.current = currentPosition > lastScrollPosition.current
        lastScrollPosition.current = currentPosition
      }
        // Item.js
      const Item = (props) => { 
        let {
          item:{ image , title, isViewable },
          index
        } = props
        const itemContainer = isViewable ? styles.itemContainer : [styles.itemContainer]
        return (
          //add animation to Animated.View
          <Animatable.View style={itemContainer} ref={ref=>itemRefs[index]=ref} >
            <Image
              resizeMode="contain"
              style={styles.tinyLogo}
              source={{
                uri: image,
              }}
            />
          </Animatable.View>
        );
      }
      return (
        <SafeAreaView style={styles.container}>
          <FlatListWrapper
            horizontal={true}
            //{/*give each data item an isViewable prop*/}
            data={DATA.map((item,i)=>{
              item.isViewable = viewableItemsIndices.findIndex(ix=>ix == i) >=0;
              return item
            })}
            renderItem={(item,i)=><Item {...item} />}
            keyExtractor={item => item.id}
            onViewableItemsChanged={({viewableItems, changed})=>{
              // setViewableItemsIndices(viewableItems.map(item=>item.index))
              viewableItems.forEach(item=>{
                let itemRef = itemRefs[item.index];
                itemRef?.transitionTo({opacity:1})
              })
              changed.forEach(item=>{
                let itemRef = itemRefs[item.index];
                if(!item.isViewable)
                  itemRef?.transitionTo({opacity:0})
              })
            }}
            //{/*config that decides when an item is viewable*/} 
            viewabilityConfig={{itemVisiblePercentThreshold:100}}
            onScroll={handleScroll}
            disableScrollMomentum={true}
          />
         {/* Extra stuff that just tells you what items should be visible*/}
          <Text>Items that should be visible:</Text>
          {viewableItemsIndices.map(i=><Text key={'text-'+i}>  {DATA[i].title}</Text>)}
        </SafeAreaView>
      );
    }
      
    const styles = StyleSheet.create({
      container: {
        // flex: 1,
        marginTop: StatusBar.currentHeight || 0,
      },
      itemContainer: {
        opacity:0
      },
      title: {
        fontSize: 32,
      },
      tinyLogo: {
        borderRadius : 4,
        width: 150,
        height:150
      },
    });
    
      export default FlatListAnimation;