列表排序看起来简单——不就是数组元素换个位置吗但加上动画和交互就有很多细节拖拽时视觉反馈、松手后的位置动画、排序过程中的状态同步。这篇从手动排序到拖拽排序把动画细节全说清楚。数组重排算法最基础的操作——把数组元素从位置A移到位置BprivatemoveItem(from:number,to:number):void{letitem:DataItemthis.items[from];letnewItems:DataItem[][];// 先移除from位置的元素for(leti:number0;ithis.items.length;i){if(i!from){newItems.push(this.items[i]);}}// 在to位置插入letresult:DataItem[][];for(leti:number0;inewItems.length;i){if(ito){result.push(item);}result.push(newItems[i]);}if(tonewItems.length){result.push(item);}this.itemsresult;}关键必须给this.items赋值新数组不能直接splice修改。State数组的变更检测依赖引用变化直接修改原数组不会触发UI刷新。更简洁的方式如果运行时支持letitem:DataItemthis.items.splice(from,1)[0];this.items.splice(to,0,item);this.items[...this.items];// 触发刷新但ArkTS不支持解构和展开运算符所以用for循环构建新数组是最安全的做法。上下移动按钮最简单的排序UI——每个列表项旁边有↑↓按钮Row(){Text(item.title).layoutWeight(1)Button(↑).onClick((){if(index0){this.moveItem(index,index-1);}})Button(↓).onClick((){if(indexthis.items.length-1){this.moveItem(index,index2);// 2因为移除后索引偏移}})}moveItem的to参数要注意移除from元素后目标位置的含义会变。上移一位是index-1下移一位是index2因为当前元素被移除后后面的元素都前移了一位。位置动画排序后列表项位置变化加animation让变化有过渡ListItem(){Row(){// 内容}.width(100%).padding(12).borderRadius(10).backgroundColor(#ffffff).animation({duration:200,curve:Curve.EaseInOut})}animation修饰器加在ListItem的内容容器上。当items数组重排后ForEach根据key重新分配数据组件位置变化animation让位移产生过渡动画。ForEach的key必须稳定——用item.id不要用index。index在排序后会变导致ForEach错误地复用组件动画就会错乱。拖拽项视觉反馈拖拽中的列表项需要视觉区分——背景色变化、阴影加深、轻微放大StatedragIndex:number-1;Row(){// 内容}.backgroundColor(this.dragIndexindex?#e3f2fd:#ffffff).shadow({radius:this.dragIndexindex?8:2,color:#1a000000,offsetY:this.dragIndexindex?2:1}).scale({x:this.dragIndexindex?1.02:1,y:this.dragIndexindex?1.02:1}).animation({duration:150,curve:Curve.EaseOut})三个视觉变化同时发生背景变蓝、阴影加深、轻微放大1.02倍。animation让变化有过渡。dragIndex在拖拽开始时设为当前index拖拽结束时恢复-1。PanGesture拖拽排序用PanGesture实现跟手拖拽Row(){Text(≡)// 拖拽手柄Text(item.title)}.gesture(PanGesture().onActionStart((){this.dragIndexindex;}).onActionUpdate((event:GestureEvent){// 根据event.offsetY计算当前应该在的位置letcurrentY:numberthis.itemPositions[index]event.offsetY;letnewIndex:numberthis.calculateNewIndex(currentY);if(newIndex!indexnewIndex!this.targetIndex){this.targetIndexnewIndex;}}).onActionEnd((){if(this.targetIndex0){this.moveItem(this.dragIndex,this.targetIndex);}this.dragIndex-1;this.targetIndex-1;}))PanGesture的onActionUpdate提供offsetY——手指从起始位置的偏移。根据偏移计算当前应该在哪个位置实时预览排序效果。但PanGesture有个问题ListItem的PanGesture会跟List的滚动冲突。需要用priorityGesture或者用专门的拖拽手柄区域。List内置拖拽APIList组件有onDragStart/onDrop等拖拽回调List(){ForEach(this.items,(item:DataItem,index:number){ListItem(){Row(){Text(item.title)}}.onDragStart((event:DragEvent){this.dragIndexindex;returnthis.dragBuilder(item);}).onDrop((event:DragEvent){lettargetIndex:numberthis.calculateDropIndex(event.y);this.moveItem(this.dragIndex,targetIndex);this.dragIndex-1;})},(item:DataItem)item.id)}onDragStart返回一个Builder作为拖拽预览图。onDrop在松手时触发event.y可以计算放置位置。但onDragStart/onDrop在List中的行为不够稳定——长按触发拖拽的灵敏度、跨ListItem的拖拽位置计算都有坑。实际项目中更常用按钮排序动画过渡而不是真正拖拽。删除动画列表项删除时的动画效果StateremovingIndex:number-1;Row(){Text(item.title)}.scale({x:this.removingIndexindex?0.8:1,y:this.removingIndexindex?0.8:1}).opacity(this.removingIndexindex?0:1).animation({duration:200,curve:Curve.EaseIn})// 触发删除removeItem(index:number):void{this.removingIndexindex;setTimeout((){letnewItems:DataItem[][];for(leti:number0;ithis.items.length;i){if(i!index){newItems.push(this.items[i]);}}this.itemsnewItems;this.removingIndex-1;},200);}删除分两步先动画缩小渐隐200ms后真正移除数据。setTimeout延迟匹配animation时长。插入动画新项目插入时渐显放大StatenewItemIndex:number-1;Row(){Text(item.title)}.scale({x:this.newItemIndexindex?1:0.5,y:this.newItemIndexindex?1:0.5}).opacity(this.newItemIndexindex?1:0).animation({duration:300,curve:Curve.EaseOut})// 插入后insertItem(item:DataItem,at:number):void{this.newItemIndexat;letnewItems:DataItem[][];for(leti:number0;ithis.items.length;i){if(iat){newItems.push(item);}newItems.push(this.items[i]);}if(atthis.items.length){newItems.push(item);}this.itemsnewItems;setTimeout((){this.newItemIndex-1;},300);}初始scale 0.5和opacity 0动画到1和1。300ms后清除标记。这样新插入的项有一个出现的动画。列表动画的性能注意动画越多性能开销越大。几个优化点只对可见项做动画removingIndex/newItemIndex只影响一个项animation时长别太长200ms够用超过300ms用户会觉得卡避免大范围重绘排序时ForEach只更新变化项key要稳定shadow开销大大量列表项加shadow会掉帧拖拽中临时加shadow结束后去掉LazyForEach cachedCount大列表排序必须用LazyForEach否则ForEach全量创建组件交错动画列表初始化时每项依次出现StateitemVisible:boolean[][];aboutToAppear():void{for(leti:number0;ithis.items.length;i){setTimeout((){this.itemVisible[i]true;},i*50);}}Row(){Text(item.title)}.opacity(this.itemVisible[index]?1:0).translate({y:this.itemVisible[index]?0:20}).animation({duration:300,curve:Curve.EaseOut})每项延迟50ms出现形成从上到下的波浪效果。50ms * N项 总延迟。10项500ms完成。踩坑清单问题原因解决排序后UI没更新直接修改数组没触发刷新赋值新数组给this.items动画错乱ForEach用index作key改用item.idPanGesture跟滚动冲突两个手势互相消费用priorityGesture或拖拽手柄删除动画后列表跳动画和数据移除不同步动画完成后再移除数据阴影导致掉帧大量shadow组件只在拖拽项加shadow移动位置计算错误移除元素后索引偏移先移除再计算目标位置交错动画闪烁opacity初始值不对初始化itemVisible为false按钮排序↑↓方向反moveItem的to参数理解错误下移用index2不是index1插入位置不准for循环的插入逻辑在iat时先push新项动画中操作列表动画未完成就改数据加loading标记防重复操作列表动画的原则是少量项做动画大量项保性能。排序只影响两三个项的位置变化用animation就够了。全列表出现动画只在初始化做一次不要反复触发。
HarmonyOS 6.0 拖拽排序与列表动画
列表排序看起来简单——不就是数组元素换个位置吗但加上动画和交互就有很多细节拖拽时视觉反馈、松手后的位置动画、排序过程中的状态同步。这篇从手动排序到拖拽排序把动画细节全说清楚。数组重排算法最基础的操作——把数组元素从位置A移到位置BprivatemoveItem(from:number,to:number):void{letitem:DataItemthis.items[from];letnewItems:DataItem[][];// 先移除from位置的元素for(leti:number0;ithis.items.length;i){if(i!from){newItems.push(this.items[i]);}}// 在to位置插入letresult:DataItem[][];for(leti:number0;inewItems.length;i){if(ito){result.push(item);}result.push(newItems[i]);}if(tonewItems.length){result.push(item);}this.itemsresult;}关键必须给this.items赋值新数组不能直接splice修改。State数组的变更检测依赖引用变化直接修改原数组不会触发UI刷新。更简洁的方式如果运行时支持letitem:DataItemthis.items.splice(from,1)[0];this.items.splice(to,0,item);this.items[...this.items];// 触发刷新但ArkTS不支持解构和展开运算符所以用for循环构建新数组是最安全的做法。上下移动按钮最简单的排序UI——每个列表项旁边有↑↓按钮Row(){Text(item.title).layoutWeight(1)Button(↑).onClick((){if(index0){this.moveItem(index,index-1);}})Button(↓).onClick((){if(indexthis.items.length-1){this.moveItem(index,index2);// 2因为移除后索引偏移}})}moveItem的to参数要注意移除from元素后目标位置的含义会变。上移一位是index-1下移一位是index2因为当前元素被移除后后面的元素都前移了一位。位置动画排序后列表项位置变化加animation让变化有过渡ListItem(){Row(){// 内容}.width(100%).padding(12).borderRadius(10).backgroundColor(#ffffff).animation({duration:200,curve:Curve.EaseInOut})}animation修饰器加在ListItem的内容容器上。当items数组重排后ForEach根据key重新分配数据组件位置变化animation让位移产生过渡动画。ForEach的key必须稳定——用item.id不要用index。index在排序后会变导致ForEach错误地复用组件动画就会错乱。拖拽项视觉反馈拖拽中的列表项需要视觉区分——背景色变化、阴影加深、轻微放大StatedragIndex:number-1;Row(){// 内容}.backgroundColor(this.dragIndexindex?#e3f2fd:#ffffff).shadow({radius:this.dragIndexindex?8:2,color:#1a000000,offsetY:this.dragIndexindex?2:1}).scale({x:this.dragIndexindex?1.02:1,y:this.dragIndexindex?1.02:1}).animation({duration:150,curve:Curve.EaseOut})三个视觉变化同时发生背景变蓝、阴影加深、轻微放大1.02倍。animation让变化有过渡。dragIndex在拖拽开始时设为当前index拖拽结束时恢复-1。PanGesture拖拽排序用PanGesture实现跟手拖拽Row(){Text(≡)// 拖拽手柄Text(item.title)}.gesture(PanGesture().onActionStart((){this.dragIndexindex;}).onActionUpdate((event:GestureEvent){// 根据event.offsetY计算当前应该在的位置letcurrentY:numberthis.itemPositions[index]event.offsetY;letnewIndex:numberthis.calculateNewIndex(currentY);if(newIndex!indexnewIndex!this.targetIndex){this.targetIndexnewIndex;}}).onActionEnd((){if(this.targetIndex0){this.moveItem(this.dragIndex,this.targetIndex);}this.dragIndex-1;this.targetIndex-1;}))PanGesture的onActionUpdate提供offsetY——手指从起始位置的偏移。根据偏移计算当前应该在哪个位置实时预览排序效果。但PanGesture有个问题ListItem的PanGesture会跟List的滚动冲突。需要用priorityGesture或者用专门的拖拽手柄区域。List内置拖拽APIList组件有onDragStart/onDrop等拖拽回调List(){ForEach(this.items,(item:DataItem,index:number){ListItem(){Row(){Text(item.title)}}.onDragStart((event:DragEvent){this.dragIndexindex;returnthis.dragBuilder(item);}).onDrop((event:DragEvent){lettargetIndex:numberthis.calculateDropIndex(event.y);this.moveItem(this.dragIndex,targetIndex);this.dragIndex-1;})},(item:DataItem)item.id)}onDragStart返回一个Builder作为拖拽预览图。onDrop在松手时触发event.y可以计算放置位置。但onDragStart/onDrop在List中的行为不够稳定——长按触发拖拽的灵敏度、跨ListItem的拖拽位置计算都有坑。实际项目中更常用按钮排序动画过渡而不是真正拖拽。删除动画列表项删除时的动画效果StateremovingIndex:number-1;Row(){Text(item.title)}.scale({x:this.removingIndexindex?0.8:1,y:this.removingIndexindex?0.8:1}).opacity(this.removingIndexindex?0:1).animation({duration:200,curve:Curve.EaseIn})// 触发删除removeItem(index:number):void{this.removingIndexindex;setTimeout((){letnewItems:DataItem[][];for(leti:number0;ithis.items.length;i){if(i!index){newItems.push(this.items[i]);}}this.itemsnewItems;this.removingIndex-1;},200);}删除分两步先动画缩小渐隐200ms后真正移除数据。setTimeout延迟匹配animation时长。插入动画新项目插入时渐显放大StatenewItemIndex:number-1;Row(){Text(item.title)}.scale({x:this.newItemIndexindex?1:0.5,y:this.newItemIndexindex?1:0.5}).opacity(this.newItemIndexindex?1:0).animation({duration:300,curve:Curve.EaseOut})// 插入后insertItem(item:DataItem,at:number):void{this.newItemIndexat;letnewItems:DataItem[][];for(leti:number0;ithis.items.length;i){if(iat){newItems.push(item);}newItems.push(this.items[i]);}if(atthis.items.length){newItems.push(item);}this.itemsnewItems;setTimeout((){this.newItemIndex-1;},300);}初始scale 0.5和opacity 0动画到1和1。300ms后清除标记。这样新插入的项有一个出现的动画。列表动画的性能注意动画越多性能开销越大。几个优化点只对可见项做动画removingIndex/newItemIndex只影响一个项animation时长别太长200ms够用超过300ms用户会觉得卡避免大范围重绘排序时ForEach只更新变化项key要稳定shadow开销大大量列表项加shadow会掉帧拖拽中临时加shadow结束后去掉LazyForEach cachedCount大列表排序必须用LazyForEach否则ForEach全量创建组件交错动画列表初始化时每项依次出现StateitemVisible:boolean[][];aboutToAppear():void{for(leti:number0;ithis.items.length;i){setTimeout((){this.itemVisible[i]true;},i*50);}}Row(){Text(item.title)}.opacity(this.itemVisible[index]?1:0).translate({y:this.itemVisible[index]?0:20}).animation({duration:300,curve:Curve.EaseOut})每项延迟50ms出现形成从上到下的波浪效果。50ms * N项 总延迟。10项500ms完成。踩坑清单问题原因解决排序后UI没更新直接修改数组没触发刷新赋值新数组给this.items动画错乱ForEach用index作key改用item.idPanGesture跟滚动冲突两个手势互相消费用priorityGesture或拖拽手柄删除动画后列表跳动画和数据移除不同步动画完成后再移除数据阴影导致掉帧大量shadow组件只在拖拽项加shadow移动位置计算错误移除元素后索引偏移先移除再计算目标位置交错动画闪烁opacity初始值不对初始化itemVisible为false按钮排序↑↓方向反moveItem的to参数理解错误下移用index2不是index1插入位置不准for循环的插入逻辑在iat时先push新项动画中操作列表动画未完成就改数据加loading标记防重复操作列表动画的原则是少量项做动画大量项保性能。排序只影响两三个项的位置变化用animation就够了。全列表出现动画只在初始化做一次不要反复触发。