发布于 2025-09-03
动画与交互
HarmonyOS
学习目标
通过本教程,你将学会:
- 理解动画的基本概念
- 掌握属性动画的使用
- 学会转场动画的实现
- 掌握手势识别和处理
- 实现自定义动画效果
- 优化动画性能
前置知识
- 完成生命周期管理
- 了解基本的动画概念(如果有 CSS/Web 动画经验更佳)
核心概念
动画系统
ArkUI 提供了丰富的动画 API,可以实现流畅的 UI 动画效果。
对比 Web 开发:
- CSS
transition→ ArkUIanimateTo - CSS
@keyframes→ ArkUI 动画 API - CSS
transform→ ArkUI 变换属性
详细内容
1. 属性动画
animateTo 基础使用
@Entry
@Component
struct AnimationExample {
@State width: number = 100;
@State height: number = 100;
@State opacity: number = 1.0;
@State angle: number = 0;
build() {
Column() {
// 动画目标元素
Column() {
Text('Animated')
}
.width(this.width)
.height(this.height)
.opacity(this.opacity)
.rotate({ angle: this.angle })
.backgroundColor(Color.Blue)
.margin(20)
Button('Animate')
.onClick(() => {
// 执行动画
animateTo({
duration: 1000, // 动画时长(毫秒)
curve: Curve.EaseInOut, // 动画曲线
iterations: 1, // 重复次数
playMode: PlayMode.Normal // 播放模式
}, () => {
// 修改属性值,自动产生动画
this.width = 200;
this.height = 200;
this.opacity = 0.5;
this.angle = 180;
});
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}动画曲线
// 不同的动画曲线
animateTo({
duration: 1000,
curve: Curve.EaseInOut, // 缓入缓出
// curve: Curve.EaseIn, // 缓入
// curve: Curve.EaseOut, // 缓出
// curve: Curve.Linear, // 线性
// curve: Curve.Ease, // 默认缓动
}, () => {
this.width = 200;
});
// 自定义贝塞尔曲线
animateTo({
duration: 1000,
curve: Curve.CubicBezier(0.25, 0.1, 0.25, 1.0)
}, () => {
this.width = 200;
});对比 CSS:类似 transition: width 1s ease-in-out;
2. 转场动画
页面转场
// 页面跳转时使用转场动画
router.pushUrl({
url: 'pages/Detail',
params: { id: 123 }
}, router.RouterMode.Standard, (err) => {
if (err) {
console.error('Navigation failed');
}
});组件转场
@Entry
@Component
struct TransitionExample {
@State show: boolean = true;
build() {
Column() {
if (this.show) {
// 使用 transition 定义转场动画
Text('Fade In/Out')
.transition(TransitionEffect.OPACITY.animation({
duration: 300,
curve: Curve.EaseInOut
}))
}
Button('Toggle')
.onClick(() => {
this.show = !this.show;
})
}
}
}多种转场效果
// 淡入淡出
.transition(TransitionEffect.OPACITY)
// 缩放
.transition(TransitionEffect.SCALE)
// 平移
.transition(TransitionEffect.translate({ x: 100, y: 0 }))
// 旋转
.transition(TransitionEffect.ROTATION)
// 组合效果
.transition(
TransitionEffect.OPACITY
.combine(TransitionEffect.SCALE)
.animation({ duration: 300 })
)3. 手势识别
点击手势
@Entry
@Component
struct GestureExample {
@State message: string = 'Tap me';
build() {
Column() {
Text(this.message)
.fontSize(30)
.gesture(
TapGesture({ count: 1 })
.onAction(() => {
this.message = 'Tapped!';
})
)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}长按手势
.gesture(
LongPressGesture({ duration: 1000 })
.onAction(() => {
this.message = 'Long pressed!';
})
)拖动手势
@Entry
@Component
struct DragExample {
@State offsetX: number = 0;
@State offsetY: number = 0;
build() {
Stack() {
Column() {
Text('Drag Me')
}
.width(100)
.height(100)
.backgroundColor(Color.Blue)
.translate({ x: this.offsetX, y: this.offsetY })
.gesture(
PanGesture({ fingers: 1, direction: PanDirection.All })
.onActionUpdate((event: GestureEvent) => {
this.offsetX += event.offsetX;
this.offsetY += event.offsetY;
})
)
}
.width('100%')
.height('100%')
}
}捏合手势(缩放)
@Entry
@Component
struct PinchExample {
@State scale: number = 1.0;
build() {
Column() {
Image($r('app.media.image'))
.width(200)
.height(200)
.scale({ x: this.scale, y: this.scale })
.gesture(
PinchGesture({ fingers: 2 })
.onActionUpdate((event: GestureEvent) => {
this.scale = event.scale;
})
)
}
}
}4. 组合手势
手势组合
.gesture(
GestureGroup(GestureMode.Parallel, [
TapGesture().onAction(() => {
console.log('Tap');
}),
LongPressGesture().onAction(() => {
console.log('Long press');
})
])
)手势识别优先级
.gesture(
GestureGroup(GestureMode.Exclusive, [
LongPressGesture({ duration: 500 })
.onAction(() => {
console.log('Long press');
}),
TapGesture()
.onAction(() => {
console.log('Tap');
})
])
)5. 自定义动画
关键帧动画
@Entry
@Component
struct KeyframeAnimation {
@State progress: number = 0;
build() {
Column() {
Text('Animation')
.width(100 + this.progress * 100)
.height(100 + this.progress * 100)
.opacity(1.0 - this.progress * 0.5)
}
.onAppear(() => {
// 使用 animateTo 实现关键帧动画
animateTo({
duration: 2000,
curve: Curve.EaseInOut
}, () => {
this.progress = 1.0;
});
})
}
}循环动画
@Entry
@Component
struct LoopAnimation {
@State angle: number = 0;
aboutToAppear() {
this.startRotation();
}
startRotation() {
animateTo({
duration: 2000,
curve: Curve.Linear,
iterations: -1, // 无限循环
playMode: PlayMode.Normal
}, () => {
this.angle = 360;
});
}
build() {
Column() {
Image($r('app.media.icon'))
.width(100)
.height(100)
.rotate({ angle: this.angle })
}
}
}6. 性能优化
使用 willChange 提示
Column() {
Text('Optimized')
}
.willChange(WillChange.Transform) // 提示浏览器优化避免过度动画
// ❌ 不好的做法:同时动画多个属性
animateTo({}, () => {
this.width = 200;
this.height = 200;
this.opacity = 0.5;
this.angle = 180;
this.x = 100;
this.y = 100;
});
// ✅ 好的做法:合并相关属性
animateTo({}, () => {
this.width = 200;
this.height = 200;
});
animateTo({}, () => {
this.opacity = 0.5;
});实践练习
练习 1:实现卡片翻转动画
目标:创建一个可以翻转的卡片
要求:
- 点击卡片时翻转
- 使用 3D 旋转动画
- 显示正面和背面内容
参考代码:
@Entry
@Component
struct FlipCard {
@State flipped: boolean = false;
@State angleY: number = 0;
flip() {
animateTo({
duration: 500,
curve: Curve.EaseInOut
}, () => {
this.angleY = this.flipped ? 0 : 180;
this.flipped = !this.flipped;
});
}
build() {
Stack() {
// 正面
if (!this.flipped) {
Column() {
Text('Front')
.fontSize(30)
}
.width(200)
.height(300)
.backgroundColor(Color.Blue)
.rotate({ x: 0, y: this.angleY })
}
// 背面
if (this.flipped) {
Column() {
Text('Back')
.fontSize(30)
}
.width(200)
.height(300)
.backgroundColor(Color.Red)
.rotate({ x: 0, y: this.angleY + 180 })
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.flip();
})
}
}练习 2:实现下拉刷新动画
目标:实现下拉刷新时的加载动画
要求:
- 下拉时显示加载指示器
- 使用旋转动画
- 刷新完成后停止动画
参考代码:
@Entry
@Component
struct PullToRefresh {
@State refreshing: boolean = false;
@State angle: number = 0;
startRefresh() {
this.refreshing = true;
this.startRotation();
// 模拟刷新
setTimeout(() => {
this.refreshing = false;
}, 2000);
}
startRotation() {
animateTo({
duration: 1000,
curve: Curve.Linear,
iterations: -1
}, () => {
this.angle = 360;
});
}
build() {
Column() {
if (this.refreshing) {
Image($r('app.media.ic_refresh'))
.width(30)
.height(30)
.rotate({ angle: this.angle })
}
List() {
// 列表内容
}
}
}
}练习 3:实现拖拽排序
目标:实现列表项的拖拽排序功能
要求:
- 长按开始拖拽
- 拖拽时显示预览
- 释放时完成排序
参考代码:
@Entry
@Component
struct DragSortList {
@State items: string[] = ['Item 1', 'Item 2', 'Item 3', 'Item 4'];
@State draggingIndex: number = -1;
@State dragOffset: number = 0;
build() {
List() {
ForEach(this.items, (item: string, index: number) => {
ListItem() {
Row() {
Text(item)
}
.width('100%')
.padding(15)
.opacity(this.draggingIndex === index ? 0.5 : 1.0)
.translate({ y: this.draggingIndex === index ? this.dragOffset : 0 })
.gesture(
LongPressGesture({ duration: 300 })
.onAction(() => {
this.draggingIndex = index;
})
)
.gesture(
PanGesture()
.onActionUpdate((event: GestureEvent) => {
if (this.draggingIndex === index) {
this.dragOffset = event.offsetY;
}
})
.onActionEnd(() => {
// 完成排序
this.draggingIndex = -1;
this.dragOffset = 0;
})
)
}
})
}
}
}常见问题
Q1: 如何暂停和恢复动画?
A: 可以使用动画控制器(AnimationController)来控制动画的播放、暂停和恢复。
Q2: 如何实现弹簧动画?
A: 使用自定义的贝塞尔曲线或物理动画库来实现弹簧效果。
Q3: 手势冲突如何解决?
A: 使用 GestureGroup 的 Exclusive 模式,或调整手势的识别顺序。
Q4: 动画性能如何优化?
A:
- 使用
willChange提示 - 避免同时动画过多属性
- 使用 GPU 加速的属性(transform、opacity)
Q5: 如何实现页面切换动画?
A: 使用路由转场动画,或在页面组件上使用 transition 属性。
扩展阅读
下一步
完成动画学习后,建议继续学习:
- 项目实战 - 综合运用所有知识完成项目