当前位置: 首页 > news >正文

【React Native】点赞特效动画组件FlowLikeView

一、组件概述

FlowLikeView 是一个用于实现点赞粒子动画效果的 React Native 组件。核心功能为:用户点击底部触发按钮时,多个点赞图标(支持本地/网络图片)会沿随机贝塞尔曲线向上飘动,伴随透明度渐变消失的动画效果。适用于社交应用、短视频等需要点赞反馈的场景。


二、核心特性

  • 粒子动画:点赞图标沿三阶贝塞尔曲线运动,路径随机生成
  • 参数可配:支持自定义动画时长、图标大小、容器尺寸等
  • 交互反馈:触发按钮带有缩放动画,增强点击感知
  • 性能优化:使用 Animated 原生驱动动画,减少 JS 线程压力
  • 灵活扩展:支持自定义触发按钮样式

三、Props 说明

Prop 名称类型是否必传默认值说明
styleStyleProp<ViewStyle>{}容器整体样式
likeAssetsArray<Source | ImageRequireSource> | undefined-点赞图标资源数组(支持本地图片资源或网络图片 URI)
animContainerBottomnumber0动画容器底部边距(用于调整与页面底部的距离)
animContainerWidthnumber-动画容器宽度(需与实际布局宽度一致,用于计算贝塞尔曲线控制点)
animContainerHeightnumber-动画容器高度(需与实际布局高度一致,用于计算贝塞尔曲线控制点)
durationnumber1600单次动画时长(单位:ms,控制图标从起点到终点的总时间)
picSizenumber30点赞图标的尺寸(宽高均为该值,单位:dp)
onLikeTrigger() => void-触发点赞的回调函数(图标开始动画时调用)
renderLikeButton() => React.ReactNode-自定义触发按钮的渲染函数(若不传则使用默认的 “+” 按钮)

四、核心方法(内部使用)

组件内部通过 useCallback 封装了以下核心方法,用于控制动画流程:

1. startAnim

  • 功能:启动点赞动画(生成新图标并开始运动)
  • 触发条件:调用 triggerLike 时自动触发
  • 实现细节
    • 使用 Animated.timing 创建线性动画,通过 Animated.loop 实现循环计数
    • 监听动画值变化,动态更新所有图标的 progress(进度)、opacity(透明度)和位置(通过贝塞尔曲线计算)

2. stopAnim

  • 功能:停止当前所有动画
  • 触发条件:当没有活跃的点赞图标(likes 为空)时自动调用
  • 实现细节:移除动画监听、停止动画并重置状态

3. triggerLike

  • 功能:触发点赞操作(外部调用的核心入口)
  • 触发条件:用户点击底部触发按钮时调用
  • 实现细节
    • 校验 likeAssets 非空后调用 startAnim
    • 生成随机贝塞尔曲线控制点(p1p2)和起止位置(startPointendPoint
    • 创建新的 LikeItem 对象并添加到 likes 状态中

五、使用示例

import React from 'react';
import { View, StyleSheet } from 'react-native';
import FlowLikeView from './FlowLikeView';const App = () => {// 点赞图标资源(本地或网络)const likeAssets = [require('./assets/like1.png'),require('./assets/like2.png'),{ uri: 'https://example.com/like3.png' },];return (<View style={styles.container}>{/* 其他页面内容 */}{/* 点赞动画组件 */}<FlowLikeViewanimContainerWidth={375} // 假设页面宽度为 375dpanimContainerHeight={600} // 动画区域高度(根据实际布局调整)likeAssets={likeAssets}picSize={40} // 图标尺寸 40dpanimContainerBottom={50} // 底部边距 50dp(避免被底部导航遮挡)duration={2000} // 动画时长 2000msonLikeTrigger={() => {console.log('用户触发了点赞!');}}// 自定义触发按钮(可选)renderLikeButton={() => (<View style={styles.customButton}><Text style={styles.customButtonText}>点赞</Text></View>)}/></View>);
};const styles = StyleSheet.create({container: {flex: 1,backgroundColor: '#f0f0f0',},customButton: {width: 60,height: 60,borderRadius: 30,backgroundColor: '#ff4757',alignItems: 'center',justifyContent: 'center',},customButtonText: {color: 'white',fontSize: 18,fontWeight: 'bold',},
});export default App;

六、注意事项

  1. 必传参数likeAssetsanimContainerWidthanimContainerHeight 必须传入有效值,否则无法正常显示动画。
  2. 布局测量:组件通过 measureInWindow 测量容器实际尺寸,需确保父容器布局稳定(避免动态变化导致尺寸测量失败)。
  3. 性能优化
    • 若需要大量图标同时动画(如 10 个以上),建议降低 picSize 或减少 likeAssets 数量,避免内存溢出。
    • 动画使用 useNativeDriver: true,但部分样式(如 topleft)依赖 JS 计算,复杂场景下可考虑改用 Animated.ValueXY 优化。
  4. 自定义按钮:通过 renderLikeButton 自定义按钮时,需自行处理点击事件(无需额外绑定 onPress,组件已透传)。
  5. 动画中断:若需要在页面切换时停止动画,可调用组件实例的 stopAnim 方法(需通过 ref 获取)。

七、源码

import React, {useCallback, useEffect, useRef, useState} from 'react';
import {Animated,Easing,ImageRequireSource,StyleProp,StyleSheet,Text,TouchableOpacity,View,ViewStyle,
} from 'react-native';
import NormalImage from '@components/NormalImage';
import {Source} from '@react-native-oh-tpl/react-native-fast-image/src';type LikeItem = {id: number;image: Source | ImageRequireSource | string;left: number;top: number;progress: number;opacity: number;isFinish: boolean;startPoint: {x: number; y: number};endPoint: {x: number; y: number};p1: {x: number; y: number};p2: {x: number; y: number};
};interface FlowLikeViewProps {style?: StyleProp<ViewStyle>;/** 点赞图标资源数组(本地或网络) */likeAssets: Array<Source | ImageRequireSource> | undefined;/** 底部边距(默认0) */animContainerBottom?: number;/** 动画容器宽度 */animContainerWidth: number;/** 动画容器高度 */animContainerHeight: number;/** 动画时长(默认1600ms) */duration?: number;/** 触发点赞的回调(可选) */onLikeTrigger?: () => void;/** 点赞图标大小(30) */picSize?: number;/** 自定义点赞按钮  */renderLikeButton?: () => React.ReactNode;
}const FlowLikeView: React.FC<FlowLikeViewProps> = ({style,likeAssets,animContainerBottom = 0,animContainerWidth,animContainerHeight,duration = 1600,picSize = 30,onLikeTrigger,renderLikeButton,
}) => {const [likes, setLikes] = useState<LikeItem[]>([]);const containerRef = useRef<View>(null);const isAnimating = useRef(false);const [rootHeight, setRootHeight] = useState(0);const [rootWidth, setRootWidth] = useState(0);const animatedValue = useRef<Animated.Value>(new Animated.Value(0));const lastValue = useRef(0);const [btnScale, setBtnScale] = useState(1);// 实时更新容器高度const handleContainerLayout = useCallback(() => {if (containerRef.current) {containerRef.current.measureInWindow((x, y, width, height) => {setRootHeight(height);setRootWidth(width);});}}, []);// 计算贝塞尔曲线控制点const generateControlPoints = useCallback((value: number) => {return {x: rootWidth / 2 - Math.random() * 100,y: (Math.random() * rootHeight) / value,};},[rootWidth, rootHeight],);// 贝塞尔曲线坐标计算(三阶)const calculateBezierPoint = useCallback((t: number,start: {x: number; y: number},end: {x: number; y: number},p1: {x: number; y: number},p2: {x: number; y: number},): {x: number; y: number} => {const u = 1 - t;const tt = t * t;const uu = u * u;const uuu = uu * u;const ttt = tt * t;return {x: uuu * start.x + 3 * uu * t * p1.x + 3 * u * tt * p2.x + ttt * end.x,y: uuu * start.y + 3 * uu * t * p1.y + 3 * u * tt * p2.y + ttt * end.y,};},[],);const startAnim = useCallback(() => {if (isAnimating.current) return;isAnimating.current = true;animatedValue.current.removeAllListeners();animatedValue.current.addListener(value => {const last = lastValue.current;const current = value.value;const interval = current >= last ? current - last : current + 1 - last;lastValue.current = value.value;setLikes(prev => {const temp: LikeItem[] = [];prev.forEach((item, index) => {item.progress += interval;if (index === prev.length - 1) {const sScale = 0.3;const sTime = 0.1;if (item.progress <= sTime) {setBtnScale(1 - item.progress * (sScale / sTime));} else if (item.progress <= sTime * 2) {setBtnScale(1 - sScale + (item.progress - sTime) * (sScale / sTime),);}}if (item.progress >= 1) {item.progress = 1;item.isFinish = true;}item.opacity = 1 - item.progress;const currentPos = calculateBezierPoint(item.progress,item.startPoint,item.endPoint,item.p1,item.p2,);item.top = currentPos.y;item.left = currentPos.x;if (!item.isFinish) {temp.push(item);}});return [...temp];});});const anim = Animated.timing(animatedValue.current, {toValue: 1,duration: duration,easing: Easing.linear,useNativeDriver: true, // 尽可能使用原生驱动});Animated.loop(Animated.sequence([anim])).start();}, [isAnimating, calculateBezierPoint, duration]);const stopAnim = useCallback(() => {lastValue.current = 0;animatedValue.current.removeAllListeners();animatedValue.current.stopAnimation();isAnimating.current = false;}, []);useEffect(() => {if (likes.length === 0) {stopAnim();animatedValue.current.removeAllListeners();}}, [likes, stopAnim]);// 触发点赞const triggerLike = useCallback(() => {if (!likeAssets || likeAssets.length === 0) return;startAnim();// 控制点const p1 = generateControlPoints(1);const p2 = generateControlPoints(2);// 定义精确的起始和结束位置const startPoint = {x: rootWidth / 2 - picSize / 2,y: rootHeight - animContainerBottom - picSize, // 从底部下方开始};const endPoint = {x: rootWidth / 2 + (Math.random() > 0.5 ? 1 : -1) * 100,y: 0, // 到顶部外消失};const newLike: LikeItem = {id: Date.now(),image: likeAssets[Math.floor(Math.random() * likeAssets.length)],left: 0,top: 0,progress: 0,opacity: 0,startPoint: startPoint,endPoint: endPoint,p1: p1,p2: p2,isFinish: false,};setLikes(prev => {const l = prev;if (prev.findIndex(item => item.id === newLike.id) < 0) {l.push(newLike);}return l;});onLikeTrigger?.();}, [likeAssets,onLikeTrigger,startAnim,animContainerBottom,picSize,rootWidth,rootHeight,generateControlPoints,]);return (<Viewstyle={[styles.fullScreenContainer,{width: animContainerWidth || 'auto'},style,]}><Viewref={containerRef}style={[styles.animationLayer,{height: animContainerHeight || 'auto',width: animContainerWidth || 'auto',},]}onLayout={handleContainerLayout}>{likes.map((like, index) => (<Animated.Viewkey={like.id}style={[styles.likeItem,{width: picSize,height: picSize,top: like.top,left: like.left,opacity: like.opacity,},]}>{typeof like.image === 'string' ? (<NormalImagesource={{uri: like.image}}style={styles.likeIcon}resizeMode="contain"/>) : (<NormalImagesource={like.image}style={styles.likeIcon}resizeMode="contain"/>)}</Animated.View>))}</View>{/* 底部触发按钮 */}<TouchableOpacitystyle={[styles.triggerButton, {transform: [{scale: btnScale}]}]}onPress={triggerLike}activeOpacity={1}>{renderLikeButton ? (renderLikeButton()) : (<View style={styles.triggerIcon}><Text style={styles.triggerIconText}>+</Text></View>)}</TouchableOpacity></View>);
};const styles = StyleSheet.create({fullScreenContainer: {display: 'flex',flexDirection: 'column',width: 'auto',},animationLayer: {},likeItem: {position: 'absolute',zIndex: 1000,},likeIcon: {width: '100%',height: '100%',},triggerButton: {alignItems: 'center',},triggerIcon: {width: 50,height: 50,alignItems: 'center',justifyContent: 'center',},triggerIconText: {color: 'white',},
});export default FlowLikeView;

文章转载自:

http://exNIfYYE.nzfjm.cn
http://giC7qK4r.nzfjm.cn
http://9tdWHusr.nzfjm.cn
http://9ICtxARi.nzfjm.cn
http://c4itMexV.nzfjm.cn
http://hHgMLQTi.nzfjm.cn
http://qVQYf4bz.nzfjm.cn
http://XjmFlWVl.nzfjm.cn
http://ks9ln4hN.nzfjm.cn
http://buhEXmdj.nzfjm.cn
http://Y6SseXVd.nzfjm.cn
http://31s33Wu8.nzfjm.cn
http://85oCIZYE.nzfjm.cn
http://WVK1hraC.nzfjm.cn
http://G9I44Sod.nzfjm.cn
http://q7jZSoQQ.nzfjm.cn
http://VZozPzID.nzfjm.cn
http://KmXa0Sxc.nzfjm.cn
http://7ffbVTm1.nzfjm.cn
http://bpoS5PIC.nzfjm.cn
http://JP5rEt00.nzfjm.cn
http://yczwLONs.nzfjm.cn
http://4zbBaV1s.nzfjm.cn
http://9ui8Bzp4.nzfjm.cn
http://GnavuK5d.nzfjm.cn
http://BZJcqc6E.nzfjm.cn
http://KgS9Wqqz.nzfjm.cn
http://HtleiKFo.nzfjm.cn
http://S1NZfFOU.nzfjm.cn
http://ivGRnUiA.nzfjm.cn
http://www.dtcms.com/a/374745.html

相关文章:

  • android studio gradle 访问不了
  • 【C++】C++11 篇二
  • Kubernetes 配置检查与发布安全清单
  • Perforce Klocwork 2025.2版本更新:默认启用现代分析引擎、支持 MISRA C:2025 新规、CI构建性能提升等
  • 工业总线协议转换核心:SG-DP_MOD-110 Profibus-DP 转 Modbus-RTU 网关,打通异构设备数据链路
  • Win系统下配置PCL库第三步之链接库的路径(超详细)
  • 【远程运维】Linux 远程连接 Windows 好用的软件:MobaXterm 实战指南
  • Java入门级教程13-多线程同步安全机制synchronized(内置锁)、JavaMail发送电子邮箱、爬取CSDN到邮箱、备份数据库
  • 玩转Docker | 使用Docker部署KissLists任务管理工具
  • STL库——map/set(类函数学习)
  • STM32 串口接收数据包(自定义帧头帧尾)
  • 正向代理,反向代理,负载均衡还有nginx
  • 用户态与内核态的深度解析:安全、效率与优化之道
  • 搭建本地gitea服务器
  • ArcGIS JSAPI 高级教程 - 倾斜摄影数据开启透明(修改源码)
  • 输电线路分布式故障监测装置技术解析
  • 概率论第四讲—随机变量的数字特征
  • 学习stm32 蓝牙
  • 数据库学习MySQL系列2、Windows11系统安装MySQL方法一.msi安装详细教程
  • STM32物联网项目---ESP8266微信小程序结合OneNET平台MQTT实现STM32单片机远程智能控制---代码篇(四)
  • 北京鲁成伟业 | 三屏加固笔记本电脑C156F3
  • 从0~1搭建技术团队的思路
  • 如何在 Unity3D 中实现圆角效果?
  • LeetCode 面试经典 150 题:多数元素(摩尔投票法详解 + 多解法对比)
  • CStringArray 和 CStringList
  • 银行业安全用电系统建设与智能化管理探析
  • 20250909_排查10.1.1.190档案库房综合管理系统20250908备份缺失问题+优化scp脚本(把失败原因记录进日志)并测试脚本执行情况
  • 硬件开发_基于STM32单片机的海鲜冷藏车检测系统
  • AI一周事件(2025年9月3日-9月8日)
  • Unity3D发布的文件打包成Windows安装程序