React Native Sectionlist item移除动画

2,097 阅读3分钟

React Native Sectionlist item移除动画

最近碰到一个需求,后端返回一组数据,以sectionlist形式呈现,对每一个section中的item(对一行行的简称为item)点击后都可以移除。在iOS中UITableView可以处理cell消失的动画。看了一边RN官方文档之后发现没有提供类似的API。于是在搜了一下,发现有类似的,不过是基于Flatlist实现的。

原文在这里: aboutreact.com/add-or-remo… 根据原文的思路,对sectionlist实现了一遍,需要注意这几个点:

  • Animated 处理item颜色渐变、位移动画
  • LayoutAnimation 处理sectionlist子组件(item)移除后,整体布局更新
  • 数据源更新,动画结束后,删除对应的数据并更新data

Simulator Screen Recording - iPhone 13 - 2022-06-21 at 01.54.57_.gif

1. 子组件动画

interface RenderItemProp {
  item: {
    itemTitle: string
    key: string
  }
  removeCallback: (key: string) => void
}

const RenderItem = ({ item, removeCallback }: RenderItemProp) => {
  const opacityRef = useRef(new Animated.Value(1)).current
  const positionRef = useRef(new Animated.ValueXY()).current
  const removeSelf = () => {
    Animated.parallel([
      Animated.timing(opacityRef, {
        duration: 500,
        toValue: 0,
        useNativeDriver: false
      }),
      Animated.timing(positionRef, {
        duration: 500,
        toValue: {
          x: Dimensions.get('screen').width,
          y: 0
        },
        useNativeDriver: false
      })
    ]).start(() => {
      removeCallback(item.key)
    })
  }
  return (
    <Animated.View
      style={[Styles.item, { ...positionRef.getLayout(), opacity: opacityRef }]}
    >
      <TouchableOpacity onPress={removeSelf}>
        <Text>{item.itemTitle}</Text>
      </TouchableOpacity>
    </Animated.View>
  )
}

使用opacityRef、positionRef作为item的透明度、位移值引用,添加removeSelf响应用户点击,点击后执行一个并行动画,parallel会对数组中的多个动画同时执行。这里是在500ms时间内,将透明度从1渐变至0(完全不透明->完全透明),位置向右移动整个屏幕宽度距离(x,y代表水平和垂直方向偏移量),子组件动画完成后需要通过removeCallback回调对sectionlist整体做一个布局动画。

2. 父组件动画

const AnimatedSeclist = () => {
  // 设置数据源
  const [data, setData] = useState(mockData)
  
  // 子组件动画完成回调
  const removeCallback = (key: string) => {
    LayoutAnimation.configureNext({
      duration: 400,
      update: {
        duration: 400,
        type: LayoutAnimation.Types.easeInEaseOut,
        property: 'scaleXY'
      }
    })
    // 更新数据源
    const nextData = produce(data, (draft) => {
      let secIndex: number, rowIndex: number
      draft.forEach((secItem, sectionIndex) => {
        secItem.data.forEach((rowItem, rowItemIndex) => {
          if (rowItem.key === key) {
            secIndex = sectionIndex
            rowIndex = rowItemIndex
          }
        })
      })
      draft[secIndex!].data.splice(rowIndex!, 1) // delete rowItem
      if (draft[secIndex!].data.length === 0) {
        draft.splice(secIndex!, 1) // if section contains empty data, remove it in sectionlist
      }
    })
    setData(nextData)
  }
  
  return (
    <SectionList
      renderItem={({ item }) => (
        <RenderItem item={item} removeCallback={removeCallback} />
      )}
      renderSectionHeader={({ section: { title } }) => {
        return <Text>{title}</Text>
      }}
      stickySectionHeadersEnabled={false}
      keyExtractor={(item) => item.key} // key一定不能重复
      sections={data}
    />
  )

子组件动画完成后,对父组件执行一个 LayoutAnimation 布局更新动画,可以指定时长duration,动画效果type,动画属性property(具体可以看下官方文档)。

3. 数据源更新

    // 更新数据源
    const nextData = produce(data, (draft) => {
      let secIndex: number, rowIndex: number
      draft.forEach((secItem, sectionIndex) => {
        secItem.data.forEach((rowItem, rowItemIndex) => {
          if (rowItem.key === key) {
            secIndex = sectionIndex
            rowIndex = rowItemIndex
          }
        })
      })
      draft[secIndex!].data.splice(rowIndex!, 1) // delete rowItem
      if (draft[secIndex!].data.length === 0) {
        draft.splice(secIndex!, 1) // if section contains empty data, remove it in sectionlist
      }
    })
    setData(nextData)

父组件动画完成后更新数据:删除所点击的子组件数据、更新数据源。通过子组件点击的回调函数传递该子组件数据所对应的唯一key,在数据源中根据该key查找到数据位置(我把它叫做sectionIndex,rowItemIndex,iOS开发的同学应该很熟悉)并删除,sectionlist数据源格式为多个对象构成的数组,每个对象又含有一个数组data字段,data中每个对象才为子组件数据。

如果我们直接在源数据上修改并setData是无效的,因为源数据的引用并没有变,这里我们可以使用immer框架提供的produce方法在修改数据源的同时复制一个新的data对象,这里把它叫做nextData。(其实也可以使用JSON.parse(JSON.stringify(data))直接拷贝一个对象,在其上做修改)。

删除点击的子组件数据需要注意是的,sectionlist含有多个section,每个section是有header显示的,当section一个子组件都没有时,我们应该把该section的数据全部删除,即draft.splice(secIndex!, 1),否则会单独显示一个header。