You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

React Native Reanimated实现的Tinder式卡片Swiper:已滑出卡片卸载前闪烁问题求助

React Native Reanimated实现的Tinder式卡片Swiper:已滑出卡片卸载前闪烁问题求助

问题根源分析

这个闪烁的核心问题是React状态更新与Reanimated共享值重置的时间差
当卡片滑出动画完成后,你原本在handleSwipe里立刻重置了translateXrotate共享值,但此时React的index状态还没完成更新,旧卡片仍然存在于视图树中——它会瞬间回到初始位置,直到状态更新完成、新卡片替换旧卡片,这就导致了短暂的闪烁。

针对性解决方案

我们需要把共享值的重置时机延后到新卡片渲染完成后,同时调整卡片渲染逻辑,确保旧卡片在被替换前保持在屏幕外。以下是具体修改步骤和完整代码:


修改后的完整代码

import { View, Image, StyleSheet, Dimensions } from 'react-native';
import { useState, useEffect, useMemo, useCallback } from 'react';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, { 
  useAnimatedStyle, 
  useSharedValue, 
  withSpring, 
  runOnJS, 
  interpolate, 
  withTiming, 
  FadeOut 
} from 'react-native-reanimated';

const { width, height } = Dimensions.get('window');

type Props = {
  imageUris: string[],
  onSwipeRight: () => void,
  onSwipeLeft: () => void,
};

export default function ImageSwiper({ imageUris, onSwipeRight, onSwipeLeft }: Props) {
  const [index, setIndex] = useState(0)
  const translateX = useSharedValue(0);
  const rotate = useSharedValue(0);

  // 当索引更新(新卡片渲染完成)后,再重置共享值(仅作用于新的顶部卡片)
  useEffect(() => {
    translateX.value = withTiming(0, { duration: 0 });
    rotate.value = withTiming(0, { duration: 0 });
  }, [index]);

  // 优化卡片列表生成逻辑,保证当前和下一张卡片的稳定性
  const cards = useMemo(() => {
    if (imageUris.length === 0) return [];
    const current = imageUris[index];
    const next = imageUris[(index + 1) % imageUris.length];
    return imageUris.length === 1 ? [current] : [current, next];
  }, [index, imageUris]);

  const handleSwipe = useCallback((direction: 'left' | 'right') => {
    if (direction === 'right') onSwipeRight();
    else onSwipeLeft();
    setIndex((prev) => (prev + 1) % imageUris.length);
    // 移除这里的共享值重置操作
  }, [onSwipeRight, onSwipeLeft, imageUris.length]);

  const pan = Gesture.Pan()
    .onUpdate((e) => {
      translateX.value = e.translationX;
      rotate.value = interpolate(translateX.value, [-width / 2, 0, width / 2], [-15, 0, 15]);
    })
    .onEnd((e) => {
      const isSwipeValid = Math.abs(e.translationX) > 150 || e.velocityX > 800;
      if (isSwipeValid) {
        const isRight = e.translationX > 0;
        // 执行滑出动画,完成后再触发状态更新
        translateX.value = withTiming(isRight ? width : -width, { duration: 300 }, (finished) => {
          if (finished) runOnJS(handleSwipe)(isRight ? 'right' : 'left');
        });
        rotate.value = withTiming(isRight ? 20 : -20, { duration: 300 });
      } else {
        // 未触发有效滑动,重置回初始位置
        translateX.value = withSpring(0);
        rotate.value = withSpring(0);
      }
    });

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [
      { translateX: translateX.value },
      { rotate: `${rotate.value}deg` },
    ],
  }));

  return (
    <View style={styles.container}>
      {cards.map((uri, i) => {
        const isTop = i === 0;
        const zIndex = 1 - i;
        // 用索引+位置+uri作为唯一key,避免重复图片导致的渲染异常
        const cardKey = `${index}-${i}-${uri}`;

        const cardContent = (
          <Animated.View
            key={cardKey}
            style={[styles.card, { zIndex }, isTop ? animatedStyle : {}]}
            // 给滑出的旧卡片添加淡出动画,提升过渡自然度
            exiting={isTop ? FadeOut.duration(200) : undefined}
          >
            <Image
              source={{ uri }}
              style={{ width: '100%', height: '100%', resizeMode: 'cover' }}
            />
          </Animated.View>
        );

        return isTop ? (
          <GestureDetector key={cardKey} gesture={pan}>
            {cardContent}
          </GestureDetector>
        ) : cardContent;
      })}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  card: {
    position: 'absolute',
    width: width * 0.7,
    height: height * 0.4,
    overflow: 'hidden',
    borderRadius: 12,
    shadowColor: "#000",
    shadowOffset: { width: 0, height: 5 },
    shadowOpacity: 0.36,
    shadowRadius: 6.68,
    elevation: 11,
  },
});

关键修改点说明

  1. 共享值重置时机调整
    useEffect监听index变化,当新卡片渲染完成后再重置translateXrotate,确保重置操作只作用于新的顶部卡片,不会影响正在被替换的旧卡片。

  2. 卡片Key优化
    key从单一的uri改为${index}-${i}-${uri},避免因重复图片地址导致的React渲染异常,确保每个卡片实例都有唯一标识。

  3. 滑出动画与状态更新的顺序
    滑出动画完全完成后才触发handleSwipe更新索引,此时旧卡片已经在屏幕外,用户看不到它的状态变化,直到新卡片自然替换它。

  4. 可选的退出动画
    给顶部卡片添加exiting={FadeOut.duration(200)},让旧卡片在被替换时平滑淡出,进一步提升滑动体验的自然度,避免生硬的视图切换。

这样修改后,旧卡片会保持在屏幕外直到被替换,不会再出现闪回初始位置的问题,整个滑动流程会更流畅。

火山引擎 最新活动