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

React Native实现类似Flutter的商品加购飞行动画求助

React Native 实现商品添加购物车的飞行动画方案

核心思路

要实现类似的「商品图片旋转飞入购物车」效果,核心是创建一个独立的动画层:点击添加按钮时,克隆当前商品的图片到动画层,通过React Native的Animated API组合平移、旋转、透明度动画,让图片从商品位置平滑过渡到购物车图标位置,动画结束后清理动画层并更新购物车状态。

分步实现代码示例

以下是完整的可运行示例代码,包含商品列表、底部TabBar和动画逻辑:

import React, { useRef, useState } from 'react';
import { View, FlatList, Image, Text, TouchableOpacity, Animated, UIManager, findNodeHandle, StatusBar, Platform } from 'react-native';

const ProductCartAnimation = () => {
  // 模拟商品数据
  const productList = useRef([
    { id: '1', name: '复古帆布包', image: 'https://via.placeholder.com/80' },
    { id: '2', name: '简约马克杯', image: 'https://via.placeholder.com/80' },
    { id: '3', name: '纯棉T恤', image: 'https://via.placeholder.com/80' },
  ]).current;

  const [cartItemCount, setCartItemCount] = useState(0);
  // 动画相关变量
  const animationProgress = useRef(new Animated.Value(0)).current;
  const isAnimationRunning = useRef(false);
  // 存储元素引用:商品图片、购物车图标、动画层图片
  const productImageRefs = useRef({});
  const cartIconRef = useRef(null);
  const animatedCloneImage = useRef(null);

  // 获取元素在屏幕上的坐标(含状态栏适配)
  const getElementScreenPosition = async (elementRef) => {
    return new Promise((resolve) => {
      UIManager.measureInWindow(findNodeHandle(elementRef), (x, y, width, height) => {
        // 安卓需要加上状态栏高度修正坐标
        const adjustedY = Platform.OS === 'android' ? y + StatusBar.currentHeight : y;
        resolve({ x, y: adjustedY, width, height });
      });
    });
  };

  // 处理添加购物车点击事件
  const handleAddToCart = async (productId) => {
    if (isAnimationRunning.current) return; // 防止重复触发动画
    isAnimationRunning.current = true;

    // 获取商品图片和购物车图标的屏幕位置
    const productPos = await getElementScreenPosition(productImageRefs.current[productId]);
    const cartPos = await getElementScreenPosition(cartIconRef.current);

    // 计算动画起始/结束位置(让图片中心对齐购物车中心)
    const startCenterX = productPos.x + productPos.width / 2;
    const startCenterY = productPos.y + productPos.height / 2;
    const endCenterX = cartPos.x + cartPos.width / 2;
    const endCenterY = cartPos.y + cartPos.height / 2;

    // 重置动画进度
    animationProgress.setValue(0);

    // 定义动画插值样式
    const animatedStyle = {
      transform: animationProgress.interpolate({
        inputRange: [0, 1],
        outputRange: [
          [
            { translateX: startCenterX - productPos.width / 2 },
            { translateY: startCenterY - productPos.height / 2 },
            { rotate: '0deg' }
          ],
          [
            { translateX: endCenterX - productPos.width / 2 },
            { translateY: endCenterY - productPos.height / 2 },
            { rotate: '360deg' }
          ]
        ]
      }),
      opacity: animationProgress.interpolate({
        inputRange: [0, 0.9, 1],
        outputRange: [1, 1, 0]
      }),
      width: productPos.width,
      height: productPos.height,
      position: 'absolute',
      zIndex: 9999,
      borderRadius: 8
    };

    // 克隆当前商品图片到动画层
    const targetProduct = productList.find(item => item.id === productId);
    animatedCloneImage.current = (
      <Animated.Image
        source={{ uri: targetProduct.image }}
        style={animatedStyle}
      />
    );
    // 强制刷新渲染动画层
    setCartItemCount(prev => prev);

    // 执行动画
    Animated.timing(animationProgress, {
      toValue: 1,
      duration: 800,
      useNativeDriver: true,
      easing: Animated.easeOut
    }).start(() => {
      // 动画结束后清理
      animatedCloneImage.current = null;
      isAnimationRunning.current = false;
      // 更新购物车数量
      setCartItemCount(prev => prev + 1);
    });
  };

  return (
    <View style={{ flex: 1, backgroundColor: '#f5f5f5' }}>
      {/* 商品列表 */}
      <FlatList
        data={productList}
        keyExtractor={item => item.id}
        contentContainerStyle={{ paddingTop: StatusBar.currentHeight }}
        renderItem={({ item }) => (
          <View style={{
            flexDirection: 'row',
            padding: 16,
            backgroundColor: '#fff',
            marginBottom: 8,
            alignItems: 'center'
          }}>
            <Image
              ref={ref => productImageRefs.current[item.id] = ref}
              source={{ uri: item.image }}
              style={{ width: 80, height: 80, borderRadius: 8 }}
            />
            <View style={{ flex: 1, marginLeft: 12 }}>
              <Text style={{ fontSize: 16, fontWeight: '500', color: '#333' }}>{item.name}</Text>
              <TouchableOpacity
                style={{
                  marginTop: 10,
                  backgroundColor: '#0066cc',
                  paddingVertical: 6,
                  paddingHorizontal: 12,
                  borderRadius: 4,
                  alignSelf: 'flex-start'
                }}
                onPress={() => handleAddToCart(item.id)}
              >
                <Text style={{ color: '#fff', fontSize: 14 }}>加入购物车</Text>
              </TouchableOpacity>
            </View>
          </View>
        )}
      />

      {/* 动画层:用于展示飞入购物车的商品图片 */}
      {animatedCloneImage.current}

      {/* 底部TabBar */}
      <View style={{
        flexDirection: 'row',
        height: 60,
        backgroundColor: '#fff',
        borderTopWidth: 1,
        borderTopColor: '#eee',
        alignItems: 'center',
        justifyContent: 'space-around'
      }}>
        <TouchableOpacity style={{ alignItems: 'center' }}>
          <Text style={{ fontSize: 22 }}>🏠</Text>
          <Text style={{ fontSize: 12, marginTop: 2 }}>首页</Text>
        </TouchableOpacity>
        <TouchableOpacity ref={cartIconRef} style={{ alignItems: 'center' }}>
          <View style={{ position: 'relative' }}>
            <Text style={{ fontSize: 22 }}>🛒</Text>
            {cartItemCount > 0 && (
              <View style={{
                position: 'absolute',
                top: -6,
                right: -6,
                backgroundColor: '#ff3b30',
                borderRadius: 10,
                width: 20,
                height: 20,
                alignItems: 'center',
                justifyContent: 'center'
              }}>
                <Text style={{ color: '#fff', fontSize: 12, fontWeight: 'bold' }}>{cartItemCount}</Text>
              </View>
            )}
          </View>
          <Text style={{ fontSize: 12, marginTop: 2 }}>购物车</Text>
        </TouchableOpacity>
        <TouchableOpacity style={{ alignItems: 'center' }}>
          <Text style={{ fontSize: 22 }}>👤</Text>
          <Text style={{ fontSize: 12, marginTop: 2 }}>我的</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
};

export default ProductCartAnimation;

关键细节与优化建议

  • 防重复点击:通过isAnimationRunning标记动画状态,避免短时间内多次点击触发多个动画。
  • 坐标适配:安卓设备需要加上状态栏高度修正Y轴坐标,确保动画路径准确。
  • 性能优化:开启useNativeDriver: true让动画在原生线程执行,避免JS线程阻塞导致卡顿。
  • 动画曲线:使用Animated.easeOut让动画结束阶段减速,更符合自然的物理效果。
  • 样式细节:动画层图片设置与商品图片一致的圆角,保持视觉统一;动画结束时通过透明度渐变隐藏图片,避免突兀消失。

内容的提问来源于stack exchange,提问作者Gokul Raj

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 08:56:06