头图

在最近的项目开发中,我遇到了需要实现复杂动画效果的需求。在探索解决方案的过程中,我发现了 ​​@AnimatableExtend​​​ 装饰器,它为实现动画效果提供了一种非常灵活且强大的方式。然而,在学习这个装饰器的过程中,我发现相关的资料并不是特别丰富,而且很多资料都缺乏系统性的讲解。因此,我决定写这篇博客,将自己的学习经验和理解分享出来,希望能帮助更多的开发者快速掌握 ​​@AnimatableExtend​​ 装饰器的使用。

1. ​​@AnimatableExtend​​ 装饰器概述

​@AnimatableExtend​​ 装饰器从 API Version 10 开始支持,为动画效果的实现提供了一种便捷的方式。从 API version 11 开始,它支持在元服务中使用。这个装饰器允许我们自定义动画属性,使得我们可以对不同类型的数据进行动画处理。

1.1 装饰器使用规则

  • 定义位置:​​@AnimatableExtend​​ 仅支持定义在全局,不支持在组件内部定义。
  • 参数类型:​​@AnimatableExtend​​​ 定义的函数参数类型必须为 ​​number​​​ 类型或者实现 ​​AnimatableArithmetic<T>​​ 接口的自定义类型。
  • 函数体限制:​​@AnimatableExtend​​​ 定义的函数体内只能调用 ​​@AnimatableExtend​​ 括号内组件的属性方法。

1.2 ​​AnimatableArithmetic<T>​​ 接口说明

​AnimatableArithmetic<T>​​​ 接口定义了非 ​​number​​​ 数据类型的动画运算规则。对于非 ​​number​​ 类型的数据(如数组、结构体、颜色等)做动画,需要实现该接口中的加法、减法、乘法和判断相等函数,使得该数据能参与动画的插值运算和识别该数据是否发生改变。

名称入参类型返回值类型说明
plusAnimatableArithmetic<T>AnimatableArithmetic<T>定义该数据类型的加法运算规则
subtractAnimatableArithmetic<T>AnimatableArithmetic<T>定义该数据类型的减法运算规则
multiplynumberAnimatableArithmetic<T>定义该数据类型的乘法运算规则
equalsAnimatableArithmetic<T>boolean定义该数据类型的相等判断规则

2. 使用场景示例

2.1 改变 Text 组件宽度实现逐帧布局效果

下面的示例通过改变 ​​Text​​ 组件的宽度实现逐帧布局的效果。

@AnimatableExtend(Text)
function animatableWidth(width: number) {
  .width(width)
}

@Entry
@Component
struct AnimatablePropertyExample {
  @State textWidth: number = 100;

  build() {
    Column() {
      Text("AnimatableProperty")
        .animatableWidth(this.textWidth)
        .animation({ duration: 3000, curve: Curve.EaseInOut })
      Button("Play")
        .onClick(() => {
          this.textWidth = this.textWidth == 100 ? 200 : 100;
        })
    }.width("100%")
    .padding(20)
  }
}

在这个示例中,我们定义了一个 ​​animatableWidth​​​ 函数,通过 ​​@AnimatableExtend​​​ 装饰器将其应用到 ​​Text​​​ 组件上。点击按钮时,​​textWidth​​ 的值会在 100 和 200 之间切换,从而触发动画效果。

2.2 实现折线的动画效果

为了实现折线的动画效果,我们需要定义一个自定义类型 ​​Point​​​ 和 ​​PointVector​​​,并让 ​​PointVector​​​ 实现 ​​AnimatableArithmetic<T>​​ 接口。

class Point {
  x: number
  y: number

  constructor(x: number, y: number) {
    this.x = x
    this.y = y
  }

  plus(rhs: Point): Point {
    return new Point(this.x + rhs.x, this.y + rhs.y)
  }

  subtract(rhs: Point): Point {
    return new Point(this.x - rhs.x, this.y - rhs.y)
  }

  multiply(scale: number): Point {
    return new Point(this.x * scale, this.y * scale)
  }

  equals(rhs: Point): boolean {
    return this.x === rhs.x && this.y === rhs.y
  }
}

// PointVector实现了AnimatableArithmetic<T>接口
class PointVector extends Array<Point> implements AnimatableArithmetic<PointVector> {
  constructor(value: Array<Point>) {
    super();
    value.forEach(p => this.push(p))
  }

  plus(rhs: PointVector): PointVector {
    let result = new PointVector([])
    const len = Math.min(this.length, rhs.length)
    for (let i = 0; i < len; i++) {
      result.push((this as Array<Point>)[i].plus((rhs as Array<Point>)[i]))
    }
    return result
  }

  subtract(rhs: PointVector): PointVector {
    let result = new PointVector([])
    const len = Math.min(this.length, rhs.length)
    for (let i = 0; i < len; i++) {
      result.push((this as Array<Point>)[i].subtract((rhs as Array<Point>)[i]))
    }
    return result
  }

  multiply(scale: number): PointVector {
    let result = new PointVector([])
    for (let i = 0; i < this.length; i++) {
      result.push((this as Array<Point>)[i].multiply(scale))
    }
    return result
  }

  equals(rhs: PointVector): boolean {
    if (this.length != rhs.length) {
      return false
    }
    for (let i = 0; i < this.length; i++) {
      if (!(this as Array<Point>)[i].equals((rhs as Array<Point>)[i])) {
        return false
      }
    }
    return true
  }

  get(): Array<Object[]> {
    let result: Array<Object[]> = []
    this.forEach(p => result.push([p.x, p.y]))
    return result
  }
}

@AnimatableExtend(Polyline)
function animatablePoints(points: PointVector) {
  .points(points.get())
}

@Entry
@Component
struct AnimatablePropertyExample {
  @State points: PointVector = new PointVector([
    new Point(30, Math.random() * 250),
    new Point(80, Math.random() * 250),
    new Point(130, Math.random() * 250),
    new Point(180, Math.random() * 250),
    new Point(230, Math.random() * 250),
  ])

  build() {
    Column() {
      Polyline()
        .animatablePoints(this.points)
        .animation({ duration: 1500, curve: Curve.EaseOut })// 设置动画参数
        .size({ height: 250, width: 350 })
        .fill(Color.Blue)
        .stroke(Color.Yellow)
        .backgroundColor('#aaccff')
      Button("Play")
        .onClick(() => {
          // points是实现了可动画协议的数据类型,points在动画过程中可按照定义的运算规则、动画参数从之前的PointVector变为新的PointVector数据,产生每一帧的PointVector数据,进而产生动画
          this.points = new PointVector([
            new Point(30, Math.random() * 250),
            new Point(80, Math.random() * 250),
            new Point(130, Math.random() * 250),
            new Point(180, Math.random() * 250),
            new Point(230, Math.random() * 250),
          ])
        })
    }.width("100%")
    .padding(20)
  }
}

在这个示例中,我们定义了 ​​Point​​​ 类表示二维平面上的点,​​PointVector​​​ 类表示点的数组。通过实现 ​​AnimatableArithmetic<T>​​​ 接口,我们为 ​​PointVector​​​ 定义了加法、减法、乘法和相等判断规则。然后,我们使用 ​​@AnimatableExtend​​​ 装饰器将 ​​animatablePoints​​​ 函数应用到 ​​Polyline​​ 组件上,点击按钮时,折线的顶点位置会随机变化,从而实现动画效果。

3. 总结

​@AnimatableExtend​​​ 装饰器为实现复杂动画效果提供了一种强大而灵活的方式。通过自定义动画属性和实现 ​​AnimatableArithmetic<T>​​​ 接口,我们可以对不同类型的数据进行动画处理。在实际开发中,我们可以根据具体需求灵活运用这个装饰器,实现各种炫酷的动画效果。希望这篇博客能帮助你快速掌握 ​​@AnimatableExtend​​ 装饰器的使用,让你的应用更加生动有趣。

通过以上的学习和实践,你可以逐步掌握 ​​@AnimatableExtend​​ 装饰器的使用方法,并且在实际项目中灵活运用,为用户带来更加丰富的动画体验。


李游Leo
6k 声望1.8k 粉丝

曾在百度、时趣互动、乐视等公司担任过高级前端(软件)开发工程师。后在北京一所当地大学任教,主要职务是教学主任,也为网易云课堂微专业的前端课程负责人。因为本身也是一名IT技术人员,所以非常关注网站制作...