React Native Reanimated动画样式插值失效问题排查
React Native滚轮选择器透明度动画修复方案
问题根源
- 全局动画样式复用:当前的
animatedStyle是组件级全局样式,所有列表项共用同一个透明度值,导致所有项透明度完全一致,无法呈现差异效果。 - 插值逻辑错误:直接用滚动偏移量
eventY.value作为插值输入,没有区分每个列表项的位置,无法根据项与选中区域的距离计算透明度。
修复方案
核心思路
给每个列表项创建独立的动画样式,基于项的索引与当前选中项索引的相对位置计算透明度:
- 计算当前选中项的索引:
activeIndex = eventY.value / ITEM_HEIGHT - 对每个项,计算它与选中项的相对距离:
distance = index - activeIndex - 根据相对距离插值透明度:距离0(选中项)时透明度1,距离±1时透明度0.5,距离±2及以上时透明度0.1
完整修复代码
import { StyleSheet, View, Modal } from "react-native"; import React from "react"; import { LinearGradient } from "expo-linear-gradient"; import Text from "../atoms/Text"; import Animated, { Extrapolation, interpolate, useAnimatedScrollHandler, useAnimatedStyle, useSharedValue, } from "react-native-reanimated"; import Button from "../atoms/Button"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { COLORS, DEVICE } from "../../common"; const ITEM_HEIGHT = 60; const ITEM_WIDTH = 100; const WHEEL_HEIGHT = ITEM_HEIGHT * 5; const WheelModal = () => { const { bottom } = useSafeAreaInsets(); const eventY = useSharedValue(0); const scrollHandler = useAnimatedScrollHandler((event) => { eventY.value = event.contentOffset.y; }); // 改为接收index的函数,为每个项生成独立动画样式 const getAnimatedStyle = (index) => useAnimatedStyle(() => { const activeIndex = eventY.value / ITEM_HEIGHT; // 计算当前项与选中项的相对距离 const distance = index - activeIndex; // 根据相对距离插值透明度 const opacity = interpolate( distance, [-2, -1, 0, 1, 2], [0.1, 0.5, 1, 0.5, 0.1], Extrapolation.CLAMP ); return { opacity }; }); const onSelect = () => { const selectedIndex = Math.round(eventY.value / ITEM_HEIGHT); console.log("selected year", yearGenerator()[selectedIndex]); }; return ( <Modal transparent visible={true} animationType="fade"> <View style={styles.titleBox}> <Text weight="bold" size="lg" tx="Choose the year" /> </View> <LinearGradient locations={[0.1, 0.3, 0.7, 0.9, 1]} colors={[ "rgba(255,255,255,.6)", "rgba(255,255,255,.95)", "rgba(255,255,255,1)", "rgba(255,255,255,.1)", "rgba(255,255,255,.2)", ]} style={styles.gradient} > <Animated.FlatList bounces={false} showsVerticalScrollIndicator={false} removeClippedSubviews={true} onScroll={scrollHandler} data={yearGenerator()} decelerationRate="fast" // 改为fast更符合滚轮选择器的手感 scrollEventThrottle={16} snapToInterval={ITEM_HEIGHT} style={styles.flatList} contentContainerStyle={styles.flatListContent} keyExtractor={(_, index) => index.toString()} renderItem={({ item, index }) => { const itemAnimatedStyle = getAnimatedStyle(index); return ( <Animated.View style={[styles.animatedView, itemAnimatedStyle]}> <Text size="lg" weight="medium" text={item.toString()} /> </Animated.View> ); }} /> <View style={styles.borderView} /> </LinearGradient> <Button onPress={onSelect} tx="Select" style={[styles.selectButton, { height: 45 + bottom }]} /> </Modal> ); }; const styles = StyleSheet.create({ gradient: { flex: 1, justifyContent: "center", alignItems: "center", }, titleBox: { position: "absolute", top: DEVICE.height / 2 - WHEEL_HEIGHT / 2 - 49, alignSelf: "center", zIndex: 999, }, flatList: { flexGrow: 0, height: WHEEL_HEIGHT, }, flatListContent: { flexGrow: 1, paddingVertical: ITEM_HEIGHT * 2, }, animatedView: { height: ITEM_HEIGHT, width: ITEM_WIDTH, justifyContent: "center", alignItems: "center", }, borderView: { width: ITEM_WIDTH, height: ITEM_HEIGHT, position: "absolute", borderTopWidth: 1, borderBottomWidth: 1, borderColor: COLORS.primary, }, selectButton: { position: "absolute", bottom: 0, alignSelf: "center", width: "100%", borderRadius: 0, }, }); const yearGenerator = () => { const year = new Date().getFullYear(); const years = []; for (let i = 2015; i <= year; i++) { years.push(i); } return years; }; export default WheelModal;
额外优化点
- 修正了LinearGradient中的颜色值(原代码用了超出RGB范围的256,改为255)
- 把
decelerationRate改为fast,更符合滚轮选择器快速停稳的手感 - 选中逻辑用
Math.round()处理,避免滚动偏移量计算索引时的小数误差
内容的提问来源于stack exchange,提问作者omar abu alhija
相关产品推荐
相关产品推荐

