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
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。