鸿蒙中 @AnimatableExtend自定义属性动画
摘要:ArkUI通过@AnimatableExtend装饰器支持属性动画,包括数值类型和自定义类型动画。自定义类型需实现AnimatableArithmetic接口的加减乘和相等方法,以支持插值计算。使用步骤包括定义可动画属性接口、应用到组件、配置动画参数和触发动画。该技术适用于简单属性变化、复杂图形变形等场景,通过状态管理实现平滑过渡效果,同时需注意性能优化。
本文同步发表于微信公众号,微信搜索 程语新视界 即可关注,每个工作日都有文章更新
属性动画是指当可动画属性的参数值发生变化时,在UI上产生的连续视觉效果。
-
实现条件:
-
参数值发生连续变化
-
设置到能引起UI变化的属性接口上
-
ArkUI提供的支持
-
@AnimatableExtend装饰器:用于自定义可动画属性接口。
-
支持的数据类型:
-
number类型:基础数值类型
-
实现
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>接口 -
接口要求实现四个方法:
-
加法:
add(rhs: T): T -
减法:
subtract(rhs: T): T -
乘法:
multiply(scale: number): T -
相等判断:
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. 数据类型限制
-
参数类型必须连续:支持数值插值计算
-
支持两种类型:
-
number:基础数值,系统自动处理插值
-
自定义类型:必须实现
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. 性能考虑
-
避免过度计算:自定义类型的运算应尽量高效
-
减少对象创建:在动画循环中避免频繁创建新对象
-
合理使用状态管理:只有需要触发动画时才改变状态
五、实现流程
-
分析需求:确定要动画化的属性和数据类型
-
选择数据类型:
-
简单属性 → 使用number类型
-
复杂属性 → 自定义类型实现
AnimatableArithmetic<T>
-
-
定义装饰器函数:使用
@AnimatableExtend装饰目标组件 -
实现属性接口:在装饰器函数中调用系统属性接口
-
应用到组件:在组件中使用自定义属性接口
-
配置动画参数:设置duration、curve等动画参数
-
触发动画:通过状态变化或事件触发动画
更多推荐


所有评论(0)