鸿蒙Next实现仿抖音点赞动画功能

实现目标: 抖音小视频播放时,双击屏幕会在手指点击位置出现一个小红心,由大变小,然后放大渐变消失,并且点击速度快,可以出现多个小红心动画,每次出现的小红心会有一个小角度的旋转。
实现思路:
1.双击屏幕,在手指点击位置会出现小心心,因此需要监听双击手势,并且获取到点击坐标
2.快速点击,会产生多个小心心的动画,因此需要动态添加view
3.动画过程,大概是出现后先缩小后放大逐渐消失
实现结果:

演示.gif

实现过程:
1.在需要展示动画的父布局中,添加占位组件ContentSlot()用于动态添加点赞动画
2.之前一篇介绍了动态添加布局,需要定一个布局,和一个参数对象

Column(){
      ContentSlot(this.content)
    }
    .height('100%')
    .width('100%')
    .gesture(
      TapGesture({ count: 2 })
        .onAction((event: GestureEvent) => {
          let buildNode = new BuilderNode<[ParamsInterface]>(this.getUIContext());
          let redowAngle=(Math.random()*2-1)*50;
          buildNode.build(wrapBuilder<[ParamsInterface]>(buildHeartView), {
            x: event.fingerList[0].localX,
            y: event.fingerList[0].localY,
            opacity:1,
            scale:1.3,
            duration:300,
            angle:redowAngle,
            finish:()=>{
              buildNode.update({
                x: event.fingerList[0].localX,
                y: event.fingerList[0].localY,
                scale:1,
                opacity:1,
                duration:300,
                angle:redowAngle,
              })
              setTimeout(()=>{
                buildNode.update({
                  x: event.fingerList[0].localX,
                  y: event.fingerList[0].localY,
                  scale:3,
                  opacity:0,
                  duration:500,
                  angle:redowAngle,
                })
              },300)
              setTimeout(()=>{
                this.content.removeFrameNode(buildNode.getFrameNode())
              },1000)
            }
          }, { nestingBuilderSupported: true });
          this.content.addFrameNode(buildNode.getFrameNode());
        })
    )
interface ParamsInterface {
  x:number
  y:number
  finish:() => void
  opacity:number
  scale:number
  duration:number
  angle:number
}
@Builder
function buildHeartView(params: ParamsInterface) {
  Image($r('app.media.heart')).width(40).height(40)
    .position({
      x: params.x,
      y: params.y,
    })
    .opacity(params.opacity)
    .scale({x:params.scale,y:params.scale})
    .animation({
      duration: params.duration, //动画持续时间,单位为毫秒
      curve: Curve.Linear, //动画曲线
      iterations: 1, //动画播放次数
      playMode: PlayMode.Normal, //动画播放模式 正向
    })
    .onAppear( params.finish)
}

4.使用动画,需要对Image的属性进行修改,但是buildHeartView不支持内部定义参数,所以需要修改params的值,这时需要用到BuilderNode的update方法更新参数。这里不能直接在buildHeartView内里修改参数,否侧会报错。例如在onAppear中修改透明度的值,会闪退:

报错.png

实现优化:
这里我们需要展示2个动画,需要在主布局中维护想要调整的属性值,后期如果修改或者复用,一点也不方便,如何将动画的控制让Image自己控制呢? 主布局中我只要触发动画就行。实现也很简单,只要再定义一个全局的Component,动画相关的事情让这个子组件去实现,buildHeartView中引用这个全局Component。动态节点只需要负责展示定位和角度就可以了。最后加一个动画结束的回调用于移除节点。
最终源码:

import { BuilderNode, NodeContent } from '@kit.ArkUI';

@Entry
@ComponentV2
struct HeartTest{
  content: NodeContent = new NodeContent();
  build() {
    Column(){
      ContentSlot(this.content)
    }
    .height('100%')
    .width('100%')
    .gesture(
      TapGesture({ count: 2 })
        .onAction((event: GestureEvent) => {
          let buildNode = new BuilderNode<[ParamsInterface]>(this.getUIContext());
          buildNode.build(wrapBuilder<[ParamsInterface]>(buildHeartView), {
            x: event.fingerList[0].localX,
            y: event.fingerList[0].localY,
            finish:()=>{
              this.content.removeFrameNode(buildNode.getFrameNode())
            }
          }, { nestingBuilderSupported: true });
          this.content.addFrameNode(buildNode.getFrameNode());

        })
    )
  }
}
interface ParamsInterface {
  x:number
  y:number
  finish:() => void
}

@Builder
function buildHeartView(params: ParamsInterface) {
  HeartImageAnimation({ finish:params.finish}).position({
    x: params.x,
    y: params.y,
  }) .rotate({angle:(Math.random()*2-1)*50})
}
@ComponentV2
struct HeartImageAnimation{
  @Local opacityNor:number=1
  @Local scanOption:ScaleOptions={
    x:1.3,
    y:1.3
  }
  @BuilderParam finish:() => void
  build() {
    Image($r('app.media.heart')).width(40).height(40)
      .opacity(this.opacityNor)
      .scale(this.scanOption).onAppear(()=>{
      this.getUIContext().animateTo({
        duration: 300, //动画持续时间,单位为毫秒
        curve: Curve.EaseOut, //动画曲线
        iterations: 1, //动画播放次数
        playMode: PlayMode.Normal, //动画播放模式 正向
        onFinish: () => { //动画播放完成回调
          this.getUIContext().animateTo({
            duration: 500, //动画持续时间,单位为毫秒
            curve: Curve.EaseOut, //动画曲线
            iterations: 1, //动画播放次数
            playMode: PlayMode.Normal, //动画播放模式 正向
            onFinish: () => { //动画播放完成回调
              this.finish
            }
          },()=>{
            this.scanOption  = {
              x: 3,
              y: 3,
            }
            this.opacityNor = 0
          })
        }
      },()=>{
        this.scanOption  = {
          x: 1,
          y: 1,
        }
      })
    })
  }
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容