本文同步发表于微信公众号,微信搜索 程语新视界 即可关注,每个工作日都有文章更新

属性动画是指当可动画属性的参数值发生变化时,在UI上产生的连续视觉效果。

  • 实现条件:

    • 参数值发生连续变化

    • 设置到能引起UI变化的属性接口上

ArkUI提供的支持

  • @AnimatableExtend装饰器:用于自定义可动画属性接口。

  • 支持的数据类型

    1. number类型:基础数值类型

    2. 实现 AnimatableArithmetic<T> 接口的自定义类型:用于复杂数据结构的动画

二、@AnimatableExtend装饰器的使用

1. 作用

  • 扩展组件的属性,使其支持动画效果

  • 可以将原本不可动画的属性变为可动画,或增强已有属性的动画能力

2. 使用步骤

示例1:使用number类型改变Text组件宽度

第一步:定义可动画属性接口

@AnimatableExtend(Text)
function animatableWidth(width: number) {
    .width(width)  // 调用系统属性接口
}
  • 使用 @AnimatableExtend(Text) 装饰器扩展Text组件

  • 定义 animatableWidth 函数,参数为 number 类型

  • 函数体内调用 .width() 系统属性接口

第二步:应用到组件

Text("AnimatableProperty")
    .animatableWidth(this.textWidth)  // 使用自定义属性接口

第三步:绑定动画参数

.animation({ duration: 2000, curve: Curve.Ease })
  • 设置动画时长2000ms

  • 使用Ease曲线

第四步:触发动画

Button("Play")
    .onClick(() => {
        this.textWidth = this.textWidth == 80 ? 160 : 80;
    })
  • 改变属性值触发动画

  • 宽度在80和160之间切换

实现效果
  • Text组件宽度从80逐步变化到160(或反向)

  • 产生平滑的宽度变化动画

  • 实现了逐帧布局效果:通过逐帧回调函数每帧修改可动画属性的值

三、自定义数据类型实现复杂动画

1. 核心要求

  • 自定义类型必须实现 AnimatableArithmetic<T> 接口

  • 接口要求实现四个方法:

    1. 加法add(rhs: T): T

    2. 减法subtract(rhs: T): T

    3. 乘法multiply(scale: number): T

    4. 相等判断equals(rhs: T): boolean

2. 示例:图形形状动画

第一步:定义基础点类型
declare type Point = number[];
第二步:实现PointClass(单个点)
class PointClass extends Array<number> {
    constructor(value: Point) {
        super(value[0], value[1])  // 二维点坐标
    }
    
    // 实现向量加法
    add(rhs: PointClass): PointClass {
        let result: Point = new Array<number>() as Point;
        for (let i = 0; i < 2; i++) {
            result.push(rhs[i] + this[i])
        }
        return new PointClass(result);
    }
    
    // 实现向量减法
    subtract(rhs: PointClass): PointClass {
        // ...类似加法实现
    }
    
    // 实现标量乘法
    multiply(scale: number): PointClass {
        // ...缩放坐标值
    }
}
第三步:实现PointVector(点向量)
class PointVector extends Array<PointClass> implements AnimatableArithmetic<Array<Point>> {
    constructor(initialValue: Array<Point>) {
        super();
        if (initialValue.length) {
            initialValue.forEach((p: Point) => this.push(new PointClass(p)))
        }
    }
    
    // 实现AnimatableArithmetic接口
    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[i].add(rhs[i]))  // 逐点相加
        }
        return result;
    }
    
    subtract(rhs: PointVector): PointVector {
        // ...类似plus实现
    }
    
    multiply(scale: number): PointVector {
        // ...逐点缩放
    }
    
    equals(rhs: PointVector): boolean {
        // 比较长度和每个点的坐标
        if (this.length !== rhs.length) return false;
        for (let index = 0; index < this.length; ++index) {
            if (this[index][0] !== rhs[index][0] || this[index][1] !== rhs[index][1]) {
                return false;
            }
        }
        return true;
    }
}
第四步:定义可动画属性接口
@AnimatableExtend(Polyline)
function animatablePoints(points: PointVector) {
    .points(points)  // 设置Polyline的点集
}
第五步:定义动画状态
// 定义正方形起始点、宽度、平移距离
squareStartPointX: number = 75;
squareStartPointY: number = 25;
squareWidth: number = 150;
squareEndTranslateX: number = 50;
squareEndTranslateY: number = 50;

// 状态1:正方形
@State pointVec1: PointVector = new PointVector([
    [this.squareStartPointX, this.squareStartPointY],
    [this.squareStartPointX + this.squareWidth, this.squareStartPointY],
    [this.squareStartPointX + this.squareWidth, this.squareStartPointY + this.squareWidth],
    [this.squareStartPointX, this.squareStartPointY + this.squareWidth]
]);

// 状态2:变形后的形状
@State pointVec2: PointVector = new PointVector([
    [this.squareStartPointX + this.squareEndTranslateX, this.squareStartPointY + this.squareStartPointY],
    [this.squareStartPointX + this.squareWidth + this.squareEndTranslateX, this.squareStartPointY + this.squareStartPointY],
    [this.squareStartPointX + this.squareWidth, this.squareStartPointY + this.squareWidth],
    [this.squareStartPointX, this.squareStartPointY + this.squareWidth]
]);

// 当前显示的点的集合
@State polyline1Vec: PointVector = this.pointVec1;
第六步:应用到组件并绑定动画
Polyline()
    .width(300)
    .height(200)
    .backgroundColor("#0C000000")
    .fill('#317AF7')
    .animatablePoints(this.polyline1Vec)  // 使用自定义属性
    .animation({ duration: 2000, delay: 0, curve: Curve.Ease })
    .onClick(() => {
        // 点击切换形状
        if (this.polyline1Vec.equals(this.pointVec1)) {
            this.polyline1Vec = this.pointVec2;
        } else {
            this.polyline1Vec = this.pointVec1;
        }
    })
动画效果
  • 正方形图形平滑变形为另一种四边形形状

  • 四个顶点坐标同时进行插值动画

  • 点击图形可在两种形状间切换

四、技术要点

1. 数据类型限制

  • 参数类型必须连续:支持数值插值计算

  • 支持两种类型

    1. number:基础数值,系统自动处理插值

    2. 自定义类型:必须实现 AnimatableArithmetic<T> 接口

2. AnimatableArithmetic<T>接口要求

方法 作用 动画计算中的用途
add(rhs: T): T 加法运算 计算起始值和结束值的中间状态
subtract(rhs: T): T 减法运算 计算差值,用于插值计算
multiply(scale: number): T 标量乘法 乘以时间插值比例
equals(rhs: T): boolean 相等判断 判断动画是否结束

3. 嵌套支持

  • 模板T支持嵌套实现 AnimatableArithmetic<T> 的类型

  • 如示例中的 PointVector 包含 PointClass,两者都实现了相应接口

4. 动画触发方式

  • 直接赋值:改变状态变量的值

  • 结合animateTo或animation:提供更灵活的动画控制

  • 事件触发:如onClick、定时器等

5. 应用场景

场景 说明 示例
简单属性动画 单个数值属性的变化 宽度、高度、透明度等
复杂图形动画 多个相关属性同时变化 图形变形、路径动画
逐帧布局 每帧修改布局属性 动态调整组件大小、位置
不可动画属性动画化 将原本不支持动画的属性变为可动画 自定义属性扩展

6. 性能考虑

  • 避免过度计算:自定义类型的运算应尽量高效

  • 减少对象创建:在动画循环中避免频繁创建新对象

  • 合理使用状态管理:只有需要触发动画时才改变状态

五、实现流程

  1. 分析需求:确定要动画化的属性和数据类型

  2. 选择数据类型

    • 简单属性 → 使用number类型

    • 复杂属性 → 自定义类型实现 AnimatableArithmetic<T>

  3. 定义装饰器函数:使用 @AnimatableExtend 装饰目标组件

  4. 实现属性接口:在装饰器函数中调用系统属性接口

  5. 应用到组件:在组件中使用自定义属性接口

  6. 配置动画参数:设置duration、curve等动画参数

  7. 触发动画:通过状态变化或事件触发动画

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐