Animated ScrollView中scrollHandler与scrollTo的协同使用方案
解决React Native Animated ScrollView中scrollHandler与scrollTo的协同问题
我来帮你搞定这个头疼的问题!核心矛盾就是:调用scrollTo(或者通过修改scrollY触发的滚动)会触发onScroll事件,导致scrollHandler里的状态更新重复执行;但禁用scrollHandler又会丢失手动滑动时的状态同步。咱们的思路就是给滚动加个“身份标记”,区分开手动滚动和编程式滚动,让scrollHandler只在手动滚动时干活。
解决方案步骤
- 添加一个共享值
isProgrammaticScroll,用来标记当前是否是代码触发的滚动 - 在调用编程式滚动(比如你的
incrementScroll函数)时,先把标记设为true,滚动完成后再重置为false - 在
scrollHandler里判断这个标记,只有非编程式滚动时才更新selectedIndex
修改后的完整代码
import React, { useState } from 'react'; import { Text, View, StyleSheet, ScrollView, Platform, SafeAreaView, TouchableOpacity, } from 'react-native'; import Constants from 'expo-constants'; import Animated, { Extrapolate, interpolate, runOnJS, useAnimatedRef, useAnimatedScrollHandler, useAnimatedStyle, useDerivedValue, useSharedValue, scrollTo, withTiming, } from 'react-native-reanimated'; import { Ionicons } from '@expo/vector-icons'; const data = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', ]; const ITEM_HEIGHT = 25; const ITEM_WIDTH = 200; const VISIBLE_ITEMS = 3; const FONT_SIZE = 16; const SpinnerItem = (props) => { const { idx, label, scrollY } = props; const focusedStyle = useAnimatedStyle(() => { const opacity = interpolate( scrollY.value, [(idx - 2) * ITEM_HEIGHT, (idx - 1) * ITEM_HEIGHT, idx * ITEM_HEIGHT], [0.3, 1, 0.3], Extrapolate.CLAMP ); const rotate = interpolate( scrollY.value, [(idx - 2) * ITEM_HEIGHT, (idx - 1) * ITEM_HEIGHT, idx * ITEM_HEIGHT], [-60, 0, 60], Extrapolate.CLAMP ); return { opacity, transform: [{ rotateX: rotate + 'deg' }], }; }, [idx, scrollY]); const fontStyle = useAnimatedStyle(() => { const fontStyle = interpolate( scrollY.value, [(idx - 2) * ITEM_HEIGHT, (idx - 1) * ITEM_HEIGHT, idx * ITEM_HEIGHT], [FONT_SIZE, FONT_SIZE + 8, FONT_SIZE], Extrapolate.CLAMP ); return { fontSize: fontStyle, }; }, [idx, scrollY]); return ( <Animated.View style={[ { alignItems: 'center', justifyContent: 'center', width: ITEM_WIDTH, height: ITEM_HEIGHT, }, focusedStyle, ]} > <Animated.Text allowFontScaling={false} adjustsFontSizeToFit={false} style={[styles.label, fontStyle]} > {label} </Animated.Text> </Animated.View> ); }; export default function App() { const [selectedIndex, setSelectedIndex] = useState(0); const scrollY = useSharedValue(0); const aref = useAnimatedRef(); // 新增:标记是否为编程式滚动 const isProgrammaticScroll = useSharedValue(false); const scrollHandler = useAnimatedScrollHandler({ onScroll: (event) => { scrollY.value = event.contentOffset.y; // 只有非编程式滚动时,才更新选中索引 if (!isProgrammaticScroll.value) { runOnJS(setSelectedIndex)(Math.round(scrollY.value / ITEM_HEIGHT)); } }, onScrollEnd: () => { // 滚动结束后重置标记,确保手动滚动后状态正常 isProgrammaticScroll.value = false; }, }); useDerivedValue(() => { scrollTo(aref, 0, scrollY.value, true); }); const incrementScroll = () => { 'worklet'; // 标记当前是编程式滚动 isProgrammaticScroll.value = true; scrollY.value = scrollY.value + ITEM_HEIGHT; if (scrollY.value >= (data.length - 1) * ITEM_HEIGHT) scrollY.value = 0; // 编程式滚动时直接更新选中索引,不用等scrollHandler触发 runOnJS(setSelectedIndex)(Math.round(scrollY.value / ITEM_HEIGHT)); // 滚动动画结束后重置标记 withTiming(scrollY.value, {}, () => { isProgrammaticScroll.value = false; }); }; return ( <SafeAreaView style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> <Text>{'Selected Index: ' + selectedIndex}</Text> <View style={styles.container}> <TouchableOpacity onPress={() => { incrementScroll(); }} > <Ionicons name="ios-arrow-down-sharp" size={24} color="black" /> </TouchableOpacity> <Animated.ScrollView showsVerticalScrollIndicator={false} bounces={false} ref={aref} decelerationRate={Platform.OS === 'ios' ? 0 : 0.98} renderToHardwareTextureAndroid contentContainerStyle={{ alignItems: 'center', justifyContent: 'center', }} centerContent pinchGestureEnabled={false} snapToInterval={ITEM_HEIGHT} snapToAlignment="start" onScroll={scrollHandler} scrollEventThrottle={16} > {['spacer', ...data, 'spacer'].map((item, idx) => ( <SpinnerItem key={idx} idx={idx} scrollY={scrollY} label={item === 'spacer' ? '' : item} /> ))} </Animated.ScrollView> </View> </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', alignSelf: 'center', backgroundColor: '#ecf0f1', height: ITEM_HEIGHT * 3, width: ITEM_WIDTH, overflow: 'hidden', }, label: { textAlignVertical: 'center', textAlign: 'center', color: 'red', fontSize: FONT_SIZE, fontWeight: '700', }, });
关键细节说明
- 标记区分滚动类型:
isProgrammaticScroll这个共享值在编程滚动时设为true,让scrollHandler跳过状态更新逻辑;手动滚动时保持false,正常同步状态。 - 直接更新状态:编程式滚动时,我们直接调用
setSelectedIndex,不用依赖scrollHandler,避免重复更新。 - 滚动结束重置标记:不管是编程滚动还是手动滚动,在
onScrollEnd里重置标记,确保后续操作不会被错误标记。
这样就能完美实现:点击Next按钮触发的滚动不会触发scrollHandler里的状态更新,手动滑动时又能正常同步选中索引啦!
内容的提问来源于stack exchange,提问作者Hani Q
相关产品推荐
相关产品推荐

