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

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

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

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

效果预览

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

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

1. 基础结构搭建

首先,我们创建一个基本的页面结构和数据模型,用于管理点赞图标和动画。

定义图标数据结构

typescript 复制代码
// 定义点赞图标数据结构
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)
}

这个接口定义了每个点赞图标所需的所有属性,从位置到动画参数,为后续的动画实现提供了数据基础。

组件基本结构

typescript 复制代码
@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图标。

创建图标生成函数

typescript 复制代码
// 创建一个图标对象
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() // 重新绘制所有图标
}

实现基本绘制函数

typescript 复制代码
// 绘制所有图标
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)
  }
}

绑定点击事件

typescript 复制代码
.onClick((event: ClickEvent) => {
  console.info(`Clicked at: ${event.x}, ${event.y}`)
  this.addLikeIcon(event.x, event.y)
})

此时,每次点击Canvas,就会在点击位置绘制一个随机的emoji图标。但这些图标是静态的,不会移动或消失。

3. 添加动画循环系统

为了实现动画效果,我们需要一个动画循环系统,定期更新图标状态并重新绘制。

typescript 复制代码
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. 实现向上移动效果

让我们首先让图标动起来,实现一个简单的向上移动效果。

typescript 复制代码
// 更新所有图标状态
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. 添加非线性运动效果

为了让动画更加自然,我们可以使用幂函数来模拟非线性运动,使图标开始时移动较慢,然后加速。

typescript 复制代码
// 更新Y坐标 - 向上移动,速度变化更明显
const verticalDistance = 120 * Math.pow(progress, 0.7) // 使用幂函数让上升更快
icon.y = icon.initialY - verticalDistance

幂指数0.7使得图标的上升速度随时间增加,创造出更加自然的加速效果。

6. 添加渐隐效果

接下来,让图标在上升过程中逐渐消失,增加视觉上的层次感。

typescript 复制代码
// 更新透明度 - 前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. 实现放大效果

现在,让我们添加图标从小变大的动画效果,这会让整个动画更加生动。

typescript 复制代码
// 创建图标时设置初始和最大缩放比例
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增大到maxScale
if (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. 添加左右摆动效果

最后,我们来实现图标左右摆动的效果,让整个动画更加生动自然。

typescript 复制代码
// 创建图标时设置摆动参数
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个阶段:

  1. 前25%时间:保持在原点,没有摆动,专注于放大效果
  2. 25%-50%时间:向左偏移到最大值
  3. 50%-75%时间:从向左偏移变为向右偏移
  4. 75%-100%时间:从向右偏移变回向左偏移

这样就形成了一个完整的"向左向右向左"摆动轨迹,非常符合物理世界中物体的运动规律。

9. 优化绘制代码

最后,我们需要优化绘制代码,正确处理状态保存和恢复,确保每个图标的绘制不会相互影响。

typescript 复制代码
// 绘制所有图标
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. 完整代码

将上述所有步骤整合起来,我们就得到了一个完整的点赞动画效果。下面是完整的代码实现:

typescript 复制代码
@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.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的基本使用方法
  • 动画循环系统的实现
  • 图形变换(缩放、平移)
  • 透明度控制
  • 非线性动画实现
  • 状态管理的重要性

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

相关推荐
一个处女座的程序猿O(∩_∩)O4 小时前
鸿蒙Next与API 12深度解析:架构、开发实践与代码示例
华为·架构·harmonyos
月未央4 小时前
HarmonyOS Next 状态管理:Monitor 装饰器实践
ios·harmonyos
苏杰豪5 小时前
# 鸿蒙特效教程03-水波纹动画效果实现教程
华为·harmonyos
啊是是是5 小时前
HarmonyOS申请用户位置信息授权和再次授权-系统级API获取地理位置的错误码类型问题getCurrentLocation()
harmonyos
ssr——ssss5 小时前
网络华为HCIA+HCIP数据链路层协议-以太网协议
网络·华为
猿六凯6 小时前
历年云南大学计算机复试上机真题
java·华为od·华为
TARDIS_20207 小时前
OpenHarmony-XTS测试
harmonyos
__Benco7 小时前
OpenHarmony子系统开发 - Rust编译构建指导
开发语言·人工智能·后端·rust·harmonyos
一个处女座的程序猿O(∩_∩)O7 小时前
harmonyOS NEXT开发与前端开发深度对比分析
前端·华为·harmonyos