鸿蒙特效教程04-直播点赞动画效果实现教程

news/2025/3/15 22:13:02/文章来源:https://www.cnblogs.com/Megasu/p/18774272

鸿蒙特效教程04-直播点赞动画效果实现教程

在时下流行的直播、短视频等应用中,点赞动画是提升用户体验的重要元素。当用户点击屏幕时,屏幕上会出现飘动的点赞图标,感觉挺好玩的。

本教程适合HarmonyOS初学者,通过简单到复杂的步骤,通过HarmonyOS的Canvas组件,一步步实现这个好玩的点赞动画效果。

效果预览

我们将实现的效果是:用户点击屏幕时,在点击位置生成一个emoji表情图标,逐步添加了以下动画效果:

  1. 向上移动:让图标从点击位置向上飘移
  2. 非线性运动:使用幂函数让移动更加自然
  3. 渐隐效果:让图标在上升过程中逐渐消失
  4. 放大效果:让图标从小变大
  5. 左右摆动:增加水平方向的微妙摆动

image

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
@Component
struct 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}
}// 获取随机emoji
getRandomEmoji(): 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) {// 绘制emojithis.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.createTimeif (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.createTimeif (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.6const maxScale = 1.0 + Math.random() * 0.3 // 最大缩放比例1.0-1.3// ... 其他属性设置 ...return {// ... 其他属性 ...scale: initialScale, // 当前缩放比例initialScale: initialScale, // 初始缩放比例maxScale: maxScale, // 最大缩放比例// ... 其他属性 ...}
}// 在updateIcons中更新缩放比例
// 更新缩放比例 - 快速放大
// 在生命周期的前20%阶段(0.2s),缩放从initialScale增大到maxScale
if (progress < 0.2) {// 平滑插值从initialScale到maxScaleicon.scale = icon.initialScale + (icon.maxScale - icon.initialScale) * (progress / 0.2)
} else {// 保持maxScaleicon.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 : -1return {// ... 其他属性 ...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个阶段:

  1. 前25%时间:保持在原点,没有摆动,专注于放大效果
  2. 25%-50%时间:向左偏移到最大值
  3. 50%-75%时间:从向左偏移变为向右偏移
  4. 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)// 绘制emojithis.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
@Component
struct 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.6const 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 // 初始摆动方向}}// 获取随机emojigetRandomEmoji(): string {return this.emojis[Math.floor(Math.random() * this.emojis.length)]}// 添加新的点赞图标addLikeIcon(x: number, y: number) {const radius = 80 + Math.random() * 20 // 随机大小80-100const 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.createTimeif (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增大到maxScaleif (progress < 0.2) {// 平滑插值从initialScale到maxScaleicon.scale = icon.initialScale + (icon.maxScale - icon.initialScale) * (progress / 0.2)} else {// 保持maxScaleicon.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)// 绘制emojithis.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的基本使用方法
  • 动画循环系统的实现
  • 图形变换(缩放、平移)
  • 透明度控制
  • 非线性动画实现
  • 状态管理的重要性

通过这些技术,你可以创建出更多丰富多彩的动画效果,提升你的应用的用户体验。希望本教程对你有所帮助!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/899407.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

鸿蒙特效教程02-微信语音录制动画效果实现教程

鸿蒙特效教程02-微信语音录制动画效果实现教程本教程适合HarmonyOS初学者,通过简单到复杂的步骤,一步步实现类似微信APP中的语音录制动画效果。最终效果预览 我们将实现以下功能:长按"按住说话"按钮:显示录音界面和声波动画 录音过程中显示实时时长 手指上滑:取…

SpringBoot使用Kafka生产者、消费者

SpringBoot使用Kafka生产者、消费者@目录依赖配置文件生产者消费者 依赖 <!--kafka--> <dependency><groupId>org.springframework.kafka</groupId><artifactId>spring-kafka</artifactId><version>xxx</version> </depend…

jasperreport 使用和问题记录

引言:之前一直没有记录的习惯,所以在这里记录有些是回忆,所以可能有些不连贯.而且本人的问题不一定是你的问题,仅作参考. (注:本人是使用java生成pdf文件) 1.介绍(只介绍自己用的到) 进入软件 File->new->jasper report->blank A4 我们得到了一张A4纸,如下图所示.(注:…

Java学习四大名著:《Java核心技术 卷II:高级特性(原书第12版)》 | PDF免费下载

《Java核心技术 卷II:高级特性(原书第12版)》是Java学习的经典书籍,该书深入探讨了 Java 语言的高级功能,涵盖流与文件处理、并发编程、网络、数据库、JVM 调优等关键主题。适合有一定基础的开发者,帮助他们掌握高效编程技巧,优化性能,并深入理解 Java 生态系统,是进阶…

STM32CubeIDE 下载与安装教程(以 1.18.0 版本为例)

STM32CubeIDE 下载与安装教程(以 1.18.0 版本为例) 软件介绍 STM32CubeIDE 是 STMicroelectronics 提供的官方集成开发环境(IDE),专为 STM32 微控制器系列设计。它融合了 STM32CubeMX 和 Eclipse 的强大功能,提供了一个完整的开发环境,适用于嵌入式软件开发和调试。STM3…

【作业3】结对项目:实现一个自动生成小学四则运算题目的命令行程序

⭐成员:3223004473詹艺珏 and 3223004301吴梦琪 📎Github链接:https://github.com/Jue610/Jue610/tree/main/ArithProbelm这个作业属于哪个课程 23软件工程这个作业要求在哪里 【作业3】结对项目这个作业的目标 实现一个自动生成小学四则运算题目的命令行程序,培养团…

用ESP32做一个遥控机械臂

RC汽车机械臂只是一个有轮子的RC汽车。该机械臂是一个四自由度机械臂,这意味着它有四个运动部件。我使用mg90伺服电机,因为它们是金属的,但塑料sg90也应该工作。如下图所示。我使用了两个独立的电源,每个都由2S锂离子(Li-on)电池组成,一个用于MCU,另一个用于电机。这是为…

最小化安装Ubuntu

最小化安装 前言: 有时候需要搭建虚拟机,每一次都需要去找文章,搞小半天才能完成环境搭建 这一次写一篇文章记录一下,目的是以后能比较快速简单的搭建好环境 概要 最小化安装需要手动启用网卡 安装防火墙 ⇒ 打开某些端口确保ssh连接等 安装openssh-server openssh-client ⇒ …

愤怒的小鸟

Day 2025/2/20愤怒的小鸟剪切精灵图-改sprite mode为mutiple在sprite editor中slice设置弹弓与鸟的层级关系layer-player 鸟加springjoint组件-distance-0.3-autodistance关闭弹弓组件的一些基本属性:Distance:两点之间的固定距离(设定完有剩余的距离就是可拉伸的长度)Freq…

通过振动传感器,触发水的运动并将其转换为声音

“微挑战”是在巴塞罗那IAAC的“紧急未来设计硕士”课程中为期一周的工作坊。在这一周,我们有时间、空间和专业人士的支持来创建一个功能原型,它也可以是一个投机性的人工制品,有助于我们的个人研究和实践。这个原型应该基于迭代和使用:数字制造工具、生物制造、人工智能、…

单链表练习与重下AS

1.练习题:我的代码答案: /*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/ struct ListNode* reverseList(struct ListNode* head) {struct ListNode dummpy;struct ListNode* tmp;dummpy.next=NULL;//引入临时…

pycharm连接autodl服务器

昨天听舍友说,他们第一个实验都跑完了,瞬间焦虑起来,原来落后这么多,完事昨天晚上7点开始看教程,配环境,看了好几个教程,每个教程都不一样竟然!然后问舍友,舍友说:你先知道你要跑什么项目,我说:我知道啊,我要做什么项目,想要什么结果,我都知道,项目和代码都有,…