难道你的鸿蒙 App 只能“站桩”吗?——用 ✨动画 + 🔁分布式接续,把体验卷到隔壁设备去!

🏆本文收录于「滚雪球学SpringBoot」专栏(全网一个名),手把手带你零基础入门Spring Boot,从入门到就业,助你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8
🎭 前言
说真的😂,我每次打开一个 App,如果它点哪儿都“毫无反应”、页面切换像翻 PPT 一样硬邦邦,我心里都会默默吐槽一句:“你这 UI…是被谁惹生气了吗?”
鸿蒙的 ArkUI 其实给了你一整套“情绪表达系统”——点一下有回弹、列表增删有过渡、页面跳转有转场;更狠的是,你还可以把用户的任务从手机“拎”到平板/PC/TV 上继续玩,用户都不一定意识到发生了迁移🤯。
这篇我就按你给的大纲,狠狠干两章:🎬第 10 章 动画与过渡效果 + 🛰️第 14 章 分布式任务调度。不讲虚的,直接上能跑的思路 + 代码,顺带把那些“坑到我想摔键盘”的细节也拎出来😤。
🧭目录(带你少走弯路😎)
-
🎬 10. 动画与过渡效果
- 🧩 animateTo / Animation API
- 🎈 显式动画 / 隐式动画
- 🪄 组件过渡 transition
- 🤏 手势动画(拖拽、回弹、跟手)
-
🛰️ 14. 分布式任务调度
- 🔁 跨设备任务迁移(应用接续)
- 🧳 ContinueAbility / onContinue 链路
- 🧠 分布式使命(Mission)与“调度”到底在调啥
🎬10. 动画与过渡效果:让 UI “会说话” 😆
先定个基调:动画不是为了炫技,它是 UI 的“标点符号”。
该回弹的时候回弹、该淡入的时候淡入,用户才会觉得“顺”,不然就像看人说话没有停顿一样别扭🙃。
官方把动画能力拆得很清楚:
- 属性动画 animation:组件属性变化时自动插入过渡(隐式)([华为开发者][1])
- 显式动画 animateTo:把一段状态变化包进闭包,系统替你补上过渡(显式)([华为开发者][2])
- 组件内转场 transition:组件插入/删除的过渡动效(尤其适合列表)([华为开发者][3])
- 页面间转场 pageTransition:router 切页时的入场/退场动效([华为开发者][4])
- 帧动画 @ohos.animator:逐帧回调,适合“跟手 + 可暂停 + 实时响应”场景,但性能一般要更小心([华为开发者][5])
下面咱一个个狠狠干👇
🎈10.1 隐式动画:别写太用力,交给 .animation() 😏
适用场景:
- 你只是想让 width/height/opacity/backgroundColor 这些属性变化“顺滑一点”
- 不需要复杂时序、不需要回调链
官方定义:当组件通用属性变化时可以用属性动画实现渐变过渡,常见如宽高、透明度、背景色等([华为开发者][1])。
✅例子:点一下卡片展开/收起(带一点“软乎乎”的过渡 🥯)
@Entry
@Component
struct ExpandCardDemo {
@State expanded: boolean = false
build() {
Column({ space: 12 }) {
Text(this.expanded ? '收起一下😌' : '展开看看👀')
.fontSize(18)
.onClick(() => { this.expanded = !this.expanded })
Column()
.width('100%')
.height(this.expanded ? 220 : 96)
.backgroundColor(this.expanded ? '#FFE7D6' : '#E6F2FF')
.borderRadius(16)
.padding(16)
// 👇 隐式动画:属性变了就自动过渡
.animation({ duration: 240, curve: Curve.EaseOut })
.justifyContent(FlexAlign.Center)
{
Text(this.expanded ? '内容多一点点~🍜' : '内容少一点点~🍙')
}
}
.padding(20)
}
}
🧨隐式动画的“暗坑”提醒(不说你真会踩😭)
- 内容不会跟着宽高渐变“逐帧重排”:很多布局类变化,内容可能直接到终态(这点在显式动画文档里也明确提到类似行为)([华为开发者][2])
- 所以:如果你要“文字跟着盒子变大逐渐显现”,就得考虑
transition/ 分层显示 / 或更精细的动画策略。
✨10.2 显式动画 animateTo:我就想“明确地演一下”😎
官方一句话很关键:animateTo 用于把闭包导致的状态变化插入过渡动效。
通俗翻译:“你把状态怎么改写在闭包里,系统替你把过程演出来。”
✅例子:点赞按钮“噗通一下”❤️(缩放 + 回弹)
@Entry
@Component
struct LikePulseDemo {
@State liked: boolean = false
@State scale: number = 1
private pulse() {
// 🎯 显式动画:先变大,再回到 1
animateTo({ duration: 120, curve: Curve.EaseOut }, () => {
this.scale = 1.18
})
animateTo({ duration: 220, curve: Curve.Spring }, () => {
this.scale = 1
})
}
build() {
Column({ space: 14 }) {
Text(this.liked ? '已心动💘' : '点我试试😼')
.fontSize(18)
Text(this.liked ? '❤️' : '🤍')
.fontSize(64)
.scale({ x: this.scale, y: this.scale })
.onClick(() => {
this.liked = !this.liked
this.pulse()
})
}
.padding(24)
}
}
😏什么时候“别用 animateTo 硬上”?
- 你只是 width/opacity 这类简单过渡:隐式动画更省心
- 你要做“跟手拖拽”:手势更新应该实时更新状态,结束时再
animateTo回弹(下一节就讲😤)
🪄10.3 组件过渡 transition:列表增删别“瞬移”啊喂😵💫
官方定义很直白:transition 主要用于容器子组件插入和删除时显示过渡动效。
也就是说:你做“消息列表”“待办清单”“购物车增删”——不用 transition 就像魔术没烟雾,观众看得一愣一愣的😅。
✅例子:待办列表插入/删除(淡入 + 上移一点点)
@Entry
@Component
struct TodoTransitionDemo {
@State items: string[] = ['写代码🧑💻', '喝水💧', '摸鱼(合理)🐟']
@State idSeed: number = 0
build() {
Column({ space: 10 }) {
Row({ space: 10 }) {
Button('加一条➕')
.onClick(() => {
this.items = [`新任务#${++this.idSeed} 😎`, ...this.items]
})
Button('删第一条🗑️')
.onClick(() => {
if (this.items.length > 0) this.items = this.items.slice(1)
})
}
Column({ space: 8 }) {
ForEach(this.items, (it: string) => {
Row() {
Text(it).fontSize(16)
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('#F6F7FB')
// 👇 插入/删除过渡:你可以按需换成更复杂的组合
.transition({ type: TransitionType.All, opacity: 0.0, translate: { y: -10 } })
})
}
}
.padding(20)
}
}
小吐槽一句😆:列表“啪”一下出现/消失,用户会觉得应用在“抽风”;有过渡,用户会觉得它“懂事”。
🎞️10.4 页面转场 pageTransition:router 切页也要讲礼貌🤝
官方明确:当 router 切换时,你可以在 pageTransition 里自定义入场/退场动效。
(注意:这里说的是 router,不是 Navigation 那套。)
✅例子:从首页 push 到详情页(淡入 + 轻微位移)
import router from '@ohos.router';
@Entry
@Component
struct HomePage {
build() {
Column({ space: 12 }) {
Text('首页🏠').fontSize(22)
Button('去详情页➡️').onClick(() => {
router.pushUrl({ url: 'pages/DetailPage' })
})
}
.padding(24)
.pageTransition({
enter: { type: RouteType.Push, duration: 240, curve: Curve.EaseOut, opacity: 0.0, translate: { x: 30 } },
exit: { type: RouteType.Push, duration: 180, curve: Curve.EaseIn, opacity: 1.0, translate: { x: 0 } }
})
}
}
@Component
struct DetailPage {
build() {
Column({ space: 12 }) {
Text('详情页🧾').fontSize(22)
Button('回去⬅️').onClick(() => router.back())
}
.padding(24)
.pageTransition({
enter: { type: RouteType.Push, duration: 240, curve: Curve.EaseOut, opacity: 0.0, translate: { x: 30 } },
exit: { type: RouteType.Pop, duration: 220, curve: Curve.EaseOut, opacity: 1.0, translate: { x: 0 } }
})
}
}
🤫小建议:转场别太花。用户是来看内容的,不是来看你“炫技灯光秀”的😅。
🤏10.5 手势动画:跟手要“紧”,松手要“弹”!😤
手势动画最常见的正确姿势是:
onActionUpdate:实时更新状态(跟手)onActionEnd:用animateTo做回弹/吸附(结束动画)
✅例子:拖拽卡片,松手自动回弹(很解压🤤)
@Entry
@Component
struct DragSnapDemo {
@State offsetX: number = 0
@State offsetY: number = 0
build() {
Stack() {
Column()
.width(240)
.height(140)
.borderRadius(18)
.backgroundColor('#FFF1D6')
.translate({ x: this.offsetX, y: this.offsetY })
.gesture(
PanGesture()
.onActionUpdate((e: PanGestureEvent) => {
// 🤏 跟手:别加动画,直接改
this.offsetX = e.offsetX
this.offsetY = e.offsetY
})
.onActionEnd(() => {
// 🧲 松手回弹:交给 animateTo
animateTo({ duration: 260, curve: Curve.Spring }, () => {
this.offsetX = 0
this.offsetY = 0
})
})
)
{
Text('拖我~我会回去😼').fontSize(16).padding(14)
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignContent(Alignment.Center)
}
}
🧩10.6 Animation API(帧动画 @ohos.animator):别乱用,但真香😈
如果你想做到:
- 动画过程实时拿到插值(比如做物理、曲线、跟手校正)
- 能暂停、能继续、能反向
那@ohos.animator就很合适。官方也强调它有onFrame逐帧回调,并提醒性能与释放问题。
✅例子:用 Animator 做一个“进度条惯性滑动”(逐帧更新)
import { Animator as animator, AnimatorOptions, AnimatorResult } from '@kit.ArkUI';
@Entry
@Component
struct AnimatorProgressDemo {
@State progress: number = 0
private anim?: AnimatorResult
aboutToAppear() {
const options: AnimatorOptions = {
duration: 900,
easing: "friction",
delay: 0,
fill: "forwards",
direction: "normal",
iterations: 1,
begin: 0.0,
end: 100.0
}
// ⚠️ 注意:Animator 依赖 UI 上下文;要持有 result,避免中途析构:contentReference[oaicite:11]{index=11}
this.anim = animator.create(options)
this.anim.onFrame?.((v: number) => {
this.progress = v
})
}
aboutToDisappear() {
// 😤 别偷懒:释放,避免循环引用/泄漏风险:contentReference[oaicite:12]{index=12}
this.anim?.cancel()
this.anim = undefined
}
build() {
Column({ space: 12 }) {
Text(`进度:${this.progress.toFixed(0)}% 🧪`).fontSize(18)
Row()
.width('100%')
.height(14)
.borderRadius(7)
.backgroundColor('#EEE')
{
Row()
.width(`${this.progress}%`)
.height(14)
.borderRadius(7)
.backgroundColor('#7AD6A7')
}
Button('播放一下▶️').onClick(() => this.anim?.play())
Button('暂停一下⏸️').onClick(() => this.anim?.pause())
Button('反向来点🔄').onClick(() => this.anim?.reverse())
}
.padding(24)
}
}
官方也提到:从较新版本开始更推荐用 UIContext.createAnimator 来明确上下文,避免“上下文不明确”导致动画异常([华为开发者][5])。
你要是遇到“动画不执行/执行异常”,优先怀疑:UIContext 不匹配😤。
🛰️14. 分布式任务调度:把用户“搬家”搬得丝滑🧳✨
这一章我先把“听起来很玄学”的词拆开揉碎:
- 任务迁移 / 应用接续(Continuation):用户在 A 设备干到一半,换到 B 设备接着干。
- Mission(使命/任务栈):可以理解成“系统层面的任务单元”,接续本质就是把这坨任务状态(页面栈+控件状态+你的自定义数据)迁走。
- 调度(Scheduler):别想复杂,它干的事就是:决定什么时候能迁、迁去哪、迁完怎么恢复。
官方指南写得很清楚:应用接续可以迁移任务(包含页面控件状态变量等)到目标设备继续使用,并支持恢复路由与控件状态。
🔁14.1 接续链路全景图:源端保存 ➜ 目标端恢复 🧠
按官方主流程(你可以当成“接力棒传递”🏃♂️):
- 启用能力:
module.json5把 UIAbility 标记为可接续(continuable) - 源端:触发迁移时回调
onContinue(wantParam),你把要迁移的数据塞进去,并返回同意/拒绝/版本不匹配等结果 - 目标端:
onCreate/onNewWant判断是迁移启动(launchReason),从want.parameters取数据,必要时触发页面恢复(例如restoreWindowStage) - 限制:
wantParam传的数据建议控制在 100KB 以下,大数据用分布式数据对象等方案
🧷14.2 module.json5:先把“门打开”🔓
没配
continuable: true,你后面写得再花,系统也会:“不让迁😑”
{
"module": {
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:icon",
"label": "$string:EntryAbility_label",
"continuable": true // ✅ 关键:允许接续
}
]
}
}
🧳14.3 源端 onContinue:你要带什么“行李”过去?🎒
官方说得很明确:onContinue(wantParam) 在源端保存迁移数据,并返回迁移结果(AGREE/REJECT/MISMATCH)。
✅例子:保存编辑中的文档 ID + 光标位置(并做版本兼容拦截😤)
import { UIAbility, AbilityConstant } from '@kit.AbilityKit'
import { promptAction } from '@kit.ArkUI'
export default class EntryAbility extends UIAbility {
onContinue(wantParam: Record<string, Object>) {
const targetVersion = wantParam['version'] as number | undefined
// 😤 版本兼容:不兼容就别硬迁,迁过去崩了更丢人
const minCompatible = 100 // 假装这是你能兼容的最低 versionCode
if (typeof targetVersion === 'number' && targetVersion < minCompatible) {
promptAction.showToast({ message: '目标端版本太低啦😭,先升级再接续~', duration: 2000 })
return AbilityConstant.OnContinueResult.MISMATCH
}
// 🎒 行李打包(注意别用系统保留 key)
wantParam['docId'] = 'DOC_20251224'
wantParam['cursor'] = 128
wantParam['draft'] = '用户正在编辑的一小段文本...' // ⚠️ 别太大,官方建议 wantParam < 100KB:contentReference[oaicite:21]{index=21}
return AbilityConstant.OnContinueResult.AGREE
}
}
🧩14.4 目标端恢复:onCreate / onNewWant 接力开跑🏁
UIAbility 文档里就把 onContinue / onNewWant / onCreate 都列在生命周期回调里了。
官方指南也强调:目标端在 onCreate/onNewWant 恢复数据并触发页面恢复。
✅例子:识别迁移启动 + 读取参数 + 恢复页面栈(必要时)
import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit'
export default class EntryAbility extends UIAbility {
storage: LocalStorage = new LocalStorage()
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
const docId = want.parameters?.['docId'] as string
const cursor = want.parameters?.['cursor'] as number
console.info(`接续恢复✅ docId=${docId}, cursor=${cursor}`)
// 🧠 如果你依赖系统页面栈恢复,按官方建议在同步阶段触发恢复
this.context.restoreWindowStage(this.storage)
}
}
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
const docId = want.parameters?.['docId'] as string
console.info(`热启动接续恢复✅ docId=${docId}`)
this.context.restoreWindowStage(this.storage)
}
}
}
📡14.5 continuationManager:拉起设备选择 + 监听协同状态(“选谁来接班”😼)
@ohos.continuation.continuationManager 提供流转/协同入口管理能力:注册监听、拉起设备选择、更新连接状态等。
这块特别适合你做“应用内按钮触发接续”的产品形态:用户点“继续到平板”,你弹出设备列表让他选。
⚠️文档里也标了不少 deprecated 接口,实际用时建议按你项目 API 版本挑最新的(比如
registerContinuation/startContinuationDeviceManager/updateContinuationState一类)。
✅示意思路:点击按钮 ➜ 拉起设备选择 ➜ 拿到目标设备 networkId
import { continuationManager } from '@kit.AbilityKit'
import { promptAction } from '@kit.ArkUI'
function openDevicePicker(context: Context) {
// 具体参数随版本会有差异,这里强调“流程骨架”👇
continuationManager.on('deviceSelected', (device) => {
promptAction.showToast({ message: `已选择设备:${device?.name ?? '未知设备'} 📱➡️💻`, duration: 2000 })
// 你可以在这里记录 device.networkId,后续结合业务触发迁移
})
// 拉起设备管理/选择界面(不同 API 版本函数名略有差异,以文档为准):contentReference[oaicite:26]{index=26}
continuationManager.startContinuationDeviceManager(context)
}
⚡14.6 continueManager:接续“快速拉起”结果回调(别让用户干等😵)
华为 API 参考里提到 @ohos.app.ability.continueManager:可注册 prepareContinue 回调,用于获取快速拉起结果,减少等待。
✅示例:注册 prepareContinue 监听(体验上“更像瞬移”😈)
import { continueManager } from '@kit.AbilityKit'
function listenQuickStart(context: Context) {
continueManager.on('prepareContinue', context, (err, info) => {
if (err) {
console.error(`快速拉起失败❌ ${JSON.stringify(err)}`)
return
}
console.info(`快速拉起结果✅ ${JSON.stringify(info)}`)
})
}
🧠14.7 “分布式使命”与页面栈:别把路由搞混了😤
这里有个很现实的坑:
有开发者在社区问答里提到:目前仅支持 router 路由的页面栈信息自动恢复,暂不支持 navigation 路由的页面栈自动恢复,如果用 Navigation,需要你自己决定不迁页面栈,改成在 want 里带路由信息后目标端手动跳转。
所以你要做稳定方案,建议你在设计时就想好两条路:
- ✅ router 路由为主:尽量吃系统自动恢复
- 🧩 navigation 路由为主:把“当前页面路径 + 参数”当业务数据迁过去,目标端手动导航
🧱14.8 数据迁移策略:小包塞 wantParam,大包走分布式数据🧳
官方建议很明确:onContinue 里通过 wantParam 传数据控制在 100KB 以下,大数据量用分布式数据对象等方式。
我一般会这么拆(很“人类思维”,不容易翻车😌):
-
🧾 轻量状态(<100KB):
- docId、播放进度、筛选条件、当前 tab、光标位置、临时 UI 状态
- 直接塞
wantParam
-
🗄️ 中/大数据(图片、长文本、列表、离线内容):
- 用分布式 KV、分布式数据对象、文件资产迁移等(官方指南里也把这些列成了专项章节)
wantParam只传 key(例如 recordId / fileKey),目标端再取
😤小提醒:涉及隐私/敏感数据时,别偷懒塞 wantParam。该加密就加密,该做权限校验就做校验。
✅14.9 本章小结:接续做得好,用户只会说“哇塞”🤯
把 14 章真正做顺了,你的应用体验会发生质变:
- 用户从手机换到平板:不是重新打开,是“原地续上”
- 迁移时 UI 不乱、页面栈不丢、数据不炸:用户对你“信任值 +100”😎
- 你还能在应用内做“继续到 XX 设备”的按钮:产品经理看了都得给你发小红花🌸(真的)
🧧福利赠与你🧧
无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」专栏(全网一个名),bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。
最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。
同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G pdf电子书籍、简历模板、技术文章Markdown文档等海量资料。
✨️ Who am I?
我是bug菌(全网一个名),CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云多年度十佳博主/价值贡献奖,掘金多年度人气作者Top40,掘金等各大社区平台签约作者,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 30w+;更多精彩福利点击这里;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试真题、4000G PDF电子书籍、简历模板等海量资料,你想要的我都有,关键是你不来拿。

-End-
- 点赞
- 收藏
- 关注作者
评论(0)