1# 列表项交换案例 2 3### 介绍 4 5本案例通过List组件、组合手势GestureGroup、swipeAction属性以及attributeModifier属性等实现了列表项的交换和删除。 6 7### 效果图预览 8 9 10 11**使用说明**: 12 131. 进入页面,长按列表项,执行拖拽操作,当拖拽长度大于列表项所占高度一半的时候,列表项进行交换。 142. 列表项左滑,显示删除按钮,点击删除按钮,此列表项被删除。 15 16### 下载安装 17 181.模块oh-package.json5文件中引入依赖。 19```typescript 20"dependencies": { 21 "listexchange": "har包地址" 22} 23``` 24 252.ets文件import自定义视图实现列表视图。 26 27```typescript 28import { ListExchange } from 'listexchange'; 29``` 30### 快速使用 31 32本章节主要介绍了如何快速上手自定义视图实现列表切换效果组件。 33 341. 设置列表项元素的类。开发者可以根据自身业务列表项的需求场景自行变更或者拓展属性,但是需要自定义列表项元素视图。 35 36```typescript 37class ListInfo { 38 icon: ResourceStr = ''; 39 name: ResourceStr = ''; 40 41 constructor(icon: ResourceStr = '', name: ResourceStr = '') { 42 this.icon = icon; 43 this.name = name; 44 } 45 } 46``` 47 482. 数据准备。首先构建一个ListInfo类型的数组,然后向其中传入对应的内容数据。 49 50```typescript 51const MEMO_DATA: ListInfo[] = [ 52 new ListInfo($r("app.media.list_exchange_ic_public_cards_filled"), '账户余额'), 53 new ListInfo($r("app.media.list_exchange_ic_public_cards_filled2"), 'xx银行储蓄卡(1234)'), 54 new ListInfo($r("app.media.list_exchange_ic_public_cards_filled3"), 'xx银行储蓄卡(1238)'), 55 new ListInfo($r("app.media.list_exchange_ic_public_cards_filled4"), 'xx银行储蓄卡(1236)')]; 56 57@State appInfoList: ListInfo[] = MEMO_DATA; 58``` 593. 声明管理列表项交换的类ListExchangeCtrl。通过ListExchangeCtrl来实现列表项与列表项之间的位置交换。具体实现请看后续的实现步骤章节。 60 61```typescript 62// 列表项交换类 63@State listExchangeCtrl: ListExchangeCtrl<ListInfo> = new ListExchangeCtrl(); 64``` 654. 自定义列表项组件。开发者可以自定义列表项的UI。 66 67```typescript 68 69 // 列表项数据信息 70 @Builder deductionView(listItemInfo: ListInfo) {...} 71 72``` 735. 构建自定义列表视图。在代码合适的位置使用ListExchange组件并传入对应的参数。 74 75```typescript 76/** 77 * 列表交换视图 78 * appInfoList: 数据源 79 * listExchangeCtrl: 列表项交换类 80 * deductionView: 自定义列表项元素视图 81 */ 82ListExchange({ 83 appInfoList: this.appInfoList, 84 listExchangeCtrl: this.listExchangeCtrl, 85 deductionView: (listItemInfo: Object) => { 86 this.deductionView(listItemInfo as Listinfo) 87 } 88}) 89 90``` 91 92### 属性(接口)说明 93 94ListInfo类属性(开发者可以自行拓展或者更改列表的属性元素) 95 96| 属性 | 类型 | 释义 | 默认值 | 97|:-------------:|:-----------:|:----:|:---:| 98| icon | ResourceStr | 列表图片 | - | 99| name | ResourceStr | 列表名称 | - | 100 101 102ListExchange组件属性 103 104| 属性 | 类型 | 释义 | 默认值 | 105|:----------------:|:----------------:|:----------:|:---:| 106| appInfoList | ListInfo[] | 列表数据源 | - | 107| listExchangeCtrl | ListExchangeCtrl | 列表项元素交换类 | - | 108| deductionView | void | 自定义列表项元素视图 | - | 109 110### 实现思路 111 112首先创建一个数组modifier来添加自定义属性对象,根据组合手势GestureGroup来控制自定义属性的值并通过attributeModifier绑定自定义属性对象来动态加载属性。 113然后通过swipeAction属性绑定删除组件,左滑显示此删除组件,点击实现列表项的删除。 1141. 声明一个数组,添加自定义属性对象,每个自定义属性对象对应一个列表项,源码参考[AttributeModifier.ets](ListExchange/src/main/ets/model/AttributeModifier.ets)和[ListExchangeCtrl.ets](ListExchange/src/main/ets/model/ListExchangeCtrl.ets)。 115```typescript 116 initData(deductionData: Array<T>) { 117 this.deductionData = deductionData; 118 deductionData.forEach(() => { 119 this.modifier.push(new ListItemModifier()); 120 }) 121} 122 /** 123 * 通过实现AttributeModifier接口,自定义属性修改器 124 * 将拖拽排序相关样式封装成属性修改器,可以方便移植 125 */ 126 export class ListItemModifier implements AttributeModifier<ListItemAttribute> { 127 // 阴影 128 public hasShadow: boolean = false; 129 // 缩放 130 public scale: number = 1; 131 // 纵轴偏移量 132 public offsetY: number = 0; 133 // 横轴偏移量 134 public offsetX: number = 0; 135 // 透明度 136 public opacity: number = 1; 137 // 是否被删除 138 139 public static getInstance(): ListItemModifier { 140 if (!ListItemModifier.instance) { 141 ListItemModifier.instance = new ListItemModifier(); 142 } 143 return ListItemModifier.instance; 144 } 145 146 /** 147 * 定义组件普通状态时的样式 148 * @param instance: ListItem属性 149 */ 150 applyNormalAttribute(instance: ListItemAttribute): void { 151 if (this.hasShadow) { 152 instance.shadow({ radius: $r('app.integer.list_exchange_shadow_radius'), color: $r('app.color.box_shadow') }); 153 instance.zIndex(1); 154 instance.opacity(0.5); 155 } else { 156 instance.opacity(this.opacity); 157 } 158 instance.translate({ x: this.offsetX, y: this.offsetY }); 159 instance.scale({ x: this.scale, y: this.scale }); 160 } 161} 162``` 1632. 绑定attributeModifier属性以及组合手势GestureGroup,attributeModifier属性的值为对应的自定义属性对象。源码参考[ListExchangeView.ets](ListExchange/src/main/ets/view/ListExchangeView.ets)。 164```typescript 165// 列表区域 166List() { 167 ForEach(this.appInfoList, (item: Object) => { 168 ListItem() { 169 this.deductionView(item) 170 } 171 .zIndex(this.currentListItem === item ? 2 : 1) // 层级属性 172 .swipeAction({ end: this.defaultDeleteBuilder(item) }) // 用于设置ListItem的划出组件 173 .transition(TransitionEffect.OPACITY) 174 .attributeModifier(this.listExchangeCtrl.getModifier(item)) //动态设置组件的属性方法, 参数为属性修改器 175 .gesture( 176 // 以下组合手势为顺序识别,当长按手势事件未正常触发时,则不会出发拖动手势事件 177 GestureGroup(GestureMode.Sequence, 178 // 长按 179 LongPressGesture() 180 .onAction((event: GestureEvent) => { 181 this.currentListItem = item; 182 this.isLongPress = true; 183 this.listExchangeCtrl.onLongPress(item); 184 }), 185 // 拖动 186 PanGesture() 187 .onActionUpdate((event: GestureEvent) => { 188 this.listExchangeCtrl.onMove(item, event.offsetY); 189 }) 190 .onActionEnd((event: GestureEvent) => { 191 this.listExchangeCtrl.onDrop(item); 192 this.isLongPress = false; 193 }) 194 ).onCancel(() => { 195 if (!this.isLongPress) { 196 return; 197 } 198 this.listExchangeCtrl.onDrop(item); 199 })) 200 }, (item: Object) => JSON.stringify(item)) 201} 202.divider({ strokeWidth: '1px', color: 0xeaf0ef }) 203.scrollBar(BarState.Off) 204.border({ 205 radius: { 206 bottomLeft: $r('app.string.ohos_id_corner_radius_default_l'), 207 bottomRight: $r('app.string.ohos_id_corner_radius_default_l') 208 } 209}) 210.backgroundColor(Color.White) 211.width('100%') 212 213``` 214 2153. 长按列表项,通过LongPressGesture识别长按手势,执行onLongPress函数方法更改此列表项的scale、shadow、zIndex和opacity等属性,并通过animateTo来实现动画效果,源码参考[ListExchangeCtrl.ets](ListExchange/src/main/ets/model/ListExchangeCtrl.ets)。 216 217```typescript 218 onLongPress(item: T) { 219 const index: number = this.deductionData.indexOf(item); 220 this.dragRefOffset = 0; 221 // TODO:知识点:长按当前列表项透明度和放大动画 222 animateTo({ curve: Curve.Friction, duration: ANIMATE_DURATION }, () => { 223 this.state = OperationStatus.PRESSING; 224 this.modifier[index].hasShadow = true; 225 this.modifier[index].scale = 1.04; // 放大比例为1.04 226 }) 227 } 228``` 2294. 交换列表项,通过PanGesture手势的onActionUpdate方法监听拖动的纵轴移动长度,然后执行onMove方法,根据移动长度的大小来判断是否执行列表项交换方法changeItem,源码参考[ListExchangeCtrl.ets](ListExchange/src/main/ets/model/ListExchangeCtrl.ets)。 230```typescript 231 onMove(item: T, offsetY: number) { 232 const index: number = this.deductionData.indexOf(item); 233 this.offsetY = offsetY - this.dragRefOffset; 234 this.modifier[index].offsetY = this.offsetY; 235 const direction: number = this.offsetY > 0 ? 1 : -1; 236 // 触发拖动时,被覆盖子组件缩小与恢复的动画 237 const curveValue: ICurve = curves.initCurve(Curve.Sharp); 238 const value: number = curveValue.interpolate(Math.abs(this.offsetY) / ITEM_HEIGHT); 239 const shrinkScale: number = 1 - value / 10; // 计算缩放比例,value值缩小10倍 240 if (index < this.modifier.length - 1) { // 当拖拽的时候,被交换的对象会缩放 241 this.modifier[index + 1].scale = direction > 0 ? shrinkScale : 1; 242 } 243 if (index > 0) { 244 this.modifier[index - 1].scale = direction > 0 ? 1 : shrinkScale; 245 } 246 // TODO:知识点:处理列表项的切换操作 247 if (Math.abs(this.offsetY) > ITEM_HEIGHT / 2) { 248 animateTo({ curve: Curve.Friction, duration: commonConstants.ANIMATE_DURATION }, () => { 249 this.offsetY -= direction * ITEM_HEIGHT; 250 this.dragRefOffset += direction * ITEM_HEIGHT; 251 this.modifier[index].offsetY = this.offsetY; 252 this.changeItem(index, index + direction); 253 }) 254 } 255 } 256 257 changeItem(index: number, newIndex: number): void { 258 const tmp: Array<T> = this.deductionData.splice(index, 1); 259 this.deductionData.splice(newIndex, 0, tmp[0]); 260 const tmp2: Array<ListItemModifier> = this.modifier.splice(index, 1); 261 this.modifier.splice(newIndex, 0, tmp2[0]); 262 } 263``` 2645. 通过swipeAction属性绑定删除按钮组件,列表项左滑显示删除组件,点击删除按钮,列表项删除。源码参考[ListExchangeCtrl.ets](ListExchange/src/main/ets/model/ListExchangeCtrl.ets)。 265```typescript 266deleteItem(item: T): void { 267 const index = this.deductionData.indexOf(item); 268 this.dragRefOffset = 0; 269 // TODO:知识点:左偏移以及透明度动画 270 animateTo({ 271 curve: Curve.Friction, onFinish: () => { 272 // TODO:知识点:列表项删除动画 273 animateTo({ 274 curve: Curve.Friction, onFinish: () => { 275 this.state = OperationStatus.IDLE; 276 } 277 }, () => { 278 this.modifier.splice(index, 1); 279 this.deductionData.splice(index, 1); 280 }) 281 } 282 }, () => { 283 this.state = OperationStatus.DELETE; 284 this.modifier[index].offsetX = 150; // 列表项左偏移150 285 this.modifier[index].opacity = 0; // 列表项透明度为0 286 }) 287} 288``` 289 290### 工程结构&模块类型 291 292``` 293listexchange // har类型 294|---common 295| |---commonConstants.ets // 常量 296|---model 297| |---AttributeModifier.ets // 属性对象 298| |---ListExchangeCtrl.ets // 列表项交换 299| |---ListInfo.ets // 列表项信息 300| |---MockData.ets // 模拟数据 301|---util 302| |---ListExchange.ets // 自定义列表视图 303| |---Logger.ets // 日志 304|---view 305| |---ListExchangeView.ets // 视图层-应用主页面 306``` 307 308### 参考资料 309 310[List](https://docs.openharmony.cn/pages/v5.0/zh-cn/application-dev/reference/apis-arkui/arkui-ts/ts-container-list.md) 311 312[GestureGroup](https://docs.openharmony.cn/pages/v5.0/zh-cn/application-dev/reference/apis-arkui/arkui-ts/ts-combined-gestures.md) 313 314### 约束与限制 315 3161.本示例仅支持标准系统上运行。 317 3182.本示例已适配API version 12版本SDK。 319 3203.本示例需要使用DevEco Studio 5.0.0 Release及以上版本才可编译运行。 321 322### 下载 323 324如需单独下载本工程,执行如下命令: 325```javascript 326git init 327git config core.sparsecheckout true 328echo /code/UI/listexchange/ > .git/info/sparse-checkout 329git remote add origin https://gitee.com/openharmony/applications_app_samples.git 330git pull origin master 331```