文章目录前言 完整代码结构预览 第一部分数据模型与状态管理️ 第二部分自定义卡片组件 (Builder)✨ 第三部分动画与路由逻辑 (animateTo router) 第四部分页面主体构建 (build) 第五部分视觉设计与色彩搭配 页面展示 总结与实战建议前言在上一期的音乐播放器实战中我们掌握了复杂的页面布局。今天我们将挑战一个更高级的交互效果——共享元素转场动画。这个效果在现代 App 中非常常见当你点击列表中的某个卡片时它会以一个流畅的动画“变形”并过渡到下一个页面的对应元素上为用户带来无缝、连贯的视觉体验。这个项目虽然代码量不大但含金量极高涵盖了以下核心知识点页面路由传参使用router.pushUrl在页面间传递复杂数据。显式动画利用animateTo实现点击后的退出动画。状态驱动 UI通过State变量精确控制单个组件的动画状态。动态样式绑定根据状态动态改变组件的scale缩放和opacity透明度。下面我们就对这段实现丝滑动画的代码进行一次深度解析。 完整代码结构预览首先让我们从整体上把握代码结构。它定义了一个Index入口组件核心是列表的渲染、点击事件的处理以及CardItem的自定义构建。importrouterfromohos.routerinterfaceCardData{id:stringtitle:stringsubtitle:stringcolor:stringimage:string}EntryComponentstruct Index{// 1. 数据与状态定义StatecardList:CardData[][...]StateexitCardId:stringStateexitCardScale:number1StateexitCardOpacity:number1// 2. 页面主体构建build(){...}// 3. 动画与路由逻辑privatestartExitAnimation(card:CardData):void{...}// 4. 自定义卡片组件BuilderCardItem(card:CardData){...}} 第一部分数据模型与状态管理代码的开头定义了一个CardData接口和几个关键的State变量它们是驱动整个动画的核心。interfaceCardData{id:stringtitle:stringsubtitle:stringcolor:stringimage:string}StatecardList:CardData[][{id:1,title:探索之旅,subtitle:开启一段奇妙的冒险,color:#667EEA,image:...},// ... 其他卡片数据]StateexitCardId:stringStateexitCardScale:number1StateexitCardOpacity:number1CardData接口定义了卡片的数据结构包括唯一的id、标题、副标题、背景色和图片链接。cardList一个CardData类型的数组作为列表的数据源。这里的图片链接指向一个可以根据文本描述生成图片的 API非常巧妙。exitCardId这是动画的“开关”。它存储了当前正在执行退出动画的卡片的id。通过判断其他卡片的id是否与它相等来决定是否应用动画效果。exitCardScale/exitCardOpacity这两个变量分别控制动画过程中的缩放比例和透明度。它们的值会在动画执行时被改变从而驱动 UI 变化。️ 第二部分自定义卡片组件 (Builder)Builder装饰器将卡片的 UI 封装成一个独立的函数CardItem使build方法更加简洁。BuilderCardItem(card:CardData){Stack({alignContent:Alignment.Center}){// 背景卡片Column().width(100%).height(180).backgroundColor(card.color).borderRadius(20)// 内容区域Row({space:16}){Image(card.image).width(120).height(140).borderRadius(16).objectFit(ImageFit.Cover)// 动态绑定缩放和透明度.scale({x:this.exitCardIdcard.id?this.exitCardScale:1,y:this.exitCardIdcard.id?this.exitCardScale:1}).opacity(this.exitCardIdcard.id?this.exitCardOpacity:1)Column({space:8}){Text(card.title).fontSize(24).fontWeight(FontWeight.Bold).fontColor(#FFFFFF)// 标题也绑定动画.scale({x:this.exitCardIdcard.id?this.exitCardScale:1,y:this.exitCardIdcard.id?this.exitCardScale:1}).opacity(this.exitCardIdcard.id?this.exitCardOpacity:1)// ... 其他文本}.layoutWeight(1)}.width(100).padding(20)}.width(100%).height(180).shadow({radius:15,color:card.color40,offsetX:0,offsetY:8})// 整个卡片容器也绑定动画实现整体缩小.scale({x:this.exitCardIdcard.id?this.exitCardScale:1,y:this.exitCardIdcard.id?this.exitCardScale:1}).opacity(this.exitCardIdcard.id?this.exitCardOpacity:1)}布局结构使用Stack作为容器内部是一个带圆角和阴影的Column作为卡片背景再上面是一个Row来水平排列图片和文字信息。动态样式绑定这是实现动画的关键.scale(...)和.opacity(...)修饰符的值不再是固定的而是通过三元运算符this.exitCardId card.id ? ... : ...进行动态判断。只有当卡片的id与exitCardId匹配时才会应用exitCardScale和exitCardOpacity的值否则保持默认值缩放为1透明度为1。这确保了动画只作用于被点击的那一个卡片。阴影效果.shadow()修饰符为卡片添加了柔和的阴影card.color 40的写法巧妙地根据卡片背景色生成了半透明的阴影颜色提升了视觉质感。✨ 第三部分动画与路由逻辑 (animateTo router)这是整个交互的灵魂所在。startExitAnimation函数处理了从点击到页面跳转的全过程。privatestartExitAnimation(card:CardData):void{this.exitCardIdcard.id// 1. 标记当前要动画的卡片// 2. 执行显式动画animateTo({duration:300,curve:Curve.Friction,onFinish:(){// 3. 动画结束后进行页面跳转router.pushUrl({url:pages/Detail,params:{cardData:JSON.stringify(card)}})// 4. 跳转后重置状态为下一次动画做准备setTimeout((){this.exitCardIdthis.exitCardScale1this.exitCardOpacity1},100)}},(){// 5. 动画内容改变状态变量的值this.exitCardScale0.8this.exitCardOpacity0})}this.exitCardId card.id首先记录下被点击卡片的id。这个操作会触发 UI 的重新渲染CardItem中的动态样式绑定会检测到这个变化。animateTo这是鸿蒙提供的显式动画 API。它会将第二个回调函数中状态变量的变化以动画的形式呈现出来。duration: 300动画持续时间为 300 毫秒。curve: Curve.Friction使用摩擦曲线让动画有自然的减速感。在动画回调中我们将exitCardScale改为0.8exitCardOpacity改为0。这会驱动被选中的卡片在 300ms 内缩小到 80% 并淡出。onFinish动画结束后的回调。router.pushUrl使用路由跳转到详情页。关键点在于params我们将整个card对象通过JSON.stringify序列化成字符串后传递过去。详情页可以通过router.getParams()获取并反序列化从而实现数据的传递。setTimeout在跳转后使用一个短暂的延时来重置所有状态变量。这是为了在用户从详情页返回时列表能恢复到初始状态避免动画错乱。 第四部分页面主体构建 (build)build函数负责搭建页面的基本骨架结构非常清晰。build(){Column(){Text(共享元素动效).fontSize(32).fontWeight(FontWeight.Bold).fontColor(#FFFFFF).margin({top:60,bottom:30})List({space:20}){ForEach(this.cardList,(card:CardData){ListItem(){this.CardItem(card)}.onClick((){this.startExitAnimation(card)// 绑定点击事件})})}.width(100%).padding({left:20,right:20}).layoutWeight(1)}.width(100%).height(100%).backgroundColor(#1A1A2E)}List和ForEach使用List组件来高效地渲染长列表并通过ForEach循环cardList数据源为每一项生成一个CardItem。.onClick为每个ListItem绑定点击事件触发我们之前定义的startExitAnimation函数并将当前卡片的数据card作为参数传入。 第五部分视觉设计与色彩搭配整个页面采用了深邃的暗色背景与卡片鲜艳的色彩形成强烈对比突出了内容本身。视觉区域颜色代码设计意图整体背景#1A1A2E深蓝色背景营造沉静、专注的氛围让卡片更突出。卡片背景#667EEA,#F093FB等每张卡片使用不同的渐变色或亮色充满活力区分不同主题。文字颜色#FFFFFF白色文字在深色背景上保证了极佳的可读性。阴影颜色card.color 40动态生成与卡片同色系的半透明阴影细节感满满。 页面展示列表页展示四个不同主题的卡片布局清晰色彩鲜明。点击动画点击任一卡片该卡片会平滑地缩小并淡出随后跳转到详情页。 总结与实战建议通过这个共享元素转场动画的实战我们掌握了以下 ArkTS 高阶技能显式动画animateTo学会了如何使用animateTo包裹状态变化轻松实现复杂的属性动画。精细化状态控制通过一个id状态变量精确地控制列表中单个元素的样式和行为这是处理列表交互的常用技巧。页面间通信掌握了使用router.pushUrl的params参数进行页面间数据传递的方法特别是对象的序列化与反序列化。动态样式绑定深刻理解了如何将组件的样式属性如scale,opacity与状态变量绑定实现数据驱动 UI 的强大能力。