You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Native Reanimated动画样式插值失效问题排查

React Native滚轮选择器透明度动画修复方案

问题根源

  1. 全局动画样式复用:当前的animatedStyle是组件级全局样式,所有列表项共用同一个透明度值,导致所有项透明度完全一致,无法呈现差异效果。
  2. 插值逻辑错误:直接用滚动偏移量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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 06:43:11