将可动画项目添加到 Flatlist 水平项目

Add animatable Items to Flatlist horizontal items

我有如下所示的水平平面列表,我试图在向右滑动时以及在视图中看到新项目时添加弹跳效果。

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>
);

我在 viewableItemsIndices 中的 flatlist 中有所有可见项目,现在我如何根据其索引将可动画动画跟踪到单个项目..

 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>
  );

编辑:下面是完整的代码

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

您需要做的就是编辑 Item 中的动画。我用 slideIn/slideOut 替换了 fadeIn 和 fadeOut(它很糟糕,但它有动作。让它反弹需要使用 y 值):

const Item = (props) => { 
  const {
    item:{title, isViewable}
  } = props
 
  // to get bounce effect, we will animate a translation
  const translateXY = useRef(new Animated.ValueXY()).current;
  const slideValue = 40
  const slideIn = () => {
    Animated.spring(translateXY, {
      toValue: {x:slideValue,y:5},
      duration: 250,
      useNativeDriver:false
    }).start();
  };
  const slideOut = () => {
    Animated.timing(translateXY, {
      toValue: {x:-slideValue,y:5},
      duration: 250,
      useNativeDriver:false
    }).start();
  };
  
  // fade in/out base on if isViewable
  if(isViewable || isViewable == 0)
    slideIn()
  else
    slideOut()
  const initialState={transform:[{translateX:-slideValue}]}
  const animation = {transform:translateXY.getTranslateTransform()}
  return (
    //add animation to Animated.View
    <Animated.View style={[style.itemContainer,initialState,animation]}>
      <View style={style.item}>
        <Text style={style.title}>{title}</Text>
      </View>
    </Animated.View>
  );
}

那是一个很棒的图书馆!不用为 Animated.Value 操心真是太好了。

// Item.js
const Item = (props) => { 
  const {
    item:{title, isViewable},
    index
  } = props
  let animation = null
  // set your animation type base on isViewable
  if(isViewable || isViewable == 0){
    animation = "bounceInLeft"
  }
  else{
    animation = "bounceOutLeft"
  }
  
  return (
    //add animation to Animated.View
    <Animatable.View style={style.itemContainer} animation = {animation}>
      <View style={style.item}>
        <Text style={style.title}>{title}</Text>
      </View>
    </Animatable.View>
  );
}
// main component
const FlatListAnimation = () => {

  // store the indices of the viewableItmes
  const [ viewableItemsIndices, setViewableItemsIndices ] = useState([]);
  
  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'
  },
  flatlistContainer:{
    borderWidth:1,
    borderColor:'red',
    width:'50%',
    height:40
  },
  item:{
    borderWidth:1,
    padding:5,
  },
  itemContainer:{
    padding:5,
  }
})
//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:'50%',
    height:40
  },
 
})
export default FlatListWrapper

好的,所以我认为闪烁是由 extraData 道具引起的,导致 Flatlist 重新加载。重新阅读 Animatable 文档后,我发现有一种方法可以访问 Animatable View refs 并从该 ref 调用动画。知道这一点后,不再需要在屏幕上的项目发生变化时强制平面列表重新渲染,并且消除了我发现的所有闪烁

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;