HarmonyOS APP<<古今职鉴定>>开源教程第8篇:动画与交互:让界面活起来
本篇学习属性动画、转场动画、手势识别,实现祝福卡片弹出动画
·
本篇学习属性动画、转场动画、手势识别,实现祝福卡片弹出动画
图:古今职鉴开源教程封面。本篇围绕「动画与交互:让界面"活"起来」展开。
学习目标
完成本篇后,你将能够:
- ✅ 使用 animateTo 实现属性动画
- ✅ 使用 transition 实现转场动画
- ✅ 掌握常用手势识别
- ✅ 实现祝福卡片弹出动画效果
预计学习时间
约 90 分钟
---
实战一:animateTo 属性动画
第一步:创建 lesson08 目录和文件
在 products/jiaocheng/src/main/ets/ 下创建 lesson08 文件夹,新建 Lesson08Page.ets:
// 文件路径:products/jiaocheng/src/main/ets/lesson08/Lesson08Page.ets
@Entry
@Component
struct Lesson08Page {
@State boxScale: number = 1;
build() {
Column({ space: 20 }) {
Text('animateTo 动画演示')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#1e293b')
// 动画目标元素
Column()
.width(100)
.height(100)
.backgroundColor('#c41e3a')
.borderRadius(12)
.scale({ x: this.boxScale, y: this.boxScale })
Button('点击放大/缩小')
.onClick(() => {
// animateTo 包裹状态变化
animateTo({
duration: 300, // 动画时长(毫秒)
curve: Curve.EaseOut // 动画曲线
}, () => {
// 状态变化
this.boxScale = this.boxScale === 1 ? 1.5 : 1;
});
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#f8f6f5')
}
}
第二步:理解 animateTo 的工作原理
animateTo({
duration: 300, // 动画时长
curve: Curve.EaseOut, // 动画曲线
delay: 0, // 延迟开始
onFinish: () => {} // 完成回调
}, () => {
// 在这里修改状态
// 状态变化会以动画形式呈现
this.boxScale = 1.5;
});
关键点:
animateTo不是让某个属性动画,而是让状态变化产生动画- 状态变化写在第二个回调函数里
- 所有依赖该状态的 UI 都会动画更新
第三步:运行查看效果
hvigorw assembleHap --no-daemon
预期效果:
- 点击按钮,红色方块平滑放大到 1.5 倍
- 再次点击,平滑缩小回原始大小
---
实战二:多属性同时动画
第一步:添加多个动画状态
修改组件,添加更多状态:
@Entry
@Component
struct Lesson08Page {
@State boxScale: number = 1;
@State boxOpacity: number = 1;
@State boxRotate: number = 0;
build() {
Column({ space: 20 }) {
Text('多属性动画')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#1e293b')
Column()
.width(100)
.height(100)
.backgroundColor('#c41e3a')
.borderRadius(12)
.scale({ x: this.boxScale, y: this.boxScale })
.opacity(this.boxOpacity)
.rotate({ angle: this.boxRotate })
Button('组合动画')
.onClick(() => {
animateTo({
duration: 500,
curve: Curve.Spring // 弹簧效果
}, () => {
// 同时改变多个属性
if (this.boxScale === 1) {
this.boxScale = 1.3;
this.boxOpacity = 0.7;
this.boxRotate = 15;
} else {
this.boxScale = 1;
this.boxOpacity = 1;
this.boxRotate = 0;
}
});
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#f8f6f5')
}
}
第二步:理解动画曲线
| 曲线 | 效果 | 适用场景 |
|---|---|---|
Curve.Linear |
匀速 | 进度条 |
Curve.EaseIn |
开始慢,结束快 | 退出动画 |
Curve.EaseOut |
开始快,结束慢 | 进入动画 |
Curve.EaseInOut |
两端慢,中间快 | 通用过渡 |
Curve.Spring |
弹簧效果 | 弹出、强调 |
第三步:运行验证
hvigorw assembleHap --no-daemon
预期效果:
- 点击按钮,方块同时放大、变透明、旋转
- 动画有弹簧回弹效果
---
实战三:转场动画 transition
第一步:理解 transition 的作用
transition 用于组件出现/消失时的动画,配合 if 条件渲染使用。
第二步:实现显示/隐藏动画
@Entry
@Component
struct Lesson08Page {
@State isVisible: boolean = false;
build() {
Column({ space: 20 }) {
Text('转场动画演示')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#1e293b')
Button(this.isVisible ? '隐藏卡片' : '显示卡片')
.onClick(() => {
// 必须用 animateTo 包裹
animateTo({ duration: 300 }, () => {
this.isVisible = !this.isVisible;
});
})
if (this.isVisible) {
Column() {
Text('我是卡片')
.fontSize(18)
.fontColor(Color.White)
}
.width(200)
.height(120)
.backgroundColor('#c41e3a')
.borderRadius(12)
.justifyContent(FlexAlign.Center)
// 转场动画
.transition(
TransitionEffect.OPACITY
.combine(TransitionEffect.scale({ x: 0.8, y: 0.8 }))
)
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#f8f6f5')
}
}
第三步:理解 TransitionEffect
// 单一效果
TransitionEffect.OPACITY // 透明度
TransitionEffect.scale({ x: 0, y: 0 }) // 缩放
TransitionEffect.translate({ y: 100 }) // 平移
TransitionEffect.rotate({ angle: 90 }) // 旋转
// 组合效果
TransitionEffect.OPACITY
.combine(TransitionEffect.scale({ x: 0.5, y: 0.5 }))
.combine(TransitionEffect.translate({ y: 50 }))
第四步:运行验证
hvigorw assembleHap --no-daemon
预期效果:
- 点击按钮,卡片淡入并放大出现
- 再次点击,卡片淡出并缩小消失
---
实战四:手势识别
第一步:点击手势 TapGesture
@Entry
@Component
struct Lesson08Page {
@State tapCount: number = 0;
build() {
Column({ space: 20 }) {
Text('手势识别演示')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#1e293b')
Text(`点击次数: ${this.tapCount}`)
.fontSize(16)
.fontColor('#64748b')
// 单击
Column() {
Text('单击我')
.fontSize(16)
.fontColor(Color.White)
}
.width(120)
.height(60)
.backgroundColor('#c41e3a')
.borderRadius(8)
.justifyContent(FlexAlign.Center)
.gesture(
TapGesture({ count: 1 })
.onAction(() => {
this.tapCount++;
})
)
// 双击
Column() {
Text('双击我')
.fontSize(16)
.fontColor(Color.White)
}
.width(120)
.height(60)
.backgroundColor('#4169e1')
.borderRadius(8)
.justifyContent(FlexAlign.Center)
.gesture(
TapGesture({ count: 2 })
.onAction(() => {
this.tapCount += 10;
})
)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#f8f6f5')
}
}
第二步:长按手势 LongPressGesture
Column() {
Text('长按我')
.fontSize(16)
.fontColor(Color.White)
}
.width(120)
.height(60)
.backgroundColor('#228b22')
.borderRadius(8)
.justifyContent(FlexAlign.Center)
.gesture(
LongPressGesture({ duration: 500 }) // 500毫秒触发
.onAction(() => {
console.log('长按触发');
})
)
第三步:拖动手势 PanGesture
@State offsetX: number = 0;
@State offsetY: number = 0;
Column() {
Text('拖动我')
.fontSize(16)
.fontColor(Color.White)
}
.width(100)
.height(100)
.backgroundColor('#c41e3a')
.borderRadius(12)
.justifyContent(FlexAlign.Center)
.translate({ x: this.offsetX, y: this.offsetY })
.gesture(
PanGesture()
.onActionUpdate((event: GestureEvent) => {
this.offsetX = event.offsetX;
this.offsetY = event.offsetY;
})
.onActionEnd(() => {
// 松手回到原位
animateTo({ duration: 300 }, () => {
this.offsetX = 0;
this.offsetY = 0;
});
})
)
第四步:运行验证
hvigorw assembleHap --no-daemon
---
实战五:综合实战 - 祝福卡片弹出动画
第一步:分析动画需求
实现效果:
- 点击按钮,背景遮罩渐显
- 卡片从底部弹出,带缩放和透明度动画
- 点击遮罩或关闭按钮,卡片收起
第二步:定义动画状态
@Entry
@Component
struct Lesson08Page {
// 控制卡片显示
@State isCardVisible: boolean = false;
// 卡片动画属性
@State cardScale: number = 0.8;
@State cardOpacity: number = 0;
@State cardOffsetY: number = 100;
// 遮罩透明度
@State maskOpacity: number = 0;
// ... build 方法
}
第三步:实现页面结构
build() {
Stack() {
// 主页面
Column() {
Text('祝福卡片动画')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#1e293b')
.margin({ top: 100 })
Blank()
Button('抽取祝福')
.width('80%')
.height(50)
.backgroundColor('#c41e3a')
.margin({ bottom: 50 })
.onClick(() => {
this.showCard();
})
}
.width('100%')
.height('100%')
.backgroundColor('#f8f6f5')
// 遮罩层
if (this.isCardVisible) {
Column()
.width('100%')
.height('100%')
.backgroundColor('#000000')
.opacity(this.maskOpacity)
.onClick(() => {
this.hideCard();
})
}
// 祝福卡片
if (this.isCardVisible) {
this.BlessingCard()
}
}
.width('100%')
.height('100%')
}
第四步:实现卡片 Builder
@Builder
BlessingCard() {
Column() {
// 关闭按钮
Image($r('app.media.ic_close'))
.width(24)
.height(24)
.fillColor('#64748b')
.position({ x: '85%', y: 16 })
.onClick(() => {
this.hideCard();
})
// 卡片内容
Column() {
Text('🎊')
.fontSize(48)
Text('新年快乐')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#c41e3a')
.margin({ top: 20 })
Text('龙马精神')
.fontSize(20)
.fontColor('#1e293b')
.margin({ top: 8 })
Text('愿你在新的一年里')
.fontSize(14)
.fontColor('#64748b')
.margin({ top: 24 })
Text('事业有成,阖家幸福')
.fontSize(14)
.fontColor('#64748b')
.margin({ top: 4 })
}
.margin({ top: 40 })
}
.width(280)
.height(380)
.backgroundColor(Color.White)
.borderRadius(20)
.shadow({
radius: 20,
color: 'rgba(0, 0, 0, 0.2)',
offsetY: 10
})
.scale({ x: this.cardScale, y: this.cardScale })
.opacity(this.cardOpacity)
.translate({ y: this.cardOffsetY })
}
第五步:实现显示/隐藏方法
// 显示卡片
showCard() {
this.isCardVisible = true;
animateTo({
duration: 400,
curve: Curve.Spring
}, () => {
this.cardScale = 1;
this.cardOpacity = 1;
this.cardOffsetY = 0;
this.maskOpacity = 0.5;
});
}
// 隐藏卡片
hideCard() {
animateTo({
duration: 250,
curve: Curve.EaseIn,
onFinish: () => {
this.isCardVisible = false;
// 重置状态
this.cardScale = 0.8;
this.cardOpacity = 0;
this.cardOffsetY = 100;
this.maskOpacity = 0;
}
}, () => {
this.cardScale = 0.8;
this.cardOpacity = 0;
this.cardOffsetY = 100;
this.maskOpacity = 0;
});
}
第六步:运行验证
hvigorw assembleHap --no-daemon
预期效果:
- 点击按钮,遮罩渐显,卡片弹出
- 卡片有缩放、透明度、位移动画
- 点击遮罩或关闭按钮,卡片收起
- 动画流畅自然
---
完整代码
// 文件路径:products/jiaocheng/src/main/ets/lesson08/Lesson08Page.ets
@Entry
@Component
struct Lesson08Page {
@State isCardVisible: boolean = false;
@State cardScale: number = 0.8;
@State cardOpacity: number = 0;
@State cardOffsetY: number = 100;
@State maskOpacity: number = 0;
build() {
Stack() {
Column() {
Text('祝福卡片动画')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#1e293b')
.margin({ top: 100 })
Blank()
Button('抽取祝福')
.width('80%')
.height(50)
.backgroundColor('#c41e3a')
.margin({ bottom: 50 })
.onClick(() => {
this.showCard();
})
}
.width('100%')
.height('100%')
.backgroundColor('#f8f6f5')
if (this.isCardVisible) {
Column()
.width('100%')
.height('100%')
.backgroundColor('#000000')
.opacity(this.maskOpacity)
.onClick(() => {
this.hideCard();
})
}
if (this.isCardVisible) {
this.BlessingCard()
}
}
.width('100%')
.height('100%')
}
@Builder
BlessingCard() {
Column() {
Image($r('app.media.ic_close'))
.width(24)
.height(24)
.fillColor('#64748b')
.position({ x: '85%', y: 16 })
.onClick(() => {
this.hideCard();
})
Column() {
Text('🎊')
.fontSize(48)
Text('新年快乐')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#c41e3a')
.margin({ top: 20 })
Text('龙马精神')
.fontSize(20)
.fontColor('#1e293b')
.margin({ top: 8 })
Text('愿你在新的一年里')
.fontSize(14)
.fontColor('#64748b')
.margin({ top: 24 })
Text('事业有成,阖家幸福')
.fontSize(14)
.fontColor('#64748b')
.margin({ top: 4 })
}
.margin({ top: 40 })
}
.width(280)
.height(380)
.backgroundColor(Color.White)
.borderRadius(20)
.shadow({ radius: 20, color: 'rgba(0, 0, 0, 0.2)', offsetY: 10 })
.scale({ x: this.cardScale, y: this.cardScale })
.opacity(this.cardOpacity)
.translate({ y: this.cardOffsetY })
}
showCard() {
this.isCardVisible = true;
animateTo({ duration: 400, curve: Curve.Spring }, () => {
this.cardScale = 1;
this.cardOpacity = 1;
this.cardOffsetY = 0;
this.maskOpacity = 0.5;
});
}
hideCard() {
animateTo({
duration: 250,
curve: Curve.EaseIn,
onFinish: () => {
this.isCardVisible = false;
this.cardScale = 0.8;
this.cardOpacity = 0;
this.cardOffsetY = 100;
this.maskOpacity = 0;
}
}, () => {
this.cardScale = 0.8;
this.cardOpacity = 0;
this.cardOffsetY = 100;
this.maskOpacity = 0;
});
}
}
@Builder
export function Lesson08PageBuilder() {
Lesson08Page()
}
---
本课小结
核心知识点
| 知识点 | 说明 |
|---|---|
| animateTo | 显式动画,包裹状态变化 |
| Curve | 动画曲线:Linear/EaseOut/Spring 等 |
| transition | 转场动画,组件出现/消失时触发 |
| TransitionEffect | 转场效果:OPACITY/scale/translate |
| TapGesture | 点击手势 |
| LongPressGesture | 长按手势 |
| PanGesture | 拖动手势 |
动画曲线选择
| 场景 | 推荐曲线 |
|---|---|
| 弹出效果 | Curve.Spring |
| 收起效果 | Curve.EaseIn |
| 通用过渡 | Curve.EaseOut |
| 匀速动画 | Curve.Linear |
---
课后练习
练习1:添加卡片旋转动画
在卡片弹出时添加轻微旋转:
@State cardRotate: number = -5;
// 显示时
this.cardRotate = 0;
// 应用
.rotate({ angle: this.cardRotate })
练习2:实现点赞动画
点击按钮时,心形图标放大后缩小:
animateTo({ duration: 150 }, () => {
this.heartScale = 1.3;
});
setTimeout(() => {
animateTo({ duration: 150 }, () => {
this.heartScale = 1;
});
}, 150);
---
下一课预告
第9课我们将学习组件状态管理,包括:
- @State 组件内部状态
- @Prop 父传子单向数据流
- @Link 父子双向绑定
- @Provide/@Consume 跨层级传递
项目开源地址
更多推荐


所有评论(0)