写点什么

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

作者:苏杰豪
  • 2025-03-26
    北京
  • 本文字数:6752 字

    阅读完需:约 22 分钟

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

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

效果预览

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


实现步骤

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

首先,我们需要创建一个基本的页面结构和数据模型。我们将定义一个应用名称数组和一个对应的颜色数组。


@Entry@Componentstruct DragGrid {  // 应用名称数组  @State apps: string[] = [    '微信', '支付宝', 'QQ', '抖音',    '快手', '微博', '头条', '网易云'  ];    build() {    Column() {      // 这里将放置我们的应用网格      Text('应用网格示例')        .fontSize(20)        .fontColor(Color.White)    }    .width('100%')    .height('100%')    .backgroundColor('#121212')  }}
复制代码


这个基本结构包含一个应用名称数组和一个简单的 Column 容器。在这个阶段,我们只是显示一个标题文本。

步骤二:使用 Grid 布局展示应用图标

接下来,我们将使用 Grid 组件来创建网格布局,并使用 ForEach 遍历应用数组,为每个应用创建一个网格项。


@Entry@Componentstruct 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 函数来构建每个应用图标项,并添加一些颜色来区分不同应用。


@Entry@Componentstruct 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')  }}
复制代码


在这一步,我们:


  1. 添加了appColors数组,定义了各个应用图标的背景颜色。

  2. 创建了@Builder itemBuilder函数,用于构建每个应用图标项,使代码更加模块化。

  3. 为图标添加了背景颜色和圆角边框,使其更加美观。

  4. 在 ForEach 中添加了 index 参数,用于获取当前项的索引,以便为不同应用使用不同的颜色。

步骤四:添加拖拽功能

现在我们有了美观的网格布局,下一步是添加拖拽功能。我们需要设置 Grid 的editMode属性为 true,并添加相应的拖拽事件处理函数。


@Entry@Componentstruct 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')  }}
复制代码


在这一步,我们添加了拖拽功能的关键部分:


  1. 设置supportAnimation(true)来启用动画效果。

  2. 设置editMode(true)来启用编辑模式,这是实现拖拽功能的必要设置。

  3. 添加onItemDragStart事件处理函数,当用户开始拖拽时触发,返回被拖拽项的 UI 表示。

  4. 添加onItemDrop事件处理函数,当用户放置拖拽项时触发,处理位置交换逻辑。

  5. 创建changeIndex方法,用于交换数组中两个元素的位置。

  6. 在 Image 上设置draggable(false),确保是整个 GridItem 被拖拽,而不是图片本身。

步骤五:添加排序结果展示

为了让用户更直观地看到排序结果,我们在网格下方添加一个区域,用于显示当前的应用排序结果。


@Entry@Componentstruct 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')  }}
复制代码


在这一步,我们添加了一个展示排序结果的区域:


  1. 使用 Column 容器,顶部显示"应用排序结果"的标题。

  2. 使用 Flex 布局,设置wrap: FlexWrap.Wrap允许内容换行,justifyContent: FlexAlign.Center使内容居中对齐。

  3. 使用 ForEach 循环遍历应用数组,为每个应用创建一个带有背景色的文本标签。

  4. 为结果区域添加半透明背景和上方圆角,使其更加美观。

步骤六:美化界面

最后,我们美化整个界面,添加渐变背景和一些视觉改进。


@Entry@Componentstruct DragGrid {  // 前面的代码保持不变...    build() {    Column() {      // 网格和结果区域代码保持不变...    }    .width('100%')    .height('100%')    .expandSafeArea() // 扩展到安全区域    .linearGradient({ // 渐变背景      angle: 135,      colors: [        ['#121212', 0],        ['#242424', 1]      ]    })  }}
复制代码


在这一步,我们:


  1. 添加expandSafeArea()确保内容可以扩展到设备的安全区域。

  2. 使用linearGradient创建渐变背景,角度为 135 度,从深色(#121212)渐变到稍浅的色调(#242424)。

完整代码

以下是完整的实现代码:


@Entry@Componentstruct 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 是鸿蒙系统中用于创建网格布局的重要组件,它有以下关键属性:


  1. columnsTemplate: 定义网格的列模板。例如'1fr 1fr 1fr 1fr'表示四列等宽布局。'1fr'中的'fr'是 fraction(分数)的缩写,表示按比例分配空间。

  2. rowsTemplate: 定义网格的行模板。如果不设置,行高将根据内容自动调整。

  3. columnsGap: 列之间的间距。

  4. rowsGap: 行之间的间距。

  5. editMode: 是否启用编辑模式。设置为 true 时启用拖拽功能。

  6. supportAnimation: 是否支持动画。设置为 true 时,拖拽过程中会有平滑的动画效果。

拖拽功能的关键事件详解

实现拖拽功能主要依赖以下事件:


  1. onItemDragStart: 当用户开始拖拽某个项时触发。

  2. 参数:event(拖拽事件信息),itemIndex(被拖拽项的索引)

  3. 返回值:被拖拽项的 UI 表示

  4. onItemDrop: 当用户放置拖拽项时触发。

  5. 参数:event(拖拽事件信息),itemIndex(原始位置索引),insertIndex(目标位置索引),isSuccess(是否成功)

  6. 功能:处理元素位置交换逻辑


此外,还有一些可选的事件可以用于增强拖拽体验:


  • onItemDragEnter: 当拖拽项进入某个位置时触发。

  • onItemDragMove: 当拖拽项在网格中移动时触发。

  • onItemDragLeave: 当拖拽项离开某个位置时触发。

小结与进阶提示

通过本教程,我们实现了一个功能完整的可拖拽应用网格界面。主要学习了以下内容:


  1. 使用 Grid 组件创建网格布局

  2. 使用 @Builder 创建可复用的 UI 构建函数

  3. 实现拖拽排序功能

  4. 优化 UI 和用户体验


进阶提示:


  1. 可以添加长按震动反馈,增强交互体验。

  2. 可以实现数据持久化,保存用户的排序结果。

  3. 可以添加编辑模式切换,只有在特定模式下才允许拖拽排序。

  4. 可以为拖拽过程添加更丰富的动画效果,如缩放、阴影等。


希望本教程对你有所帮助,让你掌握鸿蒙系统中 Grid 组件和拖拽功能的使用方法!

发布于: 1 小时前阅读数: 10
用户头像

苏杰豪

关注

鸿蒙很开门~ 2019-03-30 加入

传智教育、黑马程序员课程研究员

评论

发布
暂无评论
鸿蒙特效教程06-可拖拽网格实现教程_鸿蒙_苏杰豪_InfoQ写作社区