🎯 案例集合List:ListItem拖拽(交换位置,过渡动画)(性能篇)
🌍 案例集合List
🚪 最近开启了学员班级点我欢迎加入(学习相关知识可获得定制礼盒)
🏷️ 效果图

📖 参考
🧩 拆解
- 原生(onMove)Repeat (Api 19+特性)(推荐)
javascript
/**
* 模拟数据
*/
const LIST_DATA: string[] = [
"购物", "体育", "服装", "军事", "电商", "娱乐", "科技", "艺术", "新闻", "电商", "农村",
"旅游", "教育", "健康", "美食", "房产", "汽车", "财经", "文化", "音乐", "电影", "书籍",
"宠物", "家居", "美妆", "母婴", "职场", "环保", "历史", "游戏"
]
@ComponentV2
export struct listDragAndDrop {
@Local listData: string[] = LIST_DATA
@Builder
listItemBuilder(obj: RepeatItem<string>) {
Text(obj.item)
.width('100%')
.height(110)
.fontSize(20)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.borderRadius(20)
.backgroundColor('#803c6ad0')
}
build() {
Column() {
List() {
Repeat<string>(this.listData)
.each((obj: RepeatItem<string>) => {
ListItem() {
this.listItemBuilder(obj)
}
.margin({
left: 10,
right: 10,
top: 5,
bottom: 5
}) // 不添加这个属性长按放大的时候组件会被遮盖
})
.virtualScroll({ totalCount: this.listData.length })
.onMove((from: number, to: number) => {
const exchangeItem = this.listData.splice(from, 1)
this.listData.splice(to, 0, exchangeItem[0])
})
}
.width('100%')
.height('100%')
.scrollBar(BarState.Off) // 关闭导航条
.edgeEffect(EdgeEffect.None) // 关闭边缘动效
}
.width('100%')
.height('100%')
.padding({ left: 15, right: 15 })
}
}
- 原生(onMove)拖拽(LazyForEach + @Reusable)
javascript
/**
* 懒加载
*/
export class BasicDataSource<T> implements IDataSource {
private listeners: DataChangeListener[] = []
public totalCount(): number {
return 0
}
public getData(index: number): T | undefined {
return undefined
}
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
console.info('add listener')
this.listeners.push(listener)
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener)
if (pos >= 0) {
console.info('remove listener')
this.listeners.splice(pos, 1)
}
}
notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded()
})
}
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index)
})
}
notifyDataChange(index: number): void {
this.listeners.forEach(listener => {
listener.onDataChange(index)
})
}
notifyDataDelete(index: number): void {
this.listeners.forEach(listener => {
listener.onDataDelete(index)
})
}
notifyDataMove(from: number, to: number): void {
this.listeners.forEach(listener => {
listener.onDataMove(from, to)
})
}
}
/**
* 扩展懒加载
*/
export class CommonLazyModel<T> extends BasicDataSource<T> {
dataArray: Array<T> = new Array()
/**
* 获取数据长度
* @returns
*/
public totalCount(): number {
return this.dataArray.length
}
/**
* 获取某项数据
* @param index
* @returns
*/
public getData(index: number): T {
return this.dataArray[index]
}
/**
* 添加所有数据
* @param data
*/
public setAllData(data: Array<T>) {
this.dataArray = [...data]
this.notifyDataReload()
}
/**
* 交换数据
* 无需调用DataChangeListener接口通知数据源变化
* @param from
* @param to
*/
public exchange(from: number, to: number) {
const exchange = this.dataArray.splice(from, 1)
this.dataArray.splice(to, 0, exchange[0])
}
/**
* 获取数据
* @returns
*/
public getAllData() {
return this.dataArray
}
/**
* 插入数据
* @param index
* @param data
*/
public addData(index: number, data: T): void {
this.dataArray.splice(index, 0, data)
this.notifyDataAdd(index)
}
/**
* 追加数据
* @param data
*/
public pushData(data: T): void {
this.dataArray.push(data)
this.notifyDataAdd(this.dataArray.length - 1)
}
}
/**
* 模拟数据
*/
const LIST_DATA: string[] = [
"购物", "体育", "服装", "军事", "电商", "娱乐", "科技", "艺术", "新闻", "电商", "农村",
"旅游", "教育", "健康", "美食", "房产", "汽车", "财经", "文化", "音乐", "电影", "书籍",
"宠物", "家居", "美妆", "母婴", "职场", "环保", "历史", "游戏"
]
/**
* TODO: 拖拽不支持使用Repeat,所以只能使用懒加载 + 组件复用 || All in 官方的 scrollerComponents 长列表方案库
* TODO: LazyForEach + @Reusable 方式 列表数据过小不触发回收/复用
*/
@Component
export struct listDragAndDrop {
private listData: CommonLazyModel<string> = new CommonLazyModel()
aboutToAppear(): void {
for (let i = 0; i < LIST_DATA.length; i++) {
this.listData.pushData(LIST_DATA[i] + "_" + i.toString())
}
}
build() {
Column() {
List({ space: 10 }) {
LazyForEach(this.listData, (item: string) => {
ListItem() {
ListItemCase({ name: item })
}
}, (item: string) => item)
.onMove((from: number, to: number) => this.listData.exchange(from, to))
}
.width('100%')
.height('100%')
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.None)
}
.width('100%')
.height('100%')
.padding({ left: 15, right: 15 })
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
@Reusable
@Component
export struct ListItemCase {
@State name: string = ''
aboutToReuse(params: Record<string, Object>): void {
this.name = params.name as string
console.info(`${this.name}---复用`)
}
aboutToRecycle() {
console.info(`${this.name}---回收`)
}
build() {
Text(this.name)
.width('100%')
.height(110)
.fontSize(20)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.backgroundColor('#803c6ad0')
}
}
- 原生(onItemDragStart, onItemDragMove, onItemDrop)拖拽(LazyForEach + @Reusable)
javascript
/**
* 懒加载
*/
export class BasicDataSource<T> implements IDataSource {
private listeners: DataChangeListener[] = []
public totalCount(): number {
return 0
}
public getData(index: number): T | undefined {
return undefined
}
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
console.info('add listener')
this.listeners.push(listener)
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener)
if (pos >= 0) {
console.info('remove listener')
this.listeners.splice(pos, 1)
}
}
notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded()
})
}
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index)
})
}
notifyDataChange(index: number): void {
this.listeners.forEach(listener => {
listener.onDataChange(index)
})
}
notifyDataDelete(index: number): void {
this.listeners.forEach(listener => {
listener.onDataDelete(index)
})
}
notifyDataMove(from: number, to: number): void {
this.listeners.forEach(listener => {
listener.onDataMove(from, to)
})
}
}
/**
* 扩展懒加载
*/
export class CommonLazyModel<T> extends BasicDataSource<T> {
dataArray: Array<T> = new Array()
/**
* 获取数据长度
* @returns
*/
public totalCount(): number {
return this.dataArray.length
}
/**
* 获取某项数据
* @param index
* @returns
*/
public getData(index: number): T {
return this.dataArray[index]
}
/**
* 添加所有数据
* @param data
*/
public setAllData(data: Array<T>) {
this.dataArray = [...data]
this.notifyDataReload()
}
/**
* 交换数据
* @param idx_1
* @param idx_2
*/
public swapPositions(idx_1: number, idx_2: number) {
// 利用中间常量存交换值
const exchange = this.dataArray.splice(idx_1, 1)
this.dataArray.splice(idx_2, 0, exchange[0])
// 需要主动调用通知数据更新
this.notifyDataReload()
}
/**
* 获取数据
* @returns
*/
public getAllData() {
return this.dataArray
}
/**
* 插入数据
* @param index
* @param data
*/
public addData(index: number, data: T): void {
this.dataArray.splice(index, 0, data)
this.notifyDataAdd(index)
}
/**
* 追加数据
* @param data
*/
public pushData(data: T): void {
this.dataArray.push(data)
this.notifyDataAdd(this.dataArray.length - 1)
}
}
/**
* 模拟数据
*/
const LIST_DATA: string[] = [
"购物", "体育", "服装", "军事", "电商", "娱乐", "科技", "艺术", "新闻", "电商", "农村",
"旅游", "教育", "健康", "美食", "房产", "汽车", "财经", "文化", "音乐", "电影", "书籍",
"宠物", "家居", "美妆", "母婴", "职场", "环保", "历史", "游戏"
]
@Component
export struct listDragAndDrop {
/**
* 模拟数据
*/
private listData: CommonLazyModel<string> = new CommonLazyModel()
/**
* 列表构造器
*/
private listScroller: Scroller = new Scroller()
/**
* 当前拖拽某个item的下标
*/
@State curListItemDragIdx: number = -1
/**
* 可视区域的开头组件下标
*/
@State scrollerStart: number = 0
/**
* 可视区域的结尾组件下标
*/
@State scrollerEnd: number = 0
aboutToAppear(): void {
for (let i = 0; i < LIST_DATA.length; i++) {
this.listData.pushData(LIST_DATA[i] + "_" + i.toString())
}
}
/**
* 拖拽样式
*/
@Builder
draggingBuilder(curListItemDragIdx: number) {
Text(this.listData.getData(curListItemDragIdx))
.width('100%')
.height(110)
.fontSize(20)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.backgroundColor('#803c6ad0')
}
build() {
Column() {
List({ space: 10, scroller: this.listScroller }) {
LazyForEach(this.listData, (item: string, idx: number) => {
ListItem() {
ListItemCase({
name: item,
index: idx,
curListItemDragIdx: this.curListItemDragIdx
})
}
}, (item: string) => item)
}
.width('100%')
.height('100%')
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.None)
.onScrollIndex((start: number, end: number, center: number) => {
this.scrollerStart = start
this.scrollerEnd = end
})
// 开始拖拽列表元素时触发
.onItemDragStart((event: ItemDragInfo, curListItemDragIdx: number) => {
// 拖拽中显示的内容
return this.draggingBuilder(curListItemDragIdx)
})
.onItemDragMove((event: ItemDragInfo, itemIndex: number, insertIndex: number) => {
// 更新当前拖拽到某个item的下标
this.curListItemDragIdx = insertIndex
// 向下滑动
if (insertIndex === this.scrollerEnd && this.scrollerEnd <= this.listData.totalCount() - 1) {
this.listScroller.scrollToIndex(insertIndex - 5, true)
}
// 向上滑动
if (insertIndex === this.scrollerStart && this.scrollerStart !== 0) {
this.listScroller.scrollToIndex(insertIndex - 1, true)
}
})
// 拖拽结束
.onItemDrop((event: ItemDragInfo, itemIndex: number, insertIndex: number, isSuccess: boolean) => {
// 当前手势没释放 | 插入位置小于0 | 插入位置大于列表长度
if (!isSuccess || insertIndex < 0 || insertIndex >= this.listData.totalCount()) {
return
}
this.curListItemDragIdx = -1
this.listData.swapPositions(itemIndex, insertIndex)
})
}
.width('100%')
.height('100%')
.padding({ left: 15, right: 15 })
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
@Reusable
@Component
export struct ListItemCase {
@State name: string = ''
@Prop index: number = 0
@Prop curListItemDragIdx: number = -1
private itemScale: number = 0.75
aboutToReuse(params: Record<string, Object>): void {
this.name = params.name as string
console.info(`${this.name}---复用`)
}
aboutToRecycle() {
console.info(`${this.name}---回收`)
}
build() {
Text(this.name)
.width('100%')
.height(110)
.fontSize(20)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.backgroundColor('#803c6ad0')
.scale({
x: this.curListItemDragIdx === this.index ? this.itemScale : 1,
y: this.curListItemDragIdx === this.index ? this.itemScale : 1
})
.animation({ curve: Curve.Smooth, duration: 200 })
}
}
🌸🌼🌺