鸿蒙特效教程06-可拖拽网格实现教程
本教程适合 HarmonyOS Next 初学者,通过简单到复杂的步骤,一步步实现类似桌面APP中的可拖拽编辑效果。
效果预览
我们要实现的效果是一个 Grid 网格布局,用户可以通过长按并拖动来调整应用图标的位置顺序。拖拽完成后,底部会显示当前的排序结果。

实现步骤
步骤一:创建基本结构和数据模型
首先,我们需要创建一个基本的页面结构和数据模型。我们将定义一个应用名称数组和一个对应的颜色数组。
typescript
@Entry
@Component
struct DragGrid {
// 应用名称数组
@State apps: string[] = [
'微信', '支付宝', 'QQ', '抖音',
'快手', '微博', '头条', '网易云'
];
build() {
Column() {
// 这里将放置我们的应用网格
Text('应用网格示例')
.fontSize(20)
.fontColor(Color.White)
}
.width('100%')
.height('100%')
.backgroundColor('#121212')
}
}
这个基本结构包含一个应用名称数组和一个简单的Column容器。在这个阶段,我们只是显示一个标题文本。
步骤二:使用Grid布局展示应用图标
接下来,我们将使用Grid组件来创建网格布局,并使用ForEach遍历应用数组,为每个应用创建一个网格项。
typescript
@Entry
@Component
struct DragGrid {
@State apps: string[] = [
'微信', '支付宝', 'QQ', '抖音',
'快手', '微博', '头条', '网易云'
];
build() {
Column() {
// 使用Grid组件创建网格布局
Grid() {
ForEach(this.apps, (item: string) => {
GridItem() {
Column() {
// 应用图标(暂用占位图)
Image($r('app.media.startIcon'))
.width(60)
.aspectRatio(1)
// 应用名称
Text(item)
.fontSize(12)
.fontColor(Color.White)
}
.padding(10)
}
})
}
.columnsTemplate('1fr 1fr 1fr 1fr') // 4列等宽布局
.rowsGap(10) // 行间距
.columnsGap(10) // 列间距
.padding(20) // 内边距
}
.width('100%')
.height('100%')
.backgroundColor('#121212')
}
}
在这一步,我们添加了Grid组件,它具有以下关键属性:
- columnsTemplate:定义网格的列模板,'1fr 1fr 1fr 1fr'表示四列等宽布局。
- rowsGap:行间距,设置为10。
- columnsGap:列间距,设置为10。
- padding:内边距,设置为20。
每个GridItem包含一个Column布局,里面有一个Image(应用图标)和一个Text(应用名称)。
步骤三:优化图标布局和样式
现在我们有了基本的网格布局,接下来优化图标的样式和布局。我们将创建一个自定义的Builder函数来构建每个应用图标项,并添加一些颜色来区分不同应用。
typescript
@Entry
@Component
struct DragGrid {
@State apps: string[] = [
'微信', '支付宝', 'QQ', '抖音',
'快手', '微博', '头条', '网易云',
'腾讯视频', '爱奇艺', '优酷', 'B站'
];
// 定义应用图标颜色
private appColors: string[] = [
'#34C759', '#007AFF', '#5856D6', '#FF2D55',
'#FF9500', '#FF3B30', '#E73C39', '#D33A31',
'#38B0DE', '#39A5DC', '#22C8BD', '#00A1D6'
];
// 创建应用图标项的构建器
@Builder
itemBuilder(name: string, index: number) {
Column({ space: 2 }) {
// 应用图标
Image($r('app.media.startIcon'))
.width(80)
.padding(10)
.aspectRatio(1)
.backgroundColor(this.appColors[index % this.appColors.length])
.borderRadius(16)
// 应用名称
Text(name)
.fontSize(12)
.fontColor(Color.White)
}
}
build() {
Column() {
Grid() {
ForEach(this.apps, (item: string, index: number) => {
GridItem() {
this.itemBuilder(item, index)
}
})
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsGap(20)
.columnsGap(20)
.padding(20)
}
.width('100%')
.height('100%')
.backgroundColor('#121212')
}
}
在这一步,我们:
- 添加了
appColors
数组,定义了各个应用图标的背景颜色。 - 创建了
@Builder itemBuilder
函数,用于构建每个应用图标项,使代码更加模块化。 - 为图标添加了背景颜色和圆角边框,使其更加美观。
- 在ForEach中添加了index参数,用于获取当前项的索引,以便为不同应用使用不同的颜色。
步骤四:添加拖拽功能
现在我们有了美观的网格布局,下一步是添加拖拽功能。我们需要设置Grid的editMode
属性为true,并添加相应的拖拽事件处理函数。
typescript
@Entry
@Component
struct DragGrid {
@State apps: string[] = [
'微信', '支付宝', 'QQ', '抖音',
'快手', '微博', '头条', '网易云',
'腾讯视频', '爱奇艺', '优酷', 'B站'
];
private appColors: string[] = [
'#34C759', '#007AFF', '#5856D6', '#FF2D55',
'#FF9500', '#FF3B30', '#E73C39', '#D33A31',
'#38B0DE', '#39A5DC', '#22C8BD', '#00A1D6'
];
@Builder
itemBuilder(name: string) {
Column({ space: 2 }) {
Image($r('app.media.startIcon'))
.draggable(false) // 禁止图片本身被拖拽
.width(80)
.padding(10)
.aspectRatio(1)
Text(name)
.fontSize(12)
.fontColor(Color.White)
}
}
// 交换两个应用的位置
changeIndex(a: number, b: number) {
let temp = this.apps[a];
this.apps[a] = this.apps[b];
this.apps[b] = temp;
}
build() {
Column() {
Grid() {
ForEach(this.apps, (item: string) => {
GridItem() {
this.itemBuilder(item)
}
})
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsGap(20)
.columnsGap(20)
.padding(20)
.supportAnimation(true) // 启用动画
.editMode(true) // 启用编辑模式
// 拖拽开始事件
.onItemDragStart((_event: ItemDragInfo, itemIndex: number) => {
return this.itemBuilder(this.apps[itemIndex]);
})
// 拖拽放置事件
.onItemDrop((_event: ItemDragInfo, itemIndex: number, insertIndex: number, isSuccess: boolean) => {
if (!isSuccess || insertIndex >= this.apps.length) {
return;
}
this.changeIndex(itemIndex, insertIndex);
})
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor('#121212')
}
}
在这一步,我们添加了拖拽功能的关键部分:
- 设置
supportAnimation(true)
来启用动画效果。 - 设置
editMode(true)
来启用编辑模式,这是实现拖拽功能的必要设置。 - 添加
onItemDragStart
事件处理函数,当用户开始拖拽时触发,返回被拖拽项的UI表示。 - 添加
onItemDrop
事件处理函数,当用户放置拖拽项时触发,处理位置交换逻辑。 - 创建
changeIndex
方法,用于交换数组中两个元素的位置。 - 在Image上设置
draggable(false)
,确保是整个GridItem被拖拽,而不是图片本身。
步骤五:添加排序结果展示
为了让用户更直观地看到排序结果,我们在网格下方添加一个区域,用于显示当前的应用排序结果。
typescript
@Entry
@Component
struct DragGrid {
// 前面的代码保持不变...
build() {
Column() {
// 应用网格部分保持不变...
// 添加排序结果展示区域
Column({ space: 10 }) {
Text('应用排序结果')
.fontSize(16)
.fontColor(Color.White)
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center }) {
ForEach(this.apps, (item: string, index: number) => {
Text(item)
.fontSize(12)
.fontColor(Color.White)
.backgroundColor(this.appColors[index % this.appColors.length])
.borderRadius(12)
.padding({
left: 10,
right: 10,
top: 4,
bottom: 4
})
.margin(4)
})
}
.width('100%')
}
.width('100%')
.padding(10)
.backgroundColor('#0DFFFFFF') // 半透明背景
.borderRadius({ topLeft: 20, topRight: 20 }) // 上方圆角
}
.width('100%')
.height('100%')
.backgroundColor('#121212')
}
}
在这一步,我们添加了一个展示排序结果的区域:
- 使用Column容器,顶部显示"应用排序结果"的标题。
- 使用Flex布局,设置
wrap: FlexWrap.Wrap
允许内容换行,justifyContent: FlexAlign.Center
使内容居中对齐。 - 使用ForEach循环遍历应用数组,为每个应用创建一个带有背景色的文本标签。
- 为结果区域添加半透明背景和上方圆角,使其更加美观。
步骤六:美化界面
最后,我们美化整个界面,添加渐变背景和一些视觉改进。
typescript
@Entry
@Component
struct DragGrid {
// 前面的代码保持不变...
build() {
Column() {
// 网格和结果区域代码保持不变...
}
.width('100%')
.height('100%')
.expandSafeArea() // 扩展到安全区域
.linearGradient({ // 渐变背景
angle: 135,
colors: [
['#121212', 0],
['#242424', 1]
]
})
}
}
在这一步,我们:
- 添加
expandSafeArea()
确保内容可以扩展到设备的安全区域。 - 使用
linearGradient
创建渐变背景,角度为135度,从深色(#121212)渐变到稍浅的色调(#242424)。
完整代码
以下是完整的实现代码:
typescript
@Entry
@Component
struct DragGrid {
// 应用名称数组,用于显示和排序
@State apps: string[] = [
'微信', '支付宝', 'QQ', '抖音',
'快手', '微博', '头条', '网易云',
'腾讯视频', '爱奇艺', '优酷', 'B站',
'小红书', '美团', '饿了么', '滴滴',
'高德', '携程'
];
// 定义应用图标对应的颜色数组
private appColors: string[] = [
'#34C759', '#007AFF', '#5856D6', '#FF2D55',
'#FF9500', '#FF3B30', '#E73C39', '#D33A31',
'#38B0DE', '#39A5DC', '#22C8BD', '#00A1D6',
'#FF3A31', '#FFD800', '#4290F7', '#FF7700',
'#4AB66B', '#2A9AF1'
];
/**
* 构建单个应用图标项
* @param name 应用名称
* @return 返回应用图标的UI组件
*/
@Builder
itemBuilder(name: string) {
// 垂直布局,包含图标和文字
Column({ space: 2 }) {
// 应用图标图片
Image($r('app.media.startIcon'))
.draggable(false)// 禁止图片本身被拖拽,确保整个GridItem被拖拽
.width(80)
.aspectRatio(1)// 保持1:1的宽高比
.padding(10)
// 应用名称文本
Text(name)
.fontSize(12)
.fontColor(Color.White)
}
}
/**
* 交换两个应用在数组中的位置
* @param a 第一个索引
* @param b 第二个索引
*/
changeIndex(a: number, b: number) {
// 使用临时变量交换两个元素位置
let temp = this.apps[a];
this.apps[a] = this.apps[b];
this.apps[b] = temp;
}
/**
* 构建组件的UI结构
*/
build() {
// 主容器,垂直布局
Column() {
// 应用网格区域
Grid() {
// 遍历所有应用,为每个应用创建一个网格项
ForEach(this.apps, (item: string) => {
GridItem() {
// 使用自定义builder构建网格项内容
this.itemBuilder(item)
}
})
}
// 网格样式和行为设置
.columnsTemplate('1fr '.repeat(4)) // 设置4列等宽布局
.columnsGap(20) // 列间距
.rowsGap(20) // 行间距
.padding(20) // 内边距
.supportAnimation(true) // 启用动画支持
.editMode(true) // 启用编辑模式,允许拖拽
// 拖拽开始事件处理
.onItemDragStart((_event: ItemDragInfo, itemIndex: number) => {
// 返回被拖拽项的UI
return this.itemBuilder(this.apps[itemIndex]);
})
// 拖拽放置事件处理
.onItemDrop((_event: ItemDragInfo, itemIndex: number, insertIndex: number, isSuccess: boolean) => {
// 如果拖拽失败或目标位置无效,则不执行操作
if (!isSuccess || insertIndex >= this.apps.length) {
return;
}
// 交换元素位置
this.changeIndex(itemIndex, insertIndex);
})
.layoutWeight(1) // 使网格区域占用剩余空间
// 结果显示区域
Column({ space: 10 }) {
// 标题文本
Text('应用排序结果')
.fontSize(16)
.fontColor(Color.White)
// 弹性布局,允许换行
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center }) {
// 遍历应用数组,为每个应用创建一个彩色标签
ForEach(this.apps, (item: string, index: number) => {
Text(item)
.fontSize(12)
.fontColor(Color.White)
.backgroundColor(this.appColors[index % this.appColors.length])
.borderRadius(12)
.padding({
left: 10,
right: 10,
top: 4,
bottom: 4
})
.margin(4)
})
}
.width('100%')
}
.width('100%')
.padding(10) // 内边距
.backgroundColor('#0DFFFFFF') // 半透明背景
.expandSafeArea() // 扩展到安全区域
.borderRadius({ topLeft: 20, topRight: 20 }) // 上左右圆角
}
// 主容器样式设置
.width('100%')
.height('100%')
.expandSafeArea() // 扩展到安全区域
.linearGradient({
angle: 135, // 渐变角度
colors: [
['#121212', 0], // 起点色
['#242424', 1] // 终点色
]
})
}
}
Grid组件的关键属性详解
Grid是鸿蒙系统中用于创建网格布局的重要组件,它有以下关键属性:
-
columnsTemplate : 定义网格的列模板。例如
'1fr 1fr 1fr 1fr'
表示四列等宽布局。'1fr'中的'fr'是fraction(分数)的缩写,表示按比例分配空间。 -
rowsTemplate: 定义网格的行模板。如果不设置,行高将根据内容自动调整。
-
columnsGap: 列之间的间距。
-
rowsGap: 行之间的间距。
-
editMode: 是否启用编辑模式。设置为true时启用拖拽功能。
-
supportAnimation: 是否支持动画。设置为true时,拖拽过程中会有平滑的动画效果。
拖拽功能的关键事件详解
实现拖拽功能主要依赖以下事件:
-
onItemDragStart: 当用户开始拖拽某个项时触发。
- 参数:event(拖拽事件信息),itemIndex(被拖拽项的索引)
- 返回值:被拖拽项的UI表示
-
onItemDrop: 当用户放置拖拽项时触发。
- 参数:event(拖拽事件信息),itemIndex(原始位置索引),insertIndex(目标位置索引),isSuccess(是否成功)
- 功能:处理元素位置交换逻辑
此外,还有一些可选的事件可以用于增强拖拽体验:
- onItemDragEnter: 当拖拽项进入某个位置时触发。
- onItemDragMove: 当拖拽项在网格中移动时触发。
- onItemDragLeave: 当拖拽项离开某个位置时触发。
小结与进阶提示
通过本教程,我们实现了一个功能完整的可拖拽应用网格界面。主要学习了以下内容:
- 使用Grid组件创建网格布局
- 使用@Builder创建可复用的UI构建函数
- 实现拖拽排序功能
- 优化UI和用户体验
进阶提示:
- 可以添加长按震动反馈,增强交互体验。
- 可以实现数据持久化,保存用户的排序结果。
- 可以添加编辑模式切换,只有在特定模式下才允许拖拽排序。
- 可以为拖拽过程添加更丰富的动画效果,如缩放、阴影等。
希望本教程对你有所帮助,让你掌握鸿蒙系统中Grid组件和拖拽功能的使用方法!