React Native横向ScrollView子元素尺寸共享值存储异常问题
Reanimated横向滚动吸附功能尺寸存储异常修复
问题场景
我正在实现一款功能类似Airbnb的横向滚动筛选栏,效果如下:
需求是存储ScrollView下每个子视图元素的引用,追踪宽度不固定的各子视图宽度、内容偏移位置,实现滚动停止后自动吸附到最近子元素的效果。
当前遇到的问题:onLayout回调触发时,各子视图的尺寸数据无法正确赋值到useSharedValue定义的共享值中,最终sportTileDimensions里仅存储了运动项目列表中的1条结果。
现有问题代码如下:
const DiscoverSportFilter = () => { const sports = useSelector(getSports); const sportTileDimensions = useSharedValue({}); const position = useSharedValue(0); const animatedRef = useAnimatedRef<Animated.ScrollView>(); const scrollToNearestItem = (offset: number) => { 'worklet'; console.log('sportTileDimensions.current', sportTileDimensions.value); // do some scrolling stuff }; const scrollHandler = useAnimatedScrollHandler({ onScroll: (e, ctx) => { position.value = e.contentOffset.x; }, onMomentumEnd: (e, ctx) => { scrollToNearestItem(e.contentOffset.x); }, }); const SportTile = ({ name }) => { return ( <View onLayout={({ nativeEvent }) => { const { x, width } = nativeEvent.layout; sportTileDimensions.value = { ...sportTileDimensions.value, [name.toLowerCase()]: { x, width }, }; }} style={{ padding: 10, alignItems: 'center' }}> <CustomIcon style={{ marginBottom: 5 }} name={getSportIcon(name)} size={24} /> <Text style={{ fontSize: 12 }}>{name}</Text> </View> ); }; return ( <Animated.ScrollView ref={animatedRef} style={{ flexGrow: 0 }} horizontal showsHorizontalScrollIndicator={false} onScroll={scrollHandler} scrollEventThrottle={1}> {sports.map(({ name }) => ( <SportTile key={name} name={name} /> ))} </Animated.ScrollView> ); };
故障原因
- 根因是
SportTile组件定义在父组件DiscoverSportFilter内部,每次父组件重渲染都会生成全新的SportTile函数,onLayout回调触发时捕获的是初始状态的空共享值对象,每次赋值都是基于空对象新增单条key,之前写入的其他条目全部丢失,最终只会保留最后触发onLayout的那1条数据。 - 直接展开
sportTileDimensions.value做对象合并的写法,在Reanimated的共享值更新逻辑中不是原子操作,多个onLayout几乎同时触发时,会出现读写竞态,进一步加剧数据丢失问题。
修复方案
- 将
SportTile组件移到父组件外部定义,避免每次重渲染生成新组件导致的闭包陈旧问题 - 共享值更新改用
.modify()方法做原子更新,避免多回调同时触发时的竞态问题 - 尺寸更新逻辑可以直接封装为worklet函数传入子组件,保证更新运行在UI线程,无同步延迟
修复后的核心代码参考:
// 把SportTile移到父组件外部定义 const SportTile = ({ name, onLayoutMeasure }) => { return ( <View onLayout={({ nativeEvent }) => { const { x, width } = nativeEvent.layout; onLayoutMeasure(name.toLowerCase(), x, width); }} style={{ padding: 10, alignItems: 'center' }}> <CustomIcon style={{ marginBottom: 5 }} name={getSportIcon(name)} size={24} /> <Text style={{ fontSize: 12 }}>{name}</Text> </View> ); }; const DiscoverSportFilter = () => { const sports = useSelector(getSports); const sportTileDimensions = useSharedValue({}); const position = useSharedValue(0); const animatedRef = useAnimatedRef<Animated.ScrollView>(); // 定义原子更新的worklet方法 const updateTileDimension = (key, x, width) => { 'worklet'; sportTileDimensions.modify((old) => { 'worklet'; old[key] = { x, width }; return old; }); }; const scrollToNearestItem = (offset: number) => { 'worklet'; console.log('sportTileDimensions.current', sportTileDimensions.value); // 滚动吸附逻辑 }; const scrollHandler = useAnimatedScrollHandler({ onScroll: (e) => { position.value = e.contentOffset.x; }, onMomentumEnd: (e) => { scrollToNearestItem(e.contentOffset.x); }, }); return ( <Animated.ScrollView ref={animatedRef} style={{ flexGrow: 0 }} horizontal showsHorizontalScrollIndicator={false} onScroll={scrollHandler} scrollEventThrottle={1}> {sports.map(({ name }) => ( <SportTile key={name} name={name} onLayoutMeasure={updateTileDimension} /> ))} </Animated.ScrollView> ); };
内容的提问来源于stack exchange,提问作者s2RaVe
相关产品推荐
相关产品推荐

