react-nativereact-native-reanimated

Animated Style Interpolation Not Working in React Native Reanimated


I'm working on a React Native project where I want to create a wheel-like picker with items that fade in and out as they scroll into view. I'm using react-native-reanimated for the animations, but the opacity interpolation doesn't seem to be working as expected.

What I Have Tried: Here is my component code:

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;
  });

  const animatedStyle = useAnimatedStyle(() => {
    const activeNumber = eventY.value / ITEM_HEIGHT;
    const inputRange = [
      (activeNumber - 2) * ITEM_HEIGHT,
      (activeNumber - 1) * ITEM_HEIGHT,
      activeNumber * ITEM_HEIGHT,
      (activeNumber + 1) * ITEM_HEIGHT,
      (activeNumber + 2) * ITEM_HEIGHT,
    ];

    const opacity = interpolate(
      eventY.value,
      inputRange,
      [0.1, 0.1, 1, 0.1, 0.1],
      Extrapolation.CLAMP
    );

    return {
      opacity,
    };
  });

  const onSelect = () => {
    console.log("selected year", yearGenerator()[eventY.value / ITEM_HEIGHT]);
  };

  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(256,256,256,.6)",
          "rgba(256,256,256,.95)",
          "rgba(256,256,256,1)",
          "rgba(256,256,256,.1)",
          "rgba(256,256,256,.2)",
        ]}
        style={styles.gradient}
      >
        <Animated.FlatList
          bounces={false}
          showsVerticalScrollIndicator={false}
          removeClippedSubviews={true}
          onScroll={scrollHandler}
          data={yearGenerator()}
          decelerationRate="normal"
          scrollEventThrottle={16}
          snapToInterval={ITEM_HEIGHT}
          style={styles.flatList}
          contentContainerStyle={styles.flatListContent}
          keyExtractor={(_, index) => index.toString()}
          renderItem={({ item }) => (
            <Animated.View style={[styles.animatedView, animatedStyle]}>
              <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;


What I Expected: I expect the opacity of the items to decrease as they move away from the center item, creating a fading effect.

What Actually Happens: The opacity animation does not work, and all items have the same opacity regardless of their position in the list.

Additional Information: React Native Version: 0.74.3 React Native Reanimated Version: 3.10.1

Screenshots: my result

Question: What am I doing wrong in my animation code? How can I get the opacity interpolation to work correctly so that items fade in and out as they scroll into view?

Any help or suggestions would be greatly appreciated!


Solution

  • well, I found the solution

    I just needed to separate the renderItem component to separate component and use the index instead of activeNumber

    That's it

    Wheel Component :

    import { StyleSheet, View, Modal } from "react-native";
    import React from "react";
    import { LinearGradient } from "expo-linear-gradient";
    import Text, { TextProps } from "../../atoms/Text";
    import Animated, {
      useAnimatedScrollHandler,
      useSharedValue,
    } from "react-native-reanimated";
    import Button from "../../atoms/Button";
    import { useSafeAreaInsets } from "react-native-safe-area-context";
    import { COLORS, DEVICE } from "../../../common";
    import RenderItem from "./RenderItem";
    
    type PropsType = {
      data: { lable: string; value: string }[];
      onSelect: (value: PropsType["data"][0]) => void;
      isVisible: boolean;
      title: TextProps["tx"];
    };
    const SHOW_ITEMS = 5;
    const ITEM_HEIGHT = 60;
    const WHEEL_HEIGHT = ITEM_HEIGHT * SHOW_ITEMS;
    const ITEM_WIDTH = 100;
    
    const WheelModal = ({ onSelect, data, isVisible, title }: PropsType) => {
      const { bottom } = useSafeAreaInsets();
      const eventY = useSharedValue(0);
    
      const scrollHandler = useAnimatedScrollHandler((event) => {
        eventY.value = event.contentOffset.y;
      });
    
      const _onSelect = () => {
        const SelectedItem = data[eventY.value / ITEM_HEIGHT];
        onSelect(SelectedItem);
      };
    
      return (
        <Modal transparent visible={isVisible} animationType="fade">
          <View style={styles.titleBox}>
            <Text weight="bold" size="lg" tx={title} />
          </View>
          <LinearGradient
            locations={[0.1, 0.3, 0.7, 0.9, 1]}
            colors={[
              "rgba(256,256,256,.6)",
              "rgba(256,256,256,.95)",
              "rgba(256,256,256,1)",
              "rgba(256,256,256,.7)",
              "rgba(256,256,256,.2)",
            ]}
            style={styles.gradient}
          >
            <Animated.FlatList
              bounces={false}
              showsVerticalScrollIndicator={false}
              removeClippedSubviews={true}
              onScroll={scrollHandler}
              data={data ?? []}
              decelerationRate="normal"
              scrollEventThrottle={16}
              snapToInterval={ITEM_HEIGHT}
              style={styles.flatList}
              contentContainerStyle={styles.flatListContent}
              keyExtractor={(_, index) => index.toString()}
              renderItem={(props) => <RenderItem eventY={eventY} {...props} />}
            />
            <View style={styles.borderView} />
          </LinearGradient>
          <Button
            onPress={_onSelect}
            tx="Select"
            style={[styles.selectButton, { height: 50 + 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 * Math.floor(SHOW_ITEMS / 2),
      },
    
      borderView: {
        width: ITEM_WIDTH,
        height: ITEM_HEIGHT,
        position: "absolute",
        borderTopWidth: 1,
        borderBottomWidth: 1,
        borderColor: COLORS.primary,
        zIndex: -1,
        alignSelf: "center",
        top: DEVICE.height / 2 - ITEM_HEIGHT / 2,
      },
      selectButton: {
        position: "absolute",
        bottom: 0,
        alignSelf: "center",
        width: "100%",
        borderRadius: 0,
      },
    });
    
    export default WheelModal;
    

    RenderItem Component

      import { StyleSheet, View } from "react-native";
    import React from "react";
    import Animated, {
      Extrapolation,
      interpolate,
      useAnimatedStyle,
    } from "react-native-reanimated";
    import Text from "../../atoms/Text";
    
    const ITEM_HEIGHT = 60;
    const ITEM_WIDTH = 100;
    const RenderItem = ({ item, index, eventY }) => {
      const inputRange = [
        (index - 2) * ITEM_HEIGHT,
        (index - 1) * ITEM_HEIGHT,
        index * ITEM_HEIGHT,
        (index + 1) * ITEM_HEIGHT,
        (index + 2) * ITEM_HEIGHT,
      ];
    
      const animatedStyle = useAnimatedStyle(() => {
        const opacity = interpolate(
          eventY.value,
          inputRange,
          [0.1, 0.3, 1, 0.3, 0.1],
          Extrapolation.CLAMP
        );
        return {
          opacity,
        };
      });
    
      return (
        <Animated.View style={[styles.animatedView, animatedStyle]}>
          <Text size="lg" weight="medium" text={item.toString()} />
        </Animated.View>
      );
    };
    
    export default RenderItem;
    
    const styles = StyleSheet.create({
      animatedView: {
        height: ITEM_HEIGHT,
        width: ITEM_WIDTH,
        justifyContent: "center",
        alignItems: "center",
      },
    });