鸿蒙特效教程 04- 直播点赞动画效果实现教程
- 2025-03-26 北京
本文字数:8226 字
阅读完需:约 27 分钟
鸿蒙特效教程 04-直播点赞动画效果实现教程
在时下流行的直播、短视频等应用中,点赞动画是提升用户体验的重要元素。当用户点击屏幕时,屏幕上会出现飘动的点赞图标。
本教程适合 HarmonyOS 初学者,通过简单到复杂的步骤,通过 HarmonyOS 的 Canvas 组件,一步步实现时下流行的点赞动画效果。
效果预览
我们将实现的效果是:用户点击屏幕时,在点击位置生成一个 emoji 表情图标,逐步添加了以下动画效果:
向上移动:让图标从点击位置向上飘移
非线性运动:使用幂函数让移动更加自然
渐隐效果:让图标在上升过程中逐渐消失
放大效果:让图标从小变大
左右摆动:增加水平方向的微妙摆动
1. 基础结构搭建
首先,我们创建一个基本的页面结构和数据模型,用于管理点赞图标和动画。
定义图标数据结构
// 定义点赞图标数据结构interface LikeIcon { x: number // X坐标 y: number // Y坐标 initialX: number // 初始X坐标 initialY: number // 初始Y坐标 radius: number // 半径 emoji: string // emoji表情 fontSize: number // 字体大小 opacity: number // 透明度 createTime: number // 创建时间 lifespan: number // 生命周期(毫秒) scale: number // 当前缩放比例 initialScale: number // 初始缩放比例 maxScale: number // 最大缩放比例 maxOffset: number // 最大摆动幅度 direction: number // 摆动方向 (+1或-1)}
这个接口定义了每个点赞图标所需的所有属性,从位置到动画参数,为后续的动画实现提供了数据基础。
组件基本结构
@Entry@Componentstruct CanvasLike { // 用来配置CanvasRenderingContext2D对象的参数,开启抗锯齿 private settings: RenderingContextSettings = new RenderingContextSettings(true) // 创建CanvasRenderingContext2D对象 private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings) @State likeIcons: LikeIcon[] = [] // 存储所有点赞图标 private animationId: number = 0 // 动画ID // emoji表情数组 private readonly emojis: string[] = [ '❤️', '🧡', '💛', '💚', '💙', '💜', '🐻', '🐼', '🐨', '🦁', '🐯', '🦊', '🎁', '🎀', '🎉', '🎊', '✨', '⭐' ]
// 生命周期方法和核心功能将在后续步骤中添加 build() { Column() { Stack() { Text('直播点赞效果')
Canvas(this.context) .width('100%') .height('100%') .onClick((event: ClickEvent) => { // 点击处理逻辑将在后续步骤中添加 }) } } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) .backgroundColor(Color.White) }}
这里我们创建了基本的页面结构,包含一个标题和一个全屏的 Canvas 组件,用于绘制和响应点击事件。
2. 实现静态图标绘制
首先,我们实现最基础的功能:在 Canvas 上绘制一个静态的 emoji 图标。
创建图标生成函数
// 创建一个图标对象createIcon(x: number, y: number, radius: number, emoji: string): LikeIcon { return { x: x, y: y, initialX: x, initialY: y, radius: radius, emoji: emoji, fontSize: Math.floor(radius * 1.2), opacity: 1.0, createTime: Date.now(), lifespan: 1000, // 1秒钟生命周期 scale: 1.0, // 暂时不缩放 initialScale: 1.0, maxScale: 1.0, maxOffset: 0, // 暂时不偏移 direction: 1 }}
// 获取随机emojigetRandomEmoji(): string { return this.emojis[Math.floor(Math.random() * this.emojis.length)]}
// 添加新的点赞图标addLikeIcon(x: number, y: number) { const radius = 80 // 固定大小 const emoji = this.getRandomEmoji() this.likeIcons.push(this.createIcon(x, y, radius, emoji)) this.drawAllIcons() // 重新绘制所有图标}
实现基本绘制函数
// 绘制所有图标drawAllIcons() { // 清除画布 this.context.clearRect(0, 0, this.context.width, this.context.height) // 绘制所有图标 for (let icon of this.likeIcons) { // 绘制emoji this.context.font = `${icon.fontSize}px` this.context.textAlign = 'center' this.context.textBaseline = 'middle' this.context.fillText(icon.emoji, icon.x, icon.y) }}
绑定点击事件
.onClick((event: ClickEvent) => { console.info(`Clicked at: ${event.x}, ${event.y}`) this.addLikeIcon(event.x, event.y)})
此时,每次点击 Canvas,就会在点击位置绘制一个随机的 emoji 图标。但这些图标是静态的,不会移动或消失。
3. 添加动画循环系统
为了实现动画效果,我们需要一个动画循环系统,定期更新图标状态并重新绘制。
aboutToAppear() { // 启动动画循环 this.startAnimation()}
aboutToDisappear() { // 清除动画循环 clearInterval(this.animationId)}
// 开始动画循环startAnimation() { this.animationId = setInterval(() => { this.updateIcons() this.drawAllIcons() }, 16) // 约60fps的刷新率}
// 更新所有图标状态updateIcons() { const currentTime = Date.now() const newIcons: LikeIcon[] = []
for (let icon of this.likeIcons) { // 计算图标已存在的时间 const existTime = currentTime - icon.createTime if (existTime < icon.lifespan) { // 保留未完成生命周期的图标 newIcons.push(icon) } } // 更新图标数组 this.likeIcons = newIcons}
现在,我们有了一个基本的动画系统,但图标仍然是静态的。接下来,我们将逐步添加各种动画效果。
4. 实现向上移动效果
让我们首先让图标动起来,实现一个简单的向上移动效果。
// 更新所有图标状态updateIcons() { const currentTime = Date.now() const newIcons: LikeIcon[] = []
for (let icon of this.likeIcons) { // 计算图标已存在的时间 const existTime = currentTime - icon.createTime if (existTime < icon.lifespan) { // 计算存在时间比例 const progress = existTime / icon.lifespan // 更新Y坐标 - 向上移动 icon.y = icon.initialY - 120 * progress // 保留未完成生命周期的图标 newIcons.push(icon) } } // 更新图标数组 this.likeIcons = newIcons}
现在,图标会在 1 秒内向上移动 120 像素,然后消失。这是一个简单的线性移动,看起来有些机械。
5. 添加非线性运动效果
为了让动画更加自然,我们可以使用幂函数来模拟非线性运动,使图标开始时移动较慢,然后加速。
// 更新Y坐标 - 向上移动,速度变化更明显const verticalDistance = 120 * Math.pow(progress, 0.7) // 使用幂函数让上升更快icon.y = icon.initialY - verticalDistance
幂指数 0.7 使得图标的上升速度随时间增加,创造出更加自然的加速效果。
6. 添加渐隐效果
接下来,让图标在上升过程中逐渐消失,增加视觉上的层次感。
// 更新透明度 - 前60%保持不变,后40%逐渐消失if (progress > 0.6) { // 在最后40%的生命周期内改变透明度,使消失更快 icon.opacity = 1.0 - ((progress - 0.6) / 0.4)} else { icon.opacity = 1.0}
// 在绘制时应用透明度this.context.globalAlpha = icon.opacity
这样,图标在生命周期的前 60%保持完全不透明,后 40%时间内逐渐变透明直到完全消失。这种设计让用户有足够的时间看清图标,然后它才开始消失。
7. 实现放大效果
现在,让我们添加图标从小变大的动画效果,这会让整个动画更加生动。
// 创建图标时设置初始和最大缩放比例createIcon(x: number, y: number, radius: number, emoji: string): LikeIcon { // 为图标生成随机属性 const initialScale = 0.4 + Math.random() * 0.2 // 初始缩放比例0.4-0.6 const maxScale = 1.0 + Math.random() * 0.3 // 最大缩放比例1.0-1.3 // ... 其他属性设置 ... return { // ... 其他属性 ... scale: initialScale, // 当前缩放比例 initialScale: initialScale, // 初始缩放比例 maxScale: maxScale, // 最大缩放比例 // ... 其他属性 ... }}
// 在updateIcons中更新缩放比例// 更新缩放比例 - 快速放大// 在生命周期的前20%阶段(0.2s),缩放从initialScale增大到maxScaleif (progress < 0.2) { // 平滑插值从initialScale到maxScale icon.scale = icon.initialScale + (icon.maxScale - icon.initialScale) * (progress / 0.2)} else { // 保持maxScale icon.scale = icon.maxScale}
// 在绘制时应用缩放// 设置缩放(从中心点缩放)this.context.translate(icon.x, icon.y)this.context.scale(icon.scale, icon.scale)this.context.translate(-icon.x, -icon.y)
现在,图标会在短时间内从小变大,然后保持大小不变,直到消失。为了确保变换正确,我们使用了 translate 和 scale 组合来实现从中心点缩放。
8. 添加左右摆动效果
最后,我们来实现图标左右摆动的效果,让整个动画更加生动自然。
// 创建图标时设置摆动参数createIcon(x: number, y: number, radius: number, emoji: string): LikeIcon { // ... 其他参数设置 ... // 减小摆动幅度,改为最大8-15像素 const maxOffset = 8 + Math.random() * 7 // 最大摆动幅度8-15像素 // 随机决定初始摆动方向 const direction = Math.random() > 0.5 ? 1 : -1 return { // ... 其他属性 ... maxOffset: maxOffset, // 最大摆动幅度 direction: direction // 初始摆动方向 }}
// 在updateIcons中添加水平摆动逻辑// 更新X坐标 - 快速的左右摆动// 每0.25秒一个阶段,总共1秒4个阶段let horizontalOffset = 0;
if (progress < 0.25) { // 0-0.25s: 无偏移,专注于放大 horizontalOffset = 0;} else if (progress < 0.5) { // 0.25-0.5s: 向左偏移 const phaseProgress = (progress - 0.25) / 0.25; horizontalOffset = -icon.maxOffset * phaseProgress * icon.direction;} else if (progress < 0.75) { // 0.5-0.75s: 从向左偏移变为向右偏移 const phaseProgress = (progress - 0.5) / 0.25; horizontalOffset = icon.maxOffset * (2 * phaseProgress - 1) * icon.direction;} else { // 0.75-1s: 从向右偏移回到向左偏移 const phaseProgress = (progress - 0.75) / 0.25; horizontalOffset = icon.maxOffset * (1 - 2 * phaseProgress) * icon.direction;}
icon.x = icon.initialX + horizontalOffset;
这个摆动算法将 1 秒的生命周期分为 4 个阶段:
前 25%时间:保持在原点,没有摆动,专注于放大效果
25%-50%时间:向左偏移到最大值
50%-75%时间:从向左偏移变为向右偏移
75%-100%时间:从向右偏移变回向左偏移
这样就形成了一个完整的"向左向右向左"摆动轨迹,非常符合物理世界中物体的运动规律。
9. 优化绘制代码
最后,我们需要优化绘制代码,正确处理状态保存和恢复,确保每个图标的绘制不会相互影响。
// 绘制所有图标drawAllIcons() { // 清除画布 this.context.clearRect(0, 0, this.context.width, this.context.height) // 绘制所有图标 for (let icon of this.likeIcons) { this.context.save() // 保存当前状态 // 设置透明度 this.context.globalAlpha = icon.opacity // 设置缩放(从中心点缩放) this.context.translate(icon.x, icon.y) this.context.scale(icon.scale, icon.scale) this.context.translate(-icon.x, -icon.y) // 绘制emoji this.context.font = `${icon.fontSize}px` this.context.textAlign = 'center' this.context.textBaseline = 'middle' this.context.fillText(icon.emoji, icon.x, icon.y) this.context.restore() // 恢复之前保存的状态 }}
每次绘制图标前调用save()方法,绘制完成后调用restore()方法,确保每个图标的绘制参数不会影响其他图标。
10. 完整代码
将上述所有步骤整合起来,我们就得到了一个完整的点赞动画效果。下面是完整的代码实现:
@Entry@Componentstruct CanvasLike { // 用来配置CanvasRenderingContext2D对象的参数,开启抗锯齿 private settings: RenderingContextSettings = new RenderingContextSettings(true) // 正确创建CanvasRenderingContext2D对象 private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings) @State likeIcons: LikeIcon[] = [] // 存储所有点赞图标 private animationId: number = 0 // 动画ID // emoji表情数组 private readonly emojis: string[] = [ '❤️', '🧡', '💛', '💚', '💙', '💜', '🐻', '🐼', '🐨', '🦁', '🐯', '🦊', '🎁', '🎀', '🎉', '🎊', '✨', '⭐' ]
aboutToAppear() { // 启动动画循环 this.startAnimation() }
aboutToDisappear() { // 清除动画循环 clearInterval(this.animationId) }
// 创建一个图标对象 createIcon(x: number, y: number, radius: number, emoji: string): LikeIcon { // 为图标生成随机属性 const initialScale = 0.4 + Math.random() * 0.2 // 初始缩放比例0.4-0.6 const maxScale = 1.0 + Math.random() * 0.3 // 最大缩放比例1.0-1.3 // 减小摆动幅度,改为最大8-15像素 const maxOffset = 8 + Math.random() * 7 // 最大摆动幅度8-15像素 // 随机决定初始摆动方向 const direction = Math.random() > 0.5 ? 1 : -1 return { x: x, y: y, initialX: x, // 记录初始X坐标 initialY: y, // 记录初始Y坐标 radius: radius, emoji: emoji, fontSize: Math.floor(radius * 1.2), opacity: 1.0, createTime: Date.now(), lifespan: 1000, // 1秒钟生命周期 scale: initialScale, // 当前缩放比例 initialScale: initialScale, // 初始缩放比例 maxScale: maxScale, // 最大缩放比例 maxOffset: maxOffset, // 最大摆动幅度 direction: direction // 初始摆动方向 } }
// 获取随机emoji getRandomEmoji(): string { return this.emojis[Math.floor(Math.random() * this.emojis.length)] }
// 添加新的点赞图标 addLikeIcon(x: number, y: number) { const radius = 80 + Math.random() * 20 // 随机大小80-100 const emoji = this.getRandomEmoji()
this.likeIcons.push(this.createIcon(x, y, radius, emoji)) }
// 开始动画循环 startAnimation() { this.animationId = setInterval(() => { this.updateIcons() this.drawAllIcons() }, 16) // 约60fps的刷新率 }
// 更新所有图标状态 updateIcons() { const currentTime = Date.now() const newIcons: LikeIcon[] = []
for (let icon of this.likeIcons) { // 计算图标已存在的时间 const existTime = currentTime - icon.createTime if (existTime < icon.lifespan) { // 计算存在时间比例 const progress = existTime / icon.lifespan // 1. 更新Y坐标 - 向上移动,速度变化更明显 const verticalDistance = 120 * Math.pow(progress, 0.7) // 使用幂函数让上升更快 icon.y = icon.initialY - verticalDistance // 2. 更新X坐标 - 快速的左右摆动 // 每0.25秒一个阶段,总共1秒4个阶段 let horizontalOffset = 0; if (progress < 0.25) { // 0-0.25s: 无偏移,专注于放大 horizontalOffset = 0; } else if (progress < 0.5) { // 0.25-0.5s: 向左偏移 const phaseProgress = (progress - 0.25) / 0.25; horizontalOffset = -icon.maxOffset * phaseProgress * icon.direction; } else if (progress < 0.75) { // 0.5-0.75s: 从向左偏移变为向右偏移 const phaseProgress = (progress - 0.5) / 0.25; horizontalOffset = icon.maxOffset * (2 * phaseProgress - 1) * icon.direction; } else { // 0.75-1s: 从向右偏移回到向左偏移 const phaseProgress = (progress - 0.75) / 0.25; horizontalOffset = icon.maxOffset * (1 - 2 * phaseProgress) * icon.direction; } icon.x = icon.initialX + horizontalOffset; // 3. 更新缩放比例 - 快速放大 // 在生命周期的前20%阶段(0.2s),缩放从initialScale增大到maxScale if (progress < 0.2) { // 平滑插值从initialScale到maxScale icon.scale = icon.initialScale + (icon.maxScale - icon.initialScale) * (progress / 0.2) } else { // 保持maxScale icon.scale = icon.maxScale } // 4. 更新透明度 - 前60%保持不变,后40%逐渐消失 if (progress > 0.6) { // 在最后40%的生命周期内改变透明度,使消失更快 icon.opacity = 1.0 - ((progress - 0.6) / 0.4) } else { icon.opacity = 1.0 } // 保留未完成生命周期的图标 newIcons.push(icon) } } // 更新图标数组 this.likeIcons = newIcons }
// 绘制所有图标 drawAllIcons() { // 清除画布 this.context.clearRect(0, 0, this.context.width, this.context.height) // 绘制所有图标 for (let icon of this.likeIcons) { this.context.save() // 设置透明度 this.context.globalAlpha = icon.opacity // 设置缩放(从中心点缩放) this.context.translate(icon.x, icon.y) this.context.scale(icon.scale, icon.scale) this.context.translate(-icon.x, -icon.y) // 绘制emoji this.context.font = `${icon.fontSize}px` this.context.textAlign = 'center' this.context.textBaseline = 'middle' this.context.fillText(icon.emoji, icon.x, icon.y) this.context.restore() } }
build() { Column() { Stack() { Text('直播点赞效果')
Canvas(this.context) .width('100%') .height('100%') .onReady(() => { // Canvas已准备好,可以开始绘制 console.info(`Canvas size: ${this.context.width} x ${this.context.height}`) }) .onClick((event: ClickEvent) => { console.info(`Clicked at: ${event.x}, ${event.y}`) this.addLikeIcon(event.x, event.y) }) } } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) .backgroundColor(Color.White) .expandSafeArea() }}
总结
通过本教程,我们学习了如何使用 HarmonyOS 的 Canvas 组件实现直播点赞动画效果。我们从最基础的静态图标绘制开始,逐步形成了一个生动自然的点赞动画。在实现过程中,我们学习了以下重要知识点:
Canvas 的基本使用方法
动画循环系统的实现
图形变换(缩放、平移)
透明度控制
非线性动画实现
状态管理的重要性
通过这些技术,你可以创建出更多丰富多彩的动画效果,提升你的应用的用户体验。希望本教程对你有所帮助!
版权声明: 本文为 InfoQ 作者【苏杰豪】的原创文章。
原文链接:【http://xie.infoq.cn/article/799fc8c9f5cae91a29f4c3be9】。文章转载请联系作者。
苏杰豪
鸿蒙很开门~ 2019-03-30 加入
传智教育、黑马程序员课程研究员









评论