查看: 122|回复: 0

鸿蒙特效教程06-可拖拽网格实现教程

[复制链接]

1

主题

1

回帖

14

积分

新手上路

积分
14
发表于 2025-4-1 08:48:39 | 显示全部楼层 |阅读模式
鸿蒙特效教程06-可拖拽网格实现教程

本教程适合 HarmonyOS Next 初学者,通过简单到复杂的步骤,一步步实现类似桌面APP中的可拖拽编辑效果。
效果预览

我们要实现的效果是一个 Grid 网格布局,用户可以通过长按并拖动来调整应用图标的位置顺序。拖拽完成后,底部会显示当前的排序结果。

实现步骤

步骤一:创建基本结构和数据模型

首先,我们需要创建一个基本的页面结构和数据模型。我们将定义一个应用名称数组和一个对应的颜色数组。
  1. @Entry
  2. @Component
  3. struct DragGrid {
  4.   // 应用名称数组
  5.   @State apps: string[] = [
  6.     '微信', '支付宝', 'QQ', '抖音',
  7.     '快手', '微博', '头条', '网易云'
  8.   ];
  9.   build() {
  10.     Column() {
  11.       // 这里将放置我们的应用网格
  12.       Text('应用网格示例')
  13.         .fontSize(20)
  14.         .fontColor(Color.White)
  15.     }
  16.     .width('100%')
  17.     .height('100%')
  18.     .backgroundColor('#121212')
  19.   }
  20. }
复制代码
这个基本结构包含一个应用名称数组和一个简单的Column容器。在这个阶段,我们只是显示一个标题文本。
步骤二:使用Grid布局展示应用图标

接下来,我们将使用Grid组件来创建网格布局,并使用ForEach遍历应用数组,为每个应用创建一个网格项。
  1. @Entry
  2. @Component
  3. struct DragGrid {
  4.   @State apps: string[] = [
  5.     '微信', '支付宝', 'QQ', '抖音',
  6.     '快手', '微博', '头条', '网易云'
  7.   ];
  8.   build() {
  9.     Column() {
  10.       // 使用Grid组件创建网格布局
  11.       Grid() {
  12.         ForEach(this.apps, (item: string) => {
  13.           GridItem() {
  14.             Column() {
  15.               // 应用图标(暂用占位图)
  16.               Image($r('app.media.startIcon'))
  17.                 .width(60)
  18.                 .aspectRatio(1)
  19.               // 应用名称
  20.               Text(item)
  21.                 .fontSize(12)
  22.                 .fontColor(Color.White)
  23.             }
  24.             .padding(10)
  25.           }
  26.         })
  27.       }
  28.       .columnsTemplate('1fr 1fr 1fr 1fr') // 4列等宽布局
  29.       .rowsGap(10) // 行间距
  30.       .columnsGap(10) // 列间距
  31.       .padding(20) // 内边距
  32.     }
  33.     .width('100%')
  34.     .height('100%')
  35.     .backgroundColor('#121212')
  36.   }
  37. }
复制代码
在这一步,我们添加了Grid组件,它具有以下关键属性:
    columnsTemplate:定义网格的列模板,'1fr 1fr 1fr 1fr'表示四列等宽布局。rowsGap:行间距,设置为10。columnsGap:列间距,设置为10。padding:内边距,设置为20。
每个GridItem包含一个Column布局,里面有一个Image(应用图标)和一个Text(应用名称)。
步骤三:优化图标布局和样式

现在我们有了基本的网格布局,接下来优化图标的样式和布局。我们将创建一个自定义的Builder函数来构建每个应用图标项,并添加一些颜色来区分不同应用。
  1. @Entry
  2. @Component
  3. struct DragGrid {
  4.   @State apps: string[] = [
  5.     '微信', '支付宝', 'QQ', '抖音',
  6.     '快手', '微博', '头条', '网易云',
  7.     '腾讯视频', '爱奇艺', '优酷', 'B站'
  8.   ];
  9.   // 定义应用图标颜色
  10.   private appColors: string[] = [
  11.     '#34C759', '#007AFF', '#5856D6', '#FF2D55',
  12.     '#FF9500', '#FF3B30', '#E73C39', '#D33A31',
  13.     '#38B0DE', '#39A5DC', '#22C8BD', '#00A1D6'
  14.   ];
  15.   // 创建应用图标项的构建器
  16.   @Builder
  17.   itemBuilder(name: string, index: number) {
  18.     Column({ space: 2 }) {
  19.       // 应用图标
  20.       Image($r('app.media.startIcon'))
  21.         .width(80)
  22.         .padding(10)
  23.         .aspectRatio(1)
  24.         .backgroundColor(this.appColors[index % this.appColors.length])
  25.         .borderRadius(16)
  26.       // 应用名称
  27.       Text(name)
  28.         .fontSize(12)
  29.         .fontColor(Color.White)
  30.     }
  31.   }
  32.   build() {
  33.     Column() {
  34.       Grid() {
  35.         ForEach(this.apps, (item: string, index: number) => {
  36.           GridItem() {
  37.             this.itemBuilder(item, index)
  38.           }
  39.         })
  40.       }
  41.       .columnsTemplate('1fr 1fr 1fr 1fr')
  42.       .rowsGap(20)
  43.       .columnsGap(20)
  44.       .padding(20)
  45.     }
  46.     .width('100%')
  47.     .height('100%')
  48.     .backgroundColor('#121212')
  49.   }
  50. }
复制代码
在这一步,我们:
    添加了 appColors数组,定义了各个应用图标的背景颜色。创建了 @Builder itemBuilder函数,用于构建每个应用图标项,使代码更加模块化。为图标添加了背景颜色和圆角边框,使其更加美观。在ForEach中添加了index参数,用于获取当前项的索引,以便为不同应用使用不同的颜色。
步骤四:添加拖拽功能

现在我们有了美观的网格布局,下一步是添加拖拽功能。我们需要设置Grid的 editMode属性为true,并添加相应的拖拽事件处理函数。
  1. @Entry
  2. @Component
  3. struct DragGrid {
  4.   @State apps: string[] = [
  5.     '微信', '支付宝', 'QQ', '抖音',
  6.     '快手', '微博', '头条', '网易云',
  7.     '腾讯视频', '爱奇艺', '优酷', 'B站'
  8.   ];
  9.   private appColors: string[] = [
  10.     '#34C759', '#007AFF', '#5856D6', '#FF2D55',
  11.     '#FF9500', '#FF3B30', '#E73C39', '#D33A31',
  12.     '#38B0DE', '#39A5DC', '#22C8BD', '#00A1D6'
  13.   ];
  14.   @Builder
  15.   itemBuilder(name: string) {
  16.     Column({ space: 2 }) {
  17.       Image($r('app.media.startIcon'))
  18.         .draggable(false) // 禁止图片本身被拖拽
  19.         .width(80)
  20.         .padding(10)
  21.         .aspectRatio(1)
  22.       Text(name)
  23.         .fontSize(12)
  24.         .fontColor(Color.White)
  25.     }
  26.   }
  27.   // 交换两个应用的位置
  28.   changeIndex(a: number, b: number) {
  29.     let temp = this.apps[a];
  30.     this.apps[a] = this.apps[b];
  31.     this.apps[b] = temp;
  32.   }
  33.   build() {
  34.     Column() {
  35.       Grid() {
  36.         ForEach(this.apps, (item: string) => {
  37.           GridItem() {
  38.             this.itemBuilder(item)
  39.           }
  40.         })
  41.       }
  42.       .columnsTemplate('1fr 1fr 1fr 1fr')
  43.       .rowsGap(20)
  44.       .columnsGap(20)
  45.       .padding(20)
  46.       .supportAnimation(true) // 启用动画
  47.       .editMode(true) // 启用编辑模式
  48.       // 拖拽开始事件
  49.       .onItemDragStart((_event: ItemDragInfo, itemIndex: number) => {
  50.         return this.itemBuilder(this.apps[itemIndex]);
  51.       })
  52.       // 拖拽放置事件
  53.       .onItemDrop((_event: ItemDragInfo, itemIndex: number, insertIndex: number, isSuccess: boolean) => {
  54.         if (!isSuccess || insertIndex >= this.apps.length) {
  55.           return;
  56.         }
  57.         this.changeIndex(itemIndex, insertIndex);
  58.       })
  59.       .layoutWeight(1)
  60.     }
  61.     .width('100%')
  62.     .height('100%')
  63.     .backgroundColor('#121212')
  64.   }
  65. }
复制代码
在这一步,我们添加了拖拽功能的关键部分:
    设置 supportAnimation(true)来启用动画效果。设置 editMode(true)来启用编辑模式,这是实现拖拽功能的必要设置。添加 onItemDragStart事件处理函数,当用户开始拖拽时触发,返回被拖拽项的UI表示。添加 onItemDrop事件处理函数,当用户放置拖拽项时触发,处理位置交换逻辑。创建 changeIndex方法,用于交换数组中两个元素的位置。在Image上设置 draggable(false),确保是整个GridItem被拖拽,而不是图片本身。
步骤五:添加排序结果展示

为了让用户更直观地看到排序结果,我们在网格下方添加一个区域,用于显示当前的应用排序结果。
  1. @Entry
  2. @Component
  3. struct DragGrid {
  4.   // 前面的代码保持不变...
  5.   build() {
  6.     Column() {
  7.       // 应用网格部分保持不变...
  8.       // 添加排序结果展示区域
  9.       Column({ space: 10 }) {
  10.         Text('应用排序结果')
  11.           .fontSize(16)
  12.           .fontColor(Color.White)
  13.         Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center }) {
  14.           ForEach(this.apps, (item: string, index: number) => {
  15.             Text(item)
  16.               .fontSize(12)
  17.               .fontColor(Color.White)
  18.               .backgroundColor(this.appColors[index % this.appColors.length])
  19.               .borderRadius(12)
  20.               .padding({
  21.                 left: 10,
  22.                 right: 10,
  23.                 top: 4,
  24.                 bottom: 4
  25.               })
  26.               .margin(4)
  27.           })
  28.         }
  29.         .width('100%')
  30.       }
  31.       .width('100%')
  32.       .padding(10)
  33.       .backgroundColor('#0DFFFFFF') // 半透明背景
  34.       .borderRadius({ topLeft: 20, topRight: 20 }) // 上方圆角
  35.     }
  36.     .width('100%')
  37.     .height('100%')
  38.     .backgroundColor('#121212')
  39.   }
  40. }
复制代码
在这一步,我们添加了一个展示排序结果的区域:
    使用Column容器,顶部显示"应用排序结果"的标题。使用Flex布局,设置 wrap: FlexWrap.Wrap允许内容换行,justifyContent: FlexAlign.Center使内容居中对齐。使用ForEach循环遍历应用数组,为每个应用创建一个带有背景色的文本标签。为结果区域添加半透明背景和上方圆角,使其更加美观。
步骤六:美化界面

最后,我们美化整个界面,添加渐变背景和一些视觉改进。
  1. @Entry
  2. @Component
  3. struct DragGrid {
  4.   // 前面的代码保持不变...
  5.   build() {
  6.     Column() {
  7.       // 网格和结果区域代码保持不变...
  8.     }
  9.     .width('100%')
  10.     .height('100%')
  11.     .expandSafeArea() // 扩展到安全区域
  12.     .linearGradient({ // 渐变背景
  13.       angle: 135,
  14.       colors: [
  15.         ['#121212', 0],
  16.         ['#242424', 1]
  17.       ]
  18.     })
  19.   }
  20. }
复制代码
在这一步,我们:
    添加 expandSafeArea()确保内容可以扩展到设备的安全区域。使用 linearGradient创建渐变背景,角度为135度,从深色(#121212)渐变到稍浅的色调(#242424)。
完整代码

以下是完整的实现代码:
  1. @Entry
  2. @Component
  3. struct DragGrid {
  4.   // 应用名称数组,用于显示和排序
  5.   @State apps: string[] = [
  6.     '微信', '支付宝', 'QQ', '抖音',
  7.     '快手', '微博', '头条', '网易云',
  8.     '腾讯视频', '爱奇艺', '优酷', 'B站',
  9.     '小红书', '美团', '饿了么', '滴滴',
  10.     '高德', '携程'
  11.   ];
  12.   // 定义应用图标对应的颜色数组
  13.   private appColors: string[] = [
  14.     '#34C759', '#007AFF', '#5856D6', '#FF2D55',
  15.     '#FF9500', '#FF3B30', '#E73C39', '#D33A31',
  16.     '#38B0DE', '#39A5DC', '#22C8BD', '#00A1D6',
  17.     '#FF3A31', '#FFD800', '#4290F7', '#FF7700',
  18.     '#4AB66B', '#2A9AF1'
  19.   ];
  20.   /**
  21.    * 构建单个应用图标项
  22.    * @param name 应用名称
  23.    * @return 返回应用图标的UI组件
  24.    */
  25.   @Builder
  26.   itemBuilder(name: string) {
  27.     // 垂直布局,包含图标和文字
  28.     Column({ space: 2 }) {
  29.       // 应用图标图片
  30.       Image($r('app.media.startIcon'))
  31.         .draggable(false)// 禁止图片本身被拖拽,确保整个GridItem被拖拽
  32.         .width(80)
  33.         .aspectRatio(1)// 保持1:1的宽高比
  34.         .padding(10)
  35.       // 应用名称文本
  36.       Text(name)
  37.         .fontSize(12)
  38.         .fontColor(Color.White)
  39.     }
  40.   }
  41.   /**
  42.    * 交换两个应用在数组中的位置
  43.    * @param a 第一个索引
  44.    * @param b 第二个索引
  45.    */
  46.   changeIndex(a: number, b: number) {
  47.     // 使用临时变量交换两个元素位置
  48.     let temp = this.apps[a];
  49.     this.apps[a] = this.apps[b];
  50.     this.apps[b] = temp;
  51.   }
  52.   /**
  53.    * 构建组件的UI结构
  54.    */
  55.   build() {
  56.     // 主容器,垂直布局
  57.     Column() {
  58.       // 应用网格区域
  59.       Grid() {
  60.         // 遍历所有应用,为每个应用创建一个网格项
  61.         ForEach(this.apps, (item: string) => {
  62.           GridItem() {
  63.             // 使用自定义builder构建网格项内容
  64.             this.itemBuilder(item)
  65.           }
  66.         })
  67.       }
  68.       // 网格样式和行为设置
  69.       .columnsTemplate('1fr '.repeat(4)) // 设置4列等宽布局
  70.       .columnsGap(20) // 列间距
  71.       .rowsGap(20) // 行间距
  72.       .padding(20) // 内边距
  73.       .supportAnimation(true) // 启用动画支持
  74.       .editMode(true) // 启用编辑模式,允许拖拽
  75.       // 拖拽开始事件处理
  76.       .onItemDragStart((_event: ItemDragInfo, itemIndex: number) => {
  77.         // 返回被拖拽项的UI
  78.         return this.itemBuilder(this.apps[itemIndex]);
  79.       })
  80.       // 拖拽放置事件处理
  81.       .onItemDrop((_event: ItemDragInfo, itemIndex: number, insertIndex: number, isSuccess: boolean) => {
  82.         // 如果拖拽失败或目标位置无效,则不执行操作
  83.         if (!isSuccess || insertIndex >= this.apps.length) {
  84.           return;
  85.         }
  86.         // 交换元素位置
  87.         this.changeIndex(itemIndex, insertIndex);
  88.       })
  89.       .layoutWeight(1) // 使网格区域占用剩余空间
  90.       // 结果显示区域
  91.       Column({ space: 10 }) {
  92.         // 标题文本
  93.         Text('应用排序结果')
  94.           .fontSize(16)
  95.           .fontColor(Color.White)
  96.         // 弹性布局,允许换行
  97.         Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center }) {
  98.           // 遍历应用数组,为每个应用创建一个彩色标签
  99.           ForEach(this.apps, (item: string, index: number) => {
  100.             Text(item)
  101.               .fontSize(12)
  102.               .fontColor(Color.White)
  103.               .backgroundColor(this.appColors[index % this.appColors.length])
  104.               .borderRadius(12)
  105.               .padding({
  106.                 left: 10,
  107.                 right: 10,
  108.                 top: 4,
  109.                 bottom: 4
  110.               })
  111.               .margin(4)
  112.           })
  113.         }
  114.         .width('100%')
  115.       }
  116.       .width('100%')
  117.       .padding(10) // 内边距
  118.       .backgroundColor('#0DFFFFFF') // 半透明背景
  119.       .expandSafeArea() // 扩展到安全区域
  120.       .borderRadius({ topLeft: 20, topRight: 20 }) // 上左右圆角
  121.     }
  122.     // 主容器样式设置
  123.     .width('100%')
  124.     .height('100%')
  125.     .expandSafeArea() // 扩展到安全区域
  126.     .linearGradient({
  127.       angle: 135, // 渐变角度
  128.       colors: [
  129.         ['#121212', 0], // 起点色
  130.         ['#242424', 1] // 终点色
  131.       ]
  132.     })
  133.   }
  134. }
复制代码
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组件和拖拽功能的使用方法!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表