HarmonyOS 6实战18:列表动画优化与keyframeAnimateTo高级应用

发布时间:2026/7/16 2:14:48

HarmonyOS 6实战18:列表动画优化与keyframeAnimateTo高级应用 哈喽大家好我是你们的老朋友小齐哥哥。最近在开发一个电商类的HarmonyOS应用时遇到了一个让人头疼的问题当用户添加商品到购物车时新添加的商品会瞬间蹦到列表首位没有任何过渡动画用户体验感极差。更糟糕的是在某些低端设备上这种瞬间渲染甚至会导致页面卡顿。这不就是个简单的列表更新吗我最初不以为意。我按照常规思路使用splice方法在列表指定位置插入新数据然后通过ForEach重新渲染。代码逻辑看起来完全正确但在真机上测试时新条目的出现总是那么突兀——没有渐入效果没有高度展开动画就像变魔术一样瞬间出现。更让人困惑的是这个问题在开发阶段并不明显但在用户实际使用中频繁出现。用户反馈说添加商品时感觉页面闪了一下很不自然。这让我意识到流畅的动画过渡对于用户体验至关重要。今天我将彻底复盘并分享这次列表动画优化的完整解决之旅。这不仅仅是一个动画效果问题更是一次对HarmonyOS渲染机制和动画系统的深度探索。你将掌握从问题定位、根因分析到高级优化的完整方案从此告别列表渲染的生硬感。一、问题现象那个生硬的列表更新我的应用场景很常见一个电商购物车页面用户添加商品时新商品会插入到列表的指定位置比如首位并应该有一个平滑的过渡动画。我写下了看似标准的列表更新代码// 问题代码直接更新数据没有动画过渡 Entry Component struct ShoppingCartDemo { State cartItems: number[] [1, 2, 3]; // 商品ID列表 State newItemId: number 4; build() { Column() { // 商品列表 List() { ForEach(this.cartItems, (item: number) { ListItem() { CartItemComponent({ itemId: item, itemName: 商品 ${item}, price: 99.9 }) } .margin({ bottom: 10 }) }, (item: number) item.toString()) } .width(100%) .layoutWeight(1) // 添加按钮 Button(添加新商品) .width(80%) .height(50) .margin(20) .onClick(() { // 在列表首位插入新商品 this.cartItems.splice(0, 0, this.newItemId); this.newItemId; }) } .width(100%) .height(100%) } } // 购物车商品组件 Component struct CartItemComponent { Prop itemId: number; Prop itemName: string; Prop price: number; build() { Row() { // 商品图片 Image($r(app.media.product_placeholder)) .width(80) .height(80) .borderRadius(8) Column({ space: 8 }) { // 商品名称 Text(this.itemName) .fontSize(16) .fontColor(#333333) .fontWeight(FontWeight.Medium) // 价格 Text(¥${this.price.toFixed(2)}) .fontSize(14) .fontColor(#FF6B00) } .layoutWeight(1) .margin({ left: 12 }) // 操作按钮 Button(删除) .width(60) .height(30) .fontSize(12) } .padding(12) .backgroundColor(#FFFFFF) .borderRadius(12) .shadow({ radius: 4, color: #00000010, offsetX: 0, offsetY: 2 }) } }实际现象点击添加新商品按钮后新商品瞬间出现在列表顶部没有任何过渡效果。在快速连续添加时用户甚至看不清新商品是从哪里来的体验非常生硬。性能问题在低端设备上这种瞬间渲染有时会导致页面短暂卡顿约100-200ms滚动位置跳动触摸响应延迟二、背景知识HarmonyOS的动画系统与渲染机制要理解这个动画问题必须清楚HarmonyOS中组件渲染和动画执行的基本原理。2.1 组件生命周期与onAppear在HarmonyOS中每个组件都有完整的生命周期其中onAppear是一个关键的回调生命周期方法触发时机典型用途aboutToAppear组件即将出现时初始化数据、订阅事件onAppear组件挂载显示后​启动动画、加载数据onDisappear组件即将消失时停止动画、清理资源aboutToDisappear组件即将销毁时取消订阅、释放资源关键洞察onAppear在组件已经渲染到屏幕后触发此时组件的布局、样式都已经确定这是启动入场动画的最佳时机2.2 keyframeAnimateTo关键帧动画引擎keyframeAnimateTo是HarmonyOS提供的高级动画API支持复杂的关键帧动画// 基本语法 getUIContext().keyframeAnimateTo( options: AnimateParam, // 动画参数 keyframes: Keyframe[] // 关键帧数组 ) // 关键帧结构 interface Keyframe { duration: number; // 持续时间毫秒 curve: Curve; // 缓动曲线 event: () void; // 到达该关键帧时执行的回调 }核心优势分段控制可以定义多个关键帧每段有不同的持续时间和缓动曲线精准同步多个属性动画可以精确同步性能优化底层使用原生动画引擎性能优于JS实现的动画2.3 列表渲染的特殊性列表List组件在HarmonyOS中有特殊的渲染优化复用机制列表项ListItem会被复用减少创建开销异步渲染列表更新可能触发异步重新布局视口优化只渲染可见区域内的列表项问题根源当新数据插入列表时新创建的ListItem组件会立即以最终状态渲染跳过了动画过渡阶段。三、问题定位为什么动画不生效通过分析代码执行流程和HarmonyOS的渲染机制我逐步定位到了问题的根源。3.1 代码执行流程分析// 问题代码的执行流程 1. 用户点击按钮 → 触发onClick回调 2. cartItems.splice(0, 0, newItemId) → 更新数据 3. State检测到数据变化 → 触发组件重新构建 4. List组件重新渲染所有ListItem 5. 新ListItem的CartItemComponent被创建 6. CartItemComponent直接以最终状态渲染到屏幕 7. 用户看到新商品瞬间出现关键问题新组件创建后直接以最终状态渲染没有经历从初始状态到最终状态的过渡动画。3.2 动画缺失的根本原因状态初始化时机不当// 问题在build方法中直接设置最终状态 Component struct CartItemComponent { State heightValue: number 80; // 直接设置为最终高度 State opacityValue: number 1.0; // 直接设置为最终透明度 build() { Column() .height(this.heightValue) .opacity(this.opacityValue) } }动画启动时机错过组件在build阶段就已经确定了最终样式onAppear触发时组件已经以最终状态显示此时再启动动画用户看到的是从最终状态到最终状态的无变化动画列表渲染优化冲突HarmonyOS的列表复用机制可能跳过新组件的完整生命周期性能优化导致动画执行时机被压缩或跳过四、解决方案正确的动画实现流程基于以上分析正确的解决方案是初始状态 延迟动画。在组件创建时设置初始状态在onAppear中启动过渡动画。4.1 基础解决方案Component struct AnimatedCartItem { Prop itemId: number; Prop itemName: string; Prop price: number; // 关键初始状态设置为动画起点 State heightValue: number 0; // 初始高度为0 State opacityValue: number 0.0; // 初始透明度为0 State scaleValue: number 0.8; // 初始缩放为0.8 build() { Row() { // 商品图片 Image($r(app.media.product_placeholder)) .width(80) .height(80) .borderRadius(8) .opacity(this.opacityValue) .scale({ x: this.scaleValue, y: this.scaleValue }) Column({ space: 8 }) { Text(this.itemName) .fontSize(16) .fontColor(#333333) .fontWeight(FontWeight.Medium) .opacity(this.opacityValue) Text(¥${this.price.toFixed(2)}) .fontSize(14) .fontColor(#FF6B00) .opacity(this.opacityValue) } .layoutWeight(1) .margin({ left: 12 }) .opacity(this.opacityValue) Button(删除) .width(60) .height(30) .fontSize(12) .opacity(this.opacityValue) } .padding(12) .backgroundColor(#FFFFFF) .borderRadius(12) .shadow({ radius: 4, color: #00000010, offsetX: 0, offsetY: 2 }) .height(this.heightValue) .opacity(this.opacityValue) .scale({ x: this.scaleValue, y: this.scaleValue }) // 关键在onAppear中启动动画 .onAppear(() { this.startEntranceAnimation(); }) } // 入场动画方法 private startEntranceAnimation(): void { const uiContext this.getUIContext(); if (!uiContext) return; // 使用keyframeAnimateTo实现复杂动画序列 uiContext.keyframeAnimateTo( { iterations: 1 }, // 只执行一次 [ // 第一段高度展开600ms { duration: 600, curve: Curve.EaseOut, event: () { this.heightValue 80; // 展开到最终高度 } }, // 第二段淡入效果400ms { duration: 400, curve: Curve.EaseInOut, event: () { this.opacityValue 1.0; // 淡入到完全不透明 } }, // 第三段轻微弹跳效果200ms { duration: 200, curve: Curve.Spring, event: () { this.scaleValue 1.0; // 缩放到正常大小 } } ] ); } }4.2 优化后的列表组件Entry Component struct OptimizedShoppingCart { State cartItems: CartItem[] [ { id: 1, name: 商品1, price: 99.9, count: 1 }, { id: 2, name: 商品2, price: 149.9, count: 2 }, { id: 3, name: 商品3, price: 79.9, count: 1 } ]; State newItemIndex: number 4; State isAdding: boolean false; // 添加新商品带动画标记 async addNewItem(): Promisevoid { if (this.isAdding) return; this.isAdding true; // 创建新商品 const newItem: CartItem { id: this.newItemIndex, name: 新品 ${this.newItemIndex}, price: Math.floor(Math.random() * 200) 50, count: 1 }; // 在列表首位插入 this.cartItems.splice(0, 0, newItem); this.newItemIndex; // 等待动画完成 await new Promise(resolve setTimeout(resolve, 1200)); this.isAdding false; } // 删除商品带动画 async removeItem(index: number): Promisevoid { const item this.cartItems[index]; // 先执行消失动画 await this.animateItemRemoval(index); // 然后从数据中移除 this.cartItems.splice(index, 1); } // 商品移除动画 private async animateItemRemoval(index: number): Promisevoid { // 在实际项目中这里需要通过某种方式通知特定组件执行消失动画 // 可以通过Link或事件机制实现 return new Promise(resolve { setTimeout(resolve, 500); // 模拟动画时间 }); } build() { Column({ space: 0 }) { // 顶部标题栏 this.buildHeader() // 商品列表区域 this.buildProductList() // 底部操作栏 this.buildFooter() } .width(100%) .height(100%) .backgroundColor(#F5F5F5) } Builder buildHeader() { Row({ space: 10 }) { Text(购物车) .fontSize(20) .fontColor(#333333) .fontWeight(FontWeight.Bold) Blank() Text(共 ${this.cartItems.length} 件商品) .fontSize(14) .fontColor(#666666) } .padding({ left: 20, right: 20, top: 15, bottom: 15 }) .backgroundColor(#FFFFFF) .width(100%) } Builder buildProductList() { Scroll() { Column({ space: 10 }) { ForEach(this.cartItems, (item: CartItem, index: number) { // 使用动画优化的商品组件 AnimatedCartItem({ itemId: item.id, itemName: item.name, price: item.price, count: item.count, onRemove: () { this.removeItem(index); }, onCountChange: (newCount: number) { this.cartItems[index].count newCount; } }) .margin({ top: index 0 ? 10 : 0, bottom: 10 }) }, (item: CartItem) item.id.toString()) } .padding(20) .width(100%) } .layoutWeight(1) } Builder buildFooter() { Column({ space: 15 }) { // 总计金额 Row({ space: 10 }) { Text(合计) .fontSize(16) .fontColor(#333333) Text(¥${this.calculateTotal().toFixed(2)}) .fontSize(20) .fontColor(#FF6B00) .fontWeight(FontWeight.Bold) } .width(100%) .justifyContent(FlexAlign.Center) // 操作按钮 Row({ space: 20 }) { Button(继续购物) .width(120) .height(45) .fontSize(14) .backgroundColor(#F0F0F0) .fontColor(#333333) Button(this.isAdding ? 添加中... : 添加新商品) .width(150) .height(45) .fontSize(16) .backgroundColor(#007DFF) .fontColor(Color.White) .enabled(!this.isAdding) .onClick(() { this.addNewItem(); }) } .width(100%) .justifyContent(FlexAlign.Center) } .padding(20) .backgroundColor(#FFFFFF) .width(100%) .border({ width: 1, color: #E5E5E5 }) } // 计算总金额 private calculateTotal(): number { return this.cartItems.reduce((sum, item) { return sum (item.price * item.count); }, 0); } }五、进阶技巧高级动画效果与性能优化在实际项目中仅仅实现基础动画是不够的还需要考虑性能和用户体验。5.1 交错动画Stagger Animation当多个新项目同时插入时交错动画可以避免视觉混乱Component struct StaggerAnimatedList { State items: number[] [1, 2, 3]; State animatingIndices: Setnumber new Set(); build() { List() { ForEach(this.items, (item: number, index: number) { ListItem() { StaggerItem({ item: item, index: index, delay: index * 100, // 每个项目延迟100ms isAnimating: this.animatingIndices.has(index) }) } }) } } } Component struct StaggerItem { Prop item: number; Prop index: number; Prop delay: number; Prop isAnimating: boolean; State opacityValue: number 0; State translateY: number 20; build() { Column() { Text(项目 ${this.item}) .fontSize(16) .fontColor(#333333) } .padding(20) .backgroundColor(#FFFFFF) .borderRadius(8) .opacity(this.opacityValue) .translate({ y: this.translateY }) .onAppear(() { if (this.isAnimating) { this.startStaggerAnimation(); } }) } private startStaggerAnimation(): void { // 延迟启动动画 setTimeout(() { const uiContext this.getUIContext(); uiContext?.keyframeAnimateTo( { iterations: 1 }, [ { duration: 300, curve: Curve.EaseOut, event: () { this.opacityValue 1; this.translateY 0; } } ] ); }, this.delay); } }5.2 动画性能优化使用硬件加速.onAppear(() { // 启用GPU加速 this.getUIContext()?.setRenderOptions({ renderMode: RenderMode.HARDWARE // 使用硬件渲染 }); this.startAnimation(); })避免布局抖动// 不好动画过程中频繁触发布局计算 State widthValue: number 0; // 好使用transform代替尺寸变化 State scaleValue: number 0; .scale({ x: this.scaleValue, y: this.scaleValue })动画帧率控制private startOptimizedAnimation(): void { const uiContext this.getUIContext(); uiContext?.keyframeAnimateTo( { iterations: 1, delay: 0, fill: FillMode.FORWARDS, direction: PlayMode.NORMAL, // 性能优化选项 renderOptions: { preferredFrameRateRange: { min: 30, // 最低30fps max: 60 // 最高60fps } } }, [...] ); }5.3 复杂动画序列Component struct AdvancedAnimationDemo { State phase1Complete: boolean false; State phase2Complete: boolean false; State phase3Complete: boolean false; State heightValue: number 0; State opacityValue: number 0; State borderRadiusValue: number 0; State shadowValue: number 0; State colorValue: Color Color.Gray; build() { Column() { Text(高级动画演示) .fontSize(18) .fontColor(#333333) .margin({ bottom: 20 }) // 动画目标 Column() { Text(动画内容) .fontSize(16) .fontColor(Color.White) } .width(200) .height(this.heightValue) .backgroundColor(this.colorValue) .opacity(this.opacityValue) .borderRadius(this.borderRadiusValue) .shadow({ radius: this.shadowValue, color: #00000030, offsetX: 0, offsetY: 4 }) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) // 控制按钮 Button(开始复杂动画) .margin({ top: 30 }) .onClick(() { this.startComplexAnimation(); }) } .padding(30) .width(100%) .height(100%) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) } private async startComplexAnimation(): Promisevoid { const uiContext this.getUIContext(); if (!uiContext) return; // 第一阶段高度展开 圆角 await new Promisevoid((resolve) { uiContext.keyframeAnimateTo( { iterations: 1 }, [ { duration: 400, curve: Curve.EaseOut, event: () { this.heightValue 120; this.borderRadiusValue 20; } } ] ); setTimeout(() { this.phase1Complete true; resolve(); }, 400); }); // 第二阶段淡入 颜色变化 await new Promisevoid((resolve) { uiContext.keyframeAnimateTo( { iterations: 1 }, [ { duration: 300, curve: Curve.EaseInOut, event: () { this.opacityValue 1; this.colorValue Color.Blue; } } ] ); setTimeout(() { this.phase2Complete true; resolve(); }, 300); }); // 第三阶段阴影效果 轻微缩放 await new Promisevoid((resolve) { uiContext.keyframeAnimateTo( { iterations: 1 }, [ { duration: 200, curve: Curve.Spring, event: () { this.shadowValue 12; } } ] ); setTimeout(() { this.phase3Complete true; resolve(); }, 200); }); // 完成回调 hilog.info(0x0000, AnimationDemo, 复杂动画序列完成); } }六、完整实战示例电商购物车动画优化下面是一个集成了所有最佳实践的完整电商购物车示例import { CartItem, AnimationConfig } from ./model/DataModel; Entry Component struct ECommerceCart { State cartItems: CartItem[] this.initCartItems(); State isProcessing: boolean false; State animationConfig: AnimationConfig { enableStagger: true, staggerDelay: 80, animationDuration: 600, useHardwareAcceleration: true }; private initCartItems(): CartItem[] { return [ { id: 1001, name: 智能手机, price: 2999, count: 1, image: $r(app.media.phone) }, { id: 1002, name: 无线耳机, price: 499, count: 2, image: $r(app.media.earphone) }, { id: 1003, name: 智能手表, price: 1299, count: 1, image: $r(app.media.watch) } ]; } // 添加推荐商品 async addRecommendedItem(itemId: number): Promisevoid { if (this.isProcessing) return; this.isProcessing true; // 模拟获取推荐商品数据 const recommendedItem: CartItem await this.fetchRecommendedItem(itemId); // 执行添加动画 await this.animateItemAddition(recommendedItem); // 更新数据 this.cartItems.splice(0, 0, recommendedItem); this.isProcessing false; } // 批量添加商品 async addMultipleItems(itemIds: number[]): Promisevoid { if (this.isProcessing) return; this.isProcessing true; const newItems: CartItem[] []; const animationPromises: Promisevoid[] []; // 批量获取商品数据 for (const itemId of itemIds) { const item await this.fetchRecommendedItem(itemId); newItems.push(item); } // 批量执行动画支持交错动画 for (let i 0; i newItems.length; i) { const delay this.animationConfig.enableStagger ? i * this.animationConfig.staggerDelay : 0; animationPromises.push( this.animateItemAddition(newItems[i], delay) ); } // 等待所有动画完成 await Promise.all(animationPromises); // 批量更新数据 this.cartItems [...newItems, ...this.cartItems]; this.isProcessing false; } // 商品添加动画 private async animateItemAddition(item: CartItem, delay: number 0): Promisevoid { return new Promise((resolve) { setTimeout(() { // 在实际实现中这里会触发具体的组件动画 // 可以通过事件总线或状态管理通知特定组件 hilog.info(0x0000, CartAnimation, 开始添加动画: ${item.name}, 延迟: ${delay}ms); // 模拟动画时间 setTimeout(resolve, this.animationConfig.animationDuration); }, delay); }); } // 模拟获取推荐商品 private async fetchRecommendedItem(itemId: number): PromiseCartItem { return new Promise((resolve) { setTimeout(() { const items [ { id: 2001, name: 笔记本电脑, price: 6999, count: 1, image: $r(app.media.laptop) }, { id: 2002, name: 平板电脑, price: 3999, count: 1, image: $r(app.media.tablet) }, { id: 2003, name: 蓝牙音箱, price: 399, count: 1, image: $r(app.media.speaker) }, { id: 2004, name: 移动电源, price: 199, count: 1, image: $r(app.media.powerbank) } ]; const item items.find(i i.id itemId) || items[0]; resolve({ ...item, id: Date.now() }); // 确保ID唯一 }, 300); }); } // 计算总价 private calculateTotalPrice(): number { return this.cartItems.reduce((total, item) { return total (item.price * item.count); }, 0); } // 计算节省金额假设有折扣 private calculateSavedAmount(): number { const total this.calculateTotalPrice(); return total 1000 ? total * 0.1 : 0; // 满1000减10% } build() { Column({ space: 0 }) { // 应用栏 this.buildAppBar() // 购物车内容 this.buildCartContent() // 推荐商品 this.buildRecommendations() // 结算栏 this.buildCheckoutBar() } .width(100%) .height(100%) .backgroundColor(#F8F8F8) } Builder buildAppBar() { Row({ space: 10 }) { // 返回按钮 Button() .width(40) .height(40) .backgroundColor(Color.Transparent) .icon($r(app.media.ic_back)) Text(购物车) .fontSize(20) .fontColor(#333333) .fontWeight(FontWeight.Bold) .layoutWeight(1) // 编辑按钮 Button(编辑) .width(60) .height(32) .fontSize(14) .backgroundColor(#F0F0F0) .fontColor(#666666) } .padding({ left: 16, right: 16, top: 12, bottom: 12 }) .backgroundColor(#FFFFFF) .width(100%) } Builder buildCartContent() { Column({ space: 0 }) { // 购物车标题 Row({ space: 10 }) { Text(购物车商品) .fontSize(18) .fontColor(#333333) .fontWeight(FontWeight.Medium) Text((${this.cartItems.length})) .fontSize(14) .fontColor(#666666) } .padding({ left: 20, top: 20, bottom: 15 }) .width(100%) // 商品列表 Scroll() { Column({ space: 12 }) { ForEach(this.cartItems, (item: CartItem, index: number) { EnhancedCartItem({ item: item, index: index, animationConfig: this.animationConfig, onCountChange: (newCount: number) { this.cartItems[index].count newCount; }, onRemove: async () { await this.animateItemRemoval(index); this.cartItems.splice(index, 1); } }) }, (item: CartItem) item.id.toString()) } .padding({ left: 20, right: 20, bottom: 20 }) .width(100%) } .layoutWeight(1) } .backgroundColor(#FFFFFF) .margin({ top: 8 }) .width(100%) } Builder buildRecommendations() { Column({ space: 15 }) { // 推荐标题 Row({ space: 10 }) { Text(猜你喜欢) .fontSize(18) .fontColor(#333333) .fontWeight(FontWeight.Medium) Blank() Text(换一批) .fontSize(14) .fontColor(#007DFF) } .padding({ left: 20, top: 25, right: 20 }) .width(100%) // 推荐商品网格 Grid() { ForEach([2001, 2002, 2003, 2004], (itemId: number) { GridItem() { RecommendedItem({ itemId: itemId, onAdd: () { this.addRecommendedItem(itemId); } }) } }) } .columnsTemplate(1fr 1fr) .rowsTemplate(1fr 1fr) .columnsGap(12) .rowsGap(12) .padding({ left: 20, right: 20, bottom: 25 }) .width(100%) } .backgroundColor(#FFFFFF) .margin({ top: 8 }) .width(100%) } Builder buildCheckoutBar() { Column({ space: 15 }) { // 价格信息 Column({ space: 8 }) { Row({ space: 10 }) { Text(商品金额) .fontSize(14) .fontColor(#666666) .layoutWeight(1) Text(¥${this.calculateTotalPrice().toFixed(2)}) .fontSize(14) .fontColor(#333333) } Row({ space: 10 }) { Text(立减优惠) .fontSize(14) .fontColor(#666666) .layoutWeight(1) Text(-¥${this.calculateSavedAmount().toFixed(2)}) .fontSize(14) .fontColor(#FF6B00) } Divider() .strokeWidth(1) .color(#E5E5E5) .margin({ top: 5, bottom: 5 }) Row({ space: 10 }) { Text(实付金额) .fontSize(16) .fontColor(#333333) .fontWeight(FontWeight.Medium) .layoutWeight(1) Text(¥${(this.calculateTotalPrice() - this.calculateSavedAmount()).toFixed(2)}) .fontSize(20) .fontColor(#FF6B00) .fontWeight(FontWeight.Bold) } } .padding({ left: 20, right: 20, top: 20 }) .width(100%) // 结算按钮 Button(this.isProcessing ? 处理中... : 去结算(${this.cartItems.length})) .width(90%) .height(50) .fontSize(18) .fontWeight(FontWeight.Medium) .backgroundColor(#007DFF) .fontColor(Color.White) .enabled(!this.isProcessing this.cartItems.length 0) .onClick(() { // 结算逻辑 hilog.info(0x0000, ECommerceCart, 开始结算流程); }) .margin({ bottom: 30 }) } .backgroundColor(#FFFFFF) .margin({ top: 8 }) .width(100%) } // 商品移除动画 private async animateItemRemoval(index: number): Promisevoid { return new Promise((resolve) { hilog.info(0x0000, CartAnimation, 开始移除动画: 索引 ${index}); // 在实际实现中这里会触发具体的组件消失动画 // 动画完成后resolve setTimeout(resolve, 500); }); } } // 增强型购物车商品组件 Component struct EnhancedCartItem { Prop item: CartItem; Prop index: number; Prop animationConfig: AnimationConfig; Prop onCountChange: (count: number) void; Prop onRemove: () Promisevoid; State isRemoving: boolean false; State localCount: number 1; // 动画状态 State entranceAnimation: AnimationState { opacity: 0, scale: 0.9, translateY: 20, rotation: 0 }; aboutToAppear(): void { // 初始化本地数量 this.localCount this.item.count; } build() { // 使用Stack实现多层动画 Stack() { // 背景层用于删除动画 if (this.isRemoving) { this.buildRemovalOverlay() } // 内容层 this.buildContentLayer() } .opacity(this.entranceAnimation.opacity) .scale({ x: this.entranceAnimation.scale, y: this.entranceAnimation.scale }) .translate({ y: this.entranceAnimation.translateY }) .rotate({ angle: this.entranceAnimation.rotation }) // 入场动画 .onAppear(() { this.startEntranceAnimation(); }) } Builder buildContentLayer() { Row({ space: 15 }) { // 选择框 Checkbox() .width(24) .height(24) .selectedColor(#007DFF) // 商品图片 Image(this.item.image) .width(100) .height(100) .borderRadius(8) .objectFit(ImageFit.Cover) // 商品信息 Column({ space: 8 }) { // 商品名称 Text(this.item.name) .fontSize(16) .fontColor(#333333) .fontWeight(FontWeight.Medium) .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) .width(100%) // 商品规格如果有 Text(黑色 256GB) .fontSize(12) .fontColor(#999999) // 价格和操作 Row({ space: 10 }) { Text(¥${this.item.price.toFixed(2)}) .fontSize(18) .fontColor(#FF6B00) .fontWeight(FontWeight.Bold) Blank() // 数量选择器 this.buildQuantitySelector() } .width(100%) .margin({ top: 10 }) } .layoutWeight(1) } .padding(15) .backgroundColor(#FFFFFF) .borderRadius(12) .shadow({ radius: 6, color: #00000010, offsetX: 0, offsetY: 2 }) .width(100%) } Builder buildQuantitySelector() { Row({ space: 0 }) { // 减少按钮 Button(-) .width(36) .height(36) .fontSize(16) .backgroundColor(#F5F5F5) .fontColor(#333333) .enabled(this.localCount 1) .onClick(() { this.updateCount(this.localCount - 1); }) // 数量显示 Text(this.localCount.toString()) .width(40) .height(36) .fontSize(16) .fontColor(#333333) .textAlign(TextAlign.Center) .backgroundColor(#FFFFFF) .border({ width: 1, color: #E5E5E5 }) // 增加按钮 Button() .width(36) .height(36) .fontSize(16) .backgroundColor(#F5F5F5) .fontColor(#333333) .onClick(() { this.updateCount(this.localCount 1); }) } .borderRadius(6) .overflow(Hidden) } Builder buildRemovalOverlay() { Column() { Text(删除中...) .fontSize(14) .fontColor(Color.White) } .width(100%) .height(100%) .backgroundColor(#FF3B30) .opacity(0.9) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) } private startEntranceAnimation(): void { const uiContext this.getUIContext(); if (!uiContext) return; // 计算延迟用于交错动画 const delay this.animationConfig.enableStagger ? this.index * this.animationConfig.staggerDelay : 0; setTimeout(() { uiContext.keyframeAnimateTo( { iterations: 1, renderOptions: this.animationConfig.useHardwareAcceleration ? { renderMode: RenderMode.HARDWARE } : undefined }, [ // 第一阶段上浮 淡入 { duration: this.animationConfig.animationDuration * 0.4, curve: Curve.EaseOut, event: () { this.entranceAnimation.translateY 0; this.entranceAnimation.opacity 0.7; } }, // 第二阶段完全淡入 轻微放大 { duration: this.animationConfig.animationDuration * 0.3, curve: Curve.EaseInOut, event: () { this.entranceAnimation.opacity 1; this.entranceAnimation.scale 1.02; } }, // 第三阶段恢复原始大小 轻微弹性 { duration: this.animationConfig.animationDuration * 0.3, curve: Curve.Spring, event: () { this.entranceAnimation.scale 1; } } ] ); }, delay); } private async updateCount(newCount: number): Promisevoid { this.localCount newCount; // 执行数量变化动画 await this.animateCountChange(); // 通知父组件 this.onCountChange(newCount); } private async animateCountChange(): Promisevoid { const uiContext this.getUIContext(); if (!uiContext) return; // 简单的脉冲动画 return new Promise((resolve) { uiContext.keyframeAnimateTo( { iterations: 1 }, [ { duration: 150, curve: Curve.EaseOut, event: () { // 可以在这里添加缩放效果 } } ] ); setTimeout(resolve, 150); }); } // 执行删除动画 private async executeRemoval(): Promisevoid { this.isRemoving true; const uiContext this.getUIContext(); if (uiContext) { // 删除动画缩小 淡出 左滑 await new Promisevoid((resolve) { uiContext.keyframeAnimateTo( { iterations: 1 }, [ { duration: 300, curve: Curve.EaseIn, event: () { this.entranceAnimation.scale 0.8; this.entranceAnimation.opacity 0.5; this.entranceAnimation.translateY -10; } }, { duration: 200, curve: Curve.EaseOut, event: () { this.entranceAnimation.scale 0; this.entranceAnimation.opacity 0; this.entranceAnimation.translateY -30; } } ] ); setTimeout(resolve, 500); }); } // 调用删除回调 await this.onRemove(); } }七、总结与最佳实践通过这次列表动画优化的实践我总结了以下HarmonyOS动画开发的最佳实践7.1 核心原则初始状态原则动画组件必须在构建时设置动画起点状态延迟执行原则在onAppear中启动动画确保组件已挂载性能优先原则优先使用transform动画避免布局重计算7.2 技术要点场景推荐方案注意事项列表项入场​keyframeAnimateTo 交错延迟控制并发动画数量避免性能问题列表项出场​逆向动画 Promise等待动画完成后再移除数据|

相关新闻