前言
网格拖拽,此功能很是常见,一般用于频道的编辑或者条目顺序的排列,在鸿蒙的开发中,针对网格的编辑,系统也给出了相关的Api,通过onItemDragStart和在onItemDrop即可轻松实现,onItemDragStart用于设置拖拽过程中的显示,onItemDrop是进行数据交换逻辑处理。
根据官方提供,我们随便实现了一个简单的拖拽效果:
TypeScript
@Entry
@Component
struct Index {
@State numbers: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
@Builder
itemLayout(text: string) {
Text(text)
.fontSize(16)
.backgroundColor(Color.Pink)
.width(80)
.height(80)
.textAlign(TextAlign.Center)
}
changeIndex(index1: number, index2: number) { //交换数组位置
let temp = this.numbers[index1];
this.numbers[index1] = this.numbers[index2];
this.numbers[index2] = temp;
}
build() {
Column() {
Grid() {
ForEach(this.numbers, (n: number) => {
GridItem() {
this.itemLayout(n.toString())
}
})
}
.columnsTemplate('1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(10)
.editMode(true) //设置Grid是否进入编辑模式
.onItemDragStart((event: ItemDragInfo, itemIndex: number) => { //第一次拖拽此事件绑定的组件时,触发回调。
return this.itemLayout(this.numbers[itemIndex].toString()) //设置拖拽过程中显示
})
.onItemDrop((event: ItemDragInfo, itemIndex: number, insertIndex: number,
isSuccess: boolean) => {
if (!isSuccess || insertIndex >= this.numbers.length) {
return
}
this.changeIndex(itemIndex, insertIndex)
})
}.width('100%')
.height("100%")
}
}
以上代码我们就很简单的实现了一个网格拖拽,实际运行之后,长按确实发生了拖拽,和对应的item也进行了交换,但是并没有一个移动交换的过程,这是因为没有设置拖拽动画方法,如果想要拖拽过程中有移动动画,只需要设置supportAnimation为true即可,除此之外,还有一个非常重要的属性,那就是editMode,为true即为编辑模式,false即不可编辑。
拖拽的代码很是简单,毕竟官方也给出了相关案例,其中有两个点是未给出的,那就是禁止拖拽,和禁止与其交换。
比如,第一个和第二个条目,不能拖拽,如何进行设置呢?再比如,任何条目都不能和第一个进行交换,又该如何设置呢?
禁止拖拽
还是上述的代码,例如,把第一个条目禁止,不让其执行拖拽,实现起来很是简单,在onItemDragStart方法里,如果触摸的是指定索引,不让其执行即可。
TypeScript
.onItemDragStart((event: ItemDragInfo, itemIndex: number) => {
//第一次拖拽此事件绑定的组件时,触发回调。
if (itemIndex == 0) {
//禁止拖拽
return
}
return this.itemLayout(this.numbers[itemIndex].toString()) //设置拖拽过程中显示
})
需要注意的是,以上的代码仅仅是做到了禁止拖拽,但是,有一个潜在的问题是,别的条目是可以和它进行交换的,交换过之后,由于它的索引发生了变化,就变得可以拖拽了,如果想实现真正的,即便被交换后也不能拖拽,那就不能判断索引了,可以以唯一值进行判断,比如item数据换成对象,在对象里定义唯一值。
禁止交换
实际的开发中,除了某个条目禁止拖拽之外,也有不能和它进行交换的逻辑,比如第一个条目,就是固定的,不仅仅禁止拖拽,也不能和其进行交换,这种情况下如何进行实现呢?
TypeScript
onItemDrop((event: ItemDragInfo, itemIndex: number, insertIndex: number,
isSuccess: boolean) => {
//第一个禁止交换
if (insertIndex <= 0) {
return
}
if (!isSuccess || insertIndex >= this.numbers.length) {
return
}
this.changeIndex(itemIndex, insertIndex)
})
很是简单,如果是第一个,直接不执行即可,以上的代码虽然实现了禁止交换,但是有一个问题,那就是实际的动画已经执行了,只是最后的交换没有执行而已,在视觉上仍然存在很大的问题。
我们想要的效果是,保留移动拖拽的动画,只是拖拽到第一个时,动画不执行,其他的该怎么执行就怎么执行。
那么,在设置supportAnimation动画的时候,就不能直接设置为true了,而是移动到条目一的位置后,设置为fasle,其他再设置为true。
如何拿到第一个item的坐标呢,其实每一个组件都有一个方法,onAreaChange,通过这个属性,我们就可以拿到任何一个组件的宽高还有,xy的坐标,当然,你也可以通过计算的方式,毕竟,屏幕的宽高,还有条目的宽高你都知道。
手势移动的坐标可以通过onTouch方法进行获取,在Move事件中进行判断,如果移动到了条目一的范围之内,取消动画,否则就执行动画。
简单实现
目前呢这个网格拖拽功能已经做了一层封装,放到了refresh库中,大家如果想直接使用,可以依赖这个库:
TypeScript
"dependencies": { "@abner/refresh": "^1.3.6"}
简单案例如下:
TypeScript
import { GridDropView } from '@abner/refresh'
@Entry
@Component
struct Index {
@State numbers: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
@Builder
itemLayout(item: Object, _: number) {
Text(item.toString())
.fontSize(16)
.backgroundColor(Color.Pink)
.width(80)
.height(80)
.textAlign(TextAlign.Center)
}
build() {
Column() {
GridDropView({
columnsTemplate:"1fr 1fr 1fr",
items: this.numbers,
itemLayout: this.itemLayout,
dropLayout: this.itemLayout,
rowsGap: 10,
columnsGap: 10,
isEditMode: true,
prohibitDrop: [0, 1], //禁止拖拽的索引
prohibitMaxSwap: 0, //禁止交换
onDropData: (items) => {
console.log("拖拽结束:" + JSON.stringify(items))
}
})
}.width('100%')
.height("100%")
}
}
注意事项
实现拖拽,最重要的三个方法就是,打开编辑状态editMode,实现onItemDragStart和onItemDrop,设置拖拽移动动画和交换数据,如果想到开启补位动画,还需要实现supportAnimation方法。