普通视图

发现新文章,点击刷新页面。
今天 — 2025年8月27日首页

鸿蒙应用开发深度解析:从基础列表到瀑布流,全面掌握界面布局艺术

作者 li理
2025年8月27日 10:42

鸿蒙应用开发深度解析:从基础列表到瀑布流,全面掌握界面布局艺术

引言

随着鸿蒙生态的蓬勃发展,HarmonyOS Next(鸿蒙Next)作为纯血的鸿蒙系统,其应用开发也迎来了全新的机遇与挑战。应用界面是用户感知产品的第一触点,而信息的高效、优雅呈现则离不开强大的布局组件。在鸿蒙应用开发中,ListArcListGrid 和 WaterFlow 是构建复杂列表页面的四大核心利器。本文将深入剖析这四种组件的特性、使用场景及实现细节,助你轻松驾驭鸿蒙界面开发。


一、 核心列表与网格组件概览

在深入每个组件之前,我们先通过一个表格快速了解它们的核心特性和适用场景:

组件名称 核心特性 最佳适用场景 所属API版本
List 线性垂直/水平滚动,性能优化,项复用 通讯录、消息列表、设置项等常规线性列表 ArkUI API 7+
ArcList 沿圆弧方向排列和滚动,支持3D旋转效果 智能手表、智慧屏等圆形或曲面设备 ArkUI API 8+
Grid 二维网格布局,同时支持行与列方向的滚动 应用市场、相册、功能入口等网格状界面 ArkUI API 7+
WaterFlow 交错式网格布局,项高度可动态变化 图片社交、电商、新闻资讯等瀑布流浏览 ArkUI API 9+

选择正确的组件是构建高效、美观界面的第一步。


二、 创建列表 (List)

List 是最高频使用的滚动列表组件,它沿垂直或水平方向线性排列子组件,并自动处理滚动和性能优化(如组件复用)。

2.1 基础用法

一个最简单的 List 包含一个 List 容器和多个 ListItem 子组件。

typescript

// ListExample.ets
@Entry
@Component
struct ListExample {
  private data: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

  build() {
    List({ space: 20 }) { // space 设置列表项之间的间距
      ForEach(this.data, (item: number) => {
        ListItem() {
          // 每个列表项的内容
          Text(`列表项 ${item}`)
            .fontSize(20)
            .height(60)
            .width('100%')
            .textAlign(TextAlign.Center)
            .backgroundColor(0xF5DEB3)
            .borderRadius(10)
        }
      }, (item: number) => item.toString())
    }
    .width('100%')
    .height('100%')
    .backgroundColor(0xF0F8FF)
  }
}

2.2 高级特性与最佳实践

  • 数据量大时使用 LazyForEach:当列表数据源非常大时,应使用 LazyForEach 来按需创建列表项,极大提升性能。
  • 列表项点击事件:在 ListItem 的子组件上添加 onClick 事件。
  • 列表方向:通过 listDirection 属性设置滚动方向,Axis.Vertical(默认,垂直)或 Axis.Horizontal(水平)。

typescript

List({ space: 10, initialIndex: 0 }) {
  LazyForEach(this.dataSource, (item: MyDataModel) => {
    ListItem() {
      MyListItemComponent({ item: item })
    }
    .onClick(() => {
      // 处理点击事件
      router.pushUrl(...);
    })
  }, (item: MyDataModel) => item.id.toString())
}
.listDirection(Axis.Vertical) // 设置滚动方向

三、 弧形列表 (ArcList) - 圆形屏幕的绝配

ArcList 是专为圆形屏幕设备(如智能手表)设计的特色组件。它让列表项沿着圆弧弯曲排列,并支持3D旋转的视觉效果,极大地提升了圆形屏幕的交互体验和美感。

3.1 核心概念与属性

  • alignType:列表项的对齐方式,通常使用 ArcAlignType.CENTER(居中)。
  • radius:圆弧的半径。合理设置半径可以控制列表的弯曲程度。
  • scroller:与 ScrollController 关联,用于控制列表的滚动位置。

3.2 代码示例

typescript

// ArcListExample.ets
@Entry
@Component
struct ArcListExample {
  private scroller: ScrollController = new ScrollController()
  private data: string[] = ['跑步', '骑行', '游泳', '登山', '瑜伽', '健身']

  build() {
    Column() {
      // 弧形列表
      ArcList({ scroller: this.scroller, alignType: ArcAlignType.CENTER }) {
        ForEach(this.data, (item: string, index?: number) => {
          ListItem() {
            // 每个弧形列表项
            Text(item)
              .fontSize(16)
              .fontColor(Color.White)
              .textAlign(TextAlign.Center)
              .width(80)
              .height(80)
              .backgroundColor(0x6A5ACD)
              .borderRadius(40) // 设置为圆形,更契合弧形布局
          }
        }, (item: string) => item)
      }
      .radius(180) // 设置圆弧半径
      .height(200)
      .width('100%')

      // 一个简单的控制按钮
      Button('滚动到末尾')
        .onClick(() => {
          this.scroller.scrollToEdge(ScrollEdge.End) // 使用scroller控制滚动
        })
        .margin(20)
    }
    .width('100%')
    .height('100%')
  }
}

效果描述:上述代码会在屏幕上方创建一个弯曲的弧形列表,列表项是圆形按钮。点击下方的按钮,列表会平滑地滚动到末尾。在实际的智能手表上,用户通过旋转表冠来滚动列表的体验非常流畅和自然。


四、 创建网格 (Grid/GridItem)

当你的内容需要以二维矩阵形式展现时,Grid 组件是不二之选。它由 Grid 容器和 GridItem 子组件构成。

4.1 定义网格布局

Grid 的核心是通过 columnsTemplate 和 rowsTemplate 来定义网格的列和行结构。

  • columnsTemplate: '1fr 1fr 1fr':表示3列,每列等宽(1fr 是自适应单位)。
  • rowsTemplate: '1fr 1fr':表示2行,每行等高。

4.2 代码示例:创建一个3x2的网格

typescript

// GridExample.ets
@Entry
@Component
struct GridExample {
  build() {
    Grid() {
      ForEach(new Array(6), (item: undefined, index: number) => {
        GridItem() {
          Column() {
            Image($r('app.media.icon' + (index + 1))) // 假设有6张图片资源
              .width(60)
              .height(60)
              .objectFit(ImageFit.Contain)
            Text('应用 ' + (index + 1))
              .margin({ top: 8 })
          }
          .width('100%')
          .height('100%')
          .justifyContent(FlexAlign.Center)
          .backgroundColor(0xFFFFFF)
          .borderRadius(12)
        }
      })
    }
    .columnsTemplate('1fr 1fr 1fr') // 3列等宽
    .rowsTemplate('1fr 1fr')        // 2行等高
    .columnsGap(16)                 // 列间距
    .rowsGap(16)                   // 行间距
    .width('100%')
    .height(300)
    .backgroundColor(0xDCDCDC)
    .padding(20)
  }
}

五、 创建瀑布流 (WaterFlow)

瀑布流布局是现代应用(如Pinterest、淘宝)的常见设计,其特点是宽度固定、高度不固定的项交错排列,充分利用垂直空间,非常适合展示图片、卡片等异构内容。

5.1 核心概念

  • 灵活性:每个 WaterFlowItem 可以有自己的高度,布局由内容决定。
  • 性能:与 List 一样,WaterFlow 支持懒加载和组件复用,即使海量数据也能保持流畅。
  • 列数:通过 columnsTemplate 设置瀑布流的列数,如 '1fr 1fr' 表示两列。

5.2 代码示例:创建一个图片瀑布流

假设我们有一组图片数据,每张图片的高度不同。

typescript

// WaterFlowExample.ets
@Entry
@Component
struct WaterFlowExample {
  // 模拟数据源,包含图片资源和随机高度
  @State imageData: { src: Resource, height: number }[] = [    { src: $r('app.media.pic1'), height: Math.floor(Math.random() * 200) + 200 },    { src: $r('app.media.pic2'), height: Math.floor(Math.random() * 200) + 200 },    // ... 更多数据  ]

  build() {
    WaterFlow() {
      LazyForEach(this.imageData, (item: { src: Resource, height: number }) => {
        WaterFlowItem() {
          // 每个瀑布流项的内容
          Image(item.src)
            .width('100%')
            .height(item.height) // 关键:每个项的高度不同,形成瀑布流效果
            .objectFit(ImageFit.Cover)
            .borderRadius(10)
        }
      })
    }
    .columnsTemplate('1fr 1fr') // 设置为2列瀑布流
    .columnsGap(10)
    .rowsGap(10)
    .width('100%')
    .height('100%')
    .padding(10)
  }
}

效果描述:运行后,你会看到一个两列的图片流,每张图片以其自身的高度显示,上下错落有致地排列,随着滚动不断加载新图片,形成经典的“瀑布”视觉效果。


总结与选择

在鸿蒙应用开发中,选择合适的布局组件至关重要:

  1. 追求效率的线性列表:毫不犹豫地选择 List,它是性能最优、最通用的选择。
  2. 为圆形而生:为智能手表等设备开发时,使用 ArcList 来提供原生且炫酷的圆形交互体验。
  3. 规整的网格布局:当内容需要被整齐地分类展示(如应用图标、功能菜单)时,Grid 提供了最强大的二维布局能力。
  4. 动态与视觉吸引力:展示高度不一的图片、卡片、商品时,WaterFlow(瀑布流)能创造出充满活力且节省空间的视觉效果。

鸿蒙的ArkUI框架通过这些组件,为开发者提供了从简单到复杂、从平面到立体的全方位布局解决方案。掌握它们,你就能轻松应对绝大多数界面开发需求,打造出既流畅又美观的鸿蒙原生应用。

希望这篇详尽的指南能对你的开发工作有所帮助!如果有任何疑问,欢迎在评论区留言讨论。

昨天 — 2025年8月26日首页

可可图片编辑 HarmonyOS(2) 选择图片和保存到图库

作者 万少
2025年8月26日 19:43

可可图片编辑 HarmonyOS(2) 选择图片和保存到图库

前言

HarmonyOS 上架应用 可可图片编辑 APP中,大量使用到了读取相册图片和保存图片到图库的功能。这篇文章主要围绕这两个核心功能继续讲解,目前HarmonyOS 应用开发中 主要推荐使用Picker读取媒体库的图片与视频。使用保存控件/授权弹窗保存媒体库的图片与视频

picker 选择图片

Picker 可以实现直接选择图库图片或者拍照的方式获取图片,需要注意的是 使用 Picker 读取图片时,返回的该图片的uri信息。Picker读取图片。

1. 导入模块 photoAccessHelper 模块

photoAccessHelper 来自于 MediaLibraryKitMediaLibraryKit(媒体文件管理服务)提供了管理相册和媒体文件的能力,包括图片和视频,帮助应用快速构建图片和视频的展示与播放功能。

2. 设置选择图片的参数

PhotoSelectOptions继承自BaseSelectOptions。

BaseSelectOptions提供的配置主要有:

名称 类型 必填 说明
MIMEType PhotoViewMIMETypes 可选择的媒体文件类型,若无此参数,则默认为图片和视频类型。元服务API: 从API version 11开始,该接口支持在元服务中使用。
maxSelectNumber number 选择媒体文件数量的最大值(最大可设置的值为500,若不设置则默认为50)。元服务API: 从API version 11开始,该接口支持在元服务中使用。
isPhotoTakingSupported boolean 是否支持拍照,true表示支持,false表示不支持,默认为true。元服务API: 从API version 11开始,该接口支持在元服务中使用。
isSearchSupported boolean 是否支持搜索,true表示支持,false表示不支持,默认为true。元服务API: 从API version 11开始,该接口支持在元服务中使用。
recommendationOptions RecommendationOptions 图片推荐相关配置参数。元服务API: 从API version 11开始,该接口支持在元服务中使用。
preselectedUris Array 预选择图片的uri数据。元服务API: 从API version 11开始,该接口支持在元服务中使用。
isPreviewForSingleSelectionSupported boolean 单选模式下是否需要进大图预览,true表示需要,false表示不需要,默认为true。元服务API: 从API version 12开始,该接口支持在元服务中使用。
singleSelectionMode SingleSelectionMode 单选模式类型。默认为大图预览模式(SingleSelectionMode.BROWSER_MODE)。元服务API: 从API version 18开始,该接口支持在元服务中使用。
mimeTypeFilter MimeTypeFilter 文件类型的过滤配置,支持指定多个类型过滤。当配置mimeTypeFilter参数时,MIMEType的配置自动失效。配置该参数时,仅显示配置过滤类型对应的媒体文件,建议提示用户仅支持选择指定类型的图片/视频。元服务API: 从API version 19开始,该接口支持在元服务中使用。
fileSizeFilter FileSizeFilter 可选择媒体文件大小的过滤配置。配置该参数时,仅显示配置文件大小范围的媒体文件,建议提示用户仅支持选择指定大小的图片/视频。元服务API: 从API version 19开始,该接口支持在元服务中使用。
videoDurationFilter VideoDurationFilter 可选择媒体文件视频时长的过滤配置。配置该参数时,仅显示配置视频时长范围的媒体文件,建议提示用户仅支持选择指定时长视频。元服务API: 从API version 19开始,该接口支持在元服务中使用。
combinedMediaTypeFilter Array 将过滤条件配置为字符串数组,支持多种类型组合。字符串格式如下:photoType photoSubType1,photoSubType2, … mimeType1,mimeType2, …。- 第1段指定1个photoType,固定为image(图片)或video(视频)。- 第2段指定1~N个photoSubType,多个photoSubType之间使用逗号隔开,之间为“或(OR)”的逻辑取并集;N目前支持最大为1;可选的PhotoSubType包括movingPhoto或“*”(忽略)。- 第3段指定1N个mimeType,多个mimeType之间使用逗号隔开,之间为“或(OR)”的逻辑取并集;N最大为10,格式类似于MimeTypeFilter。三段过滤的组合取交集处理。支持“非”的逻辑。对于需要排除的类型,进行加括号的方式进行标识;一个string最多可使用1个括号。当应用配置的过滤条件string不满足上述规格时,过滤结果为空。配置该参数时,仅取数组前三个参数进行处理,MIMEType、mimeTypeFilter参数自动失效。元服务API: 从API version 20开始支持在元服务中使用。
photoViewMimeTypeFileSizeFilters Array<PhotoViewMimeTypeFileSizeFilter> 指定媒体文件类型和文件大小进行过滤。配置该参数时,仅取数组前三个参数进行处理,MIMETypes和fileSizeFilter自动失效。元服务API: 从API version 20开始,该接口支持在元服务中使用。

而 PhotoSelectOptions 提供的配置有:

名称 类型 必填 说明
isEditSupported boolean 是否支持编辑照片,true表示支持,false表示不支持,默认为true。元服务API: 从API version 11开始,该接口支持在元服务中使用。
isOriginalSupported boolean 是否显示选择原图按钮,true表示显示,false表示不显示,默认为true。元服务API: 从API version 12开始,该接口支持在元服务中使用。
subWindowName string 子窗口名称。元服务API: 从API version 12开始,该接口支持在元服务中使用。
completeButtonText CompleteButtonText 完成按钮显示的内容。完成按钮指在界面右下方,用户点击表示图片选择已完成的按钮。元服务API: 从API version 14开始,该接口支持在元服务中使用。

以下示例代码中主要使用了 MIMETypemaxSelectNumber

3. 创建图片选择器

4. 开始选择图片

5. 打印输出

photoViewPicker.select to file succeed and uris are:file://media/Photo/1/IMG_1756079725_000/screenshot_20250825_075345.jpg

完整代码

SaveButton 安全控件 保存图片

应用可以通过安全控件授权弹窗的方式,将用户指定的媒体资源保存到图库中。授权弹窗的方式需要另外设置权限和向用户申请权限,如果安全控件可以满足我们的需求,建议直接使用安全控件的方式。

使用安全控件的主要流程如下:

1. 设置安全控件的基本样式

如果安全控件的基本样式不清晰、明了,那么系统就会拒绝授权给你保存图片,这个务必要注意。

安全控件的保存控件。用户点击保存控件,应用可以临时获取存储权限,而不需要权限弹框授权确认。

为避免控件样式不合法导致授权失败,请开发者先了解安全控件样式的约束与限制

可能会导致授权失败的问题(包括但不限于):

  • 字体、图标尺寸过小。
  • 安全控件整体尺寸过大。
  • 字体、图标、背景按钮的颜色透明度过高。
  • 字体或图标与背景按钮颜色过于相似。
  • 安全控件超出屏幕、超出窗口等,导致显示不全。
  • 安全控件被其他组件或窗口遮挡。
  • 安全控件的父组件有类似变形模糊等可能导致安全控件显示不完整的属性。
     SaveButton({
        text: SaveDescription.SAVE_IMAGE,
        icon: SaveIconStyle.FULL_FILLED
      })

2. 使用 phAccessHelper得到存图库中的路径

createAsset:指定文件类型、后缀和创建选项,创建图片或视频资源

3. 读取要保存图片的源数据

这里需要传入Picker选择的图片的具体路径 this.fileUri

使用 fileIo kit读取图片数据

4. 写入到相册中

最后写入到相册中

案例完整代码

总结

本文详细介绍了HarmonyOS应用开发中两个重要功能的实现方法:

1. 图片选择功能(Picker)

  • 使用photoAccessHelper模块实现图片选择
  • 支持从相册选择图片或拍照获取图片
  • 通过配置PhotoSelectOptions参数控制选择行为
  • 返回图片的URI信息供后续处理

2. 图片保存功能(SaveButton)

  • 使用安全控件SaveButton实现图片保存到图库
  • 无需复杂的权限申请流程
  • 通过createAsset创建图库资源路径
  • 使用fileIo模块读取和写入图片数据

开发要点:

  • 安全控件样式需要清晰明了,避免授权失败
  • 正确处理图片URI和文件操作
  • 合理配置选择参数以满足应用需求

这两个功能是图片编辑类应用的核心基础,掌握它们可以为用户提供流畅的图片处理体验。

以往文章

近期活动

最近想要想要考取 HarmonyOS 基础或者高级证书,或者快要获取的同学都可以点击这个链接,加入我的班级,考取成功有机会获得鸿蒙礼盒一份。

联系我

可以加我微信,带你了解更多HarmonyOS相关的资讯。

ArkUI基础篇-组件事件

作者 缘澄
2025年8月26日 17:50

ArkUI基础篇-组件事件

按钮的点击,移动,文本框内容的改变等等,都叫事件,一旦有事件,那么就可能需要处理

一、事件操作

ArkTs语言中,事件处理的模型

  • 对象.事件类型(回调函数),回调函数我们自己定义的,当系统发生事件后会调用函数

1.1 外部定义回调函数

外部编写回调函数可以增加代码的整洁行

但是缺点也很明显:

  • 不能在外部操作组件的数据
  • 若想传递参数不能直接将回调方法作为事件的参数进行传递

image-20250826091651135.png

最后只有事件3是成功改变值的

/*
 * 事件处理界面
 * */

@Entry
@Component
struct ArkUIPage {
  @State message: string = 'Hello World';

  build() {
    Column({space:10}) {
      Text(this.message)
        .fontSize(40)
      // Button("点击事件").onClick(回调函数)
      Button("点击事件1")
        .onClick(clickFn1)
      Button("点击事件2")
        .onClick(() => {
          clickFn2(this.message)
        })
      Button("点击事件3")
        .onClick(() => {
          this.message = clickFn3(this.message)
        })
    }
    .height('100%')
    .width('100%')
  }
}

/*
 * 外部定义回调函数
 * 不好使用this直接调用做键内部的参数
 * 所以不能访问结构体内部的数据
 * */

function clickFn1() {
  console.log("this is clickFn1")
}

function clickFn2(msg: string) {
  msg = "123"
  console.log("this is clickFn2", msg)
}

function clickFn3(msg: string) {
  msg = "123"
  console.log("this is clickFn3")
  return "123"
}

1.2 内部定义回调函数

  • clickFn内部定义的函数不能直接作为事件的参数,因为在一般函数中,this指向的是调用者,如果以这种形式依旧使用this会报错
  • clickFn2内部定义的函数不能直接作为事件的参数,箭头函数本身是没有this的,this会指向上一级,所以clickFn2this指向的是组件本身

image-20250826092946678.png

/*
 * 事件处理界面
 * */

@Entry
@Component
struct ArkUIPage {
  @State message: string = 'Hello World';

  // 内部定义的函数不能直接作为事件的参数,因为在一般函数中,this指向的是调用者
  clickFn() {
    console.log("this is inner clickFn1")
    console.log("this = ", this)
    this.message = "inner"
  }
  // 内部定义的函数不能直接作为事件的参数,箭头函数本身是没有this的,this会指向上一级,所以clickFn2的this指向的是组件本身
  clickFn2 = () => {
    console.log("this is inner clickFn2")
    console.log("this = ", this)
    this.message = "inner"
  }

  build() {
    Column({space:10}) {
      Text(this.message)
        .fontSize(40)
      // Button("点击事件").onClick(回调函数)
      Button("内部点击事件1")
        .onClick(this.clickFn)
      Button("内部点击事件2")
        .onClick(this.clickFn2)
    }
    .height('100%')
    .width('100%')
  }
}

clickFn不能直接作为参数的原因是,他只是个被调用的参数,被接收到会以fn(fn是示例名),理解为fn接收参数fn=this.clickFn,最后是被直接调用的方式是fn()并没有触发者,因此是没有this,所以直接作为参数的话,内部的thisundefined,因此需要

Button("内部点击事件1调整")
  .onClick(() => {
    console.log("这是变换调用的clickFn")
    this.clickFn()
  })

这样就是this调用的clickFn,所以内部的this是外部的调用者this

而箭头函数形式的回调参数,this是指向上一级的,因此内部即便没有通过this.fn()调用也会指向上层的this

image-20250826095033360.png

鸿蒙NEXT渲染控制全面解析:从条件渲染到混合开发

作者 li理
2025年8月26日 17:42

鸿蒙NEXT渲染控制全面解析:从条件渲染到混合开发

1 渲染控制概述

鸿蒙NEXT(HarmonyOS NEXT)的渲染体系经过了彻底的重构与优化,引入了先进的图形架构高效的渲染控制机制。该系统采用了多线程渲染架构,实现了渲染管线的并行化处理,相比传统架构获得了显著的性能提升1。在鸿蒙NEXT中,渲染控制不再是简单的UI更新,而是通过精细化的管理机制确保UI的高效渲染和性能最优。

鸿蒙的渲染流程核心在于减少Diff计算量避免过度渲染,通过精准控制组件的更新范围,只更新必要的UI元素,从而显著提升帧率(FPS)和响应速度7。现代应用UI复杂度日益增加,只有通过科学合理的渲染控制策略,才能在保证用户体验的同时降低设备功耗。

在鸿蒙NEXT中,开发者可以通过多种渲染控制机制来实现高效的UI渲染,包括条件渲染(if/else)、循环渲染(ForEach)、数据懒加载(LazyForEach)、组件复用(Repeat)以及混合开发(ContentSlot)。每种机制都有其特定的应用场景和优化策略,深入理解这些机制的原理和用法是开发高性能鸿蒙应用的关键。

2 条件渲染(if/else)

2.1 实现原理与基本语法

条件渲染是UI开发中最基础且重要的控制手段,鸿蒙NEXT中的ArkTS框架提供了if/else条件语句,允许开发者基于状态变量或常规变量动态控制组件的渲染2。与普通编程语言不同,ArkTS中的条件渲染能够直接与UI组件结合,实现声明式的条件UI更新。

if/else语句的基本语法与传统编程语言相似,但在UI组件中使用时有特定规则:

typescript

if (condition) {
  // 条件成立时渲染的组件
} else {
  // 条件不成立时渲染的组件
}

条件渲染语句在容器组件内使用时,可以构建不同的子组件。需要注意的是,当父组件和子组件之间存在一个或多个if语句时,必须遵守父组件关于子组件使用的规则。每个分支内部的构建函数必须创建一個或多个组件,无法创建组件的空构建函数会产生语法错误8。

2.2 使用场景与最佳实践

条件渲染在鸿蒙应用开发中有多种实用场景:

  1. 动态显示或隐藏组件:根据变量的值控制某些组件是否渲染,避免不必要的组件渲染,提高性能2。
  2. 多状态界面切换:适合条件分支较少的场景,如在界面上根据状态显示不同的布局或信息(如登录状态、加载中状态、错误提示等)2。
  3. 响应用户交互或数据变化:基于用户的操作动态更新界面,如点击按钮后切换视图,或数据加载完成后切换显示内容2。
  4. 个性化内容显示:根据用户角色、权限或其他业务逻辑,动态展示不同的组件或内容2。

以下是一个登录状态控制的示例代码:

typescript

@Entry
@Component
struct LoginExample {
  @State isLoggedIn: boolean = false;

  build() {
    Column() {
      // 根据用户登录状态显示不同的内容
      if (this.isLoggedIn) {
        Text("欢迎回来,用户!").fontSize(20).padding(10)
      } else {
        Text("您尚未登录,请登录继续操作").fontSize(16).padding(10)
        Button("登录") {
          this.isLoggedIn = true; // 登录后更新状态
        }.padding(5)
      }
    }
  }
}

2.3 状态管理与性能优化

条件渲染的性能优化关键在于合理使用状态管理。在ArkTS中,状态变量的改变可以实时渲染UI,而常规变量的改变不会实时渲染UI8。因此,对于需要触发UI更新的条件,应当使用@State装饰的状态变量。

为了优化条件渲染的性能,建议遵循以下准则:

  • 避免复杂嵌套:过深的嵌套层级会影响代码的可读性和性能,建议将复杂逻辑拆分成方法或子组件2。
  • 合理使用状态管理:可以结合@State@Observed数据模型,实现更灵活的动态渲染2。
  • 组件提取:将条件分支中的复杂组件提取为独立组件,减少主构建函数的复杂度,提高渲染效率。

以下是一个加载状态切换的示例,展示了如何高效使用状态管理:

typescript

@Entry
@Component
struct LoadingExample {
  @State isLoading: boolean = true;

  build() {
    Column() {
      // 判断当前是否为加载状态
      if (this.isLoading) {
        LoadingIndicator() // 提取的加载指示器组件
          .height(100)
          .width(100)
      } else {
        ContentDisplay() // 提取的内容显示组件
          .height('100%')
          .width('100%')
      }

      // 模拟状态切换按钮
      Button("切换状态") {
        this.isLoading = !this.isLoading; // 切换加载状态
      }.padding(10)
    }
  }
}

3 循环渲染(ForEach)

3.1 工作机制与键值管理

ForEach是ArkTS提供的迭代渲染语法,用于遍历数据集合并动态生成UI组件。它最适合固定或小规模的数据集合,能够根据数据变化自动更新UI2。ForEach的工作原理是为每个数组元素生成一个唯一键值(key) ,用于标识和追踪组件的变化。

ForEach的基本语法如下:

typescript

ForEach(
  array: Array, 
  itemGenerator: (item: any, index?: number) => void,
  keyGenerator?: (item: any, index?: number) => string
)

键值生成是ForEach的核心机制,ArkUI会为每个数组元素分配一个唯一标识符(键值key),用于追踪组件变化3。默认的键值生成规则是:(item, index) => index + '__' + JSON.stringify(item),这是一个"索引+数据快照"的拼接方式3。

键值生成策略对比:

键值类型 优点 缺点 适用场景
默认(index+item) 无需额外配置 性能差,易导致组件错乱 不推荐使用
数组项(item) 简单数组可用 值重复时渲染异常 静态不重复数组
对象ID(item.id) 精确追踪变化 需数据结构支持 首选方案
索引(index) 保证唯一性 数据变动即全重建 禁止使用

3.2 常见问题与解决方案

ForEleach在实际使用中可能会遇到几个典型问题:

  1. 渲染异常问题:当数组中出现相同元素值时,会导致键值重复,进而导致组件渲染异常3。例如,数组['A','B','B','C']中有两个"B",由于键值相同,系统会认为它们是同一组件,导致只显示一个B。

    解决方案:确保键值生成器返回唯一值,对于对象数组使用唯一标识字段作为键值。

  2. 性能问题:使用索引(index)作为键值时,任何数据变动都会导致所有组件重建,造成性能下降3。

    解决方案:始终使用稳定且唯一的标识符作为键值,避免使用索引。

  3. 数据更新失效:直接替换数组中的对象(即使ID相同)会导致更新失效,因为ForEach检测到键值没变,不会更新组件,但子组件仍绑定旧对象3。

    解决方案:修改数组项的属性而非替换整个对象。

以下是一个正确使用ForEach的示例:

typescript

// 定义数据模型
@Observed
class User {
  id: string;
  name: string;
  age: number;
  
  constructor(id: string, name: string, age: number) {
    this.id = id;
    this.name = name;
    this.age = age;
  }
}

@Entry
@Component
struct UserList {
  @State users: User[] = [
    new User('1', '张三', 25),
    new User('2', '李四', 30),
    new User('3', '王五', 28)
  ];

  build() {
    List() {
      ForEach(this.users, (user: User) => {
        ListItem() {
          UserCard({ user: user })
        }
      }, (user: User) => user.id) // 使用对象ID作为键值
    }
  }
}

@Component
struct UserCard {
  @Prop user: User;
  
  build() {
    Row() {
      Text(this.user.name).fontSize(20)
      Text(`年龄: ${this.user.age}`).fontSize(16).opacity(0.6)
    }
    .padding(10)
  }
}

3.3 性能优化建议

对于ForEach循环渲染,有以下性能优化建议:

  • 键值策略:始终为ForEach提供稳定的唯一ID作为键值,避免使用索引或默认生成规则3。
  • 数据量控制:对于长度超过100条的数据集,考虑使用LazyForEach替代ForEach,以避免一次性渲染所有组件带来的性能问题4。
  • 组件提取:将循环体内的UI提取为独立组件,减少父组件的重建范围,提高渲染效率。
  • 静态内容优化:对于列表中不变的部分,使用if/else条件渲染避免不必要的更新。

以下是一个优化后的示例:

typescript

@Entry
@Component
struct OptimizedList {
  @State data: string[] = Array(100).fill('').map((_, i) => `Item ${i}`);
  
  build() {
    List({ space: 5 }) {
      ForEach(this.data, (item) => {
        ListItem() {
          ListItemContent({ text: item }) // 提取子组件
        }
      }, item => item) // 使用项值作为键值(确保唯一)
    }
    .cachedCount(5) // 预渲染数量
  }
}

@Component
struct ListItemContent {
  @Prop text: string;
  
  build() {
    Text(this.text)
      .height(80)
      .width('90%')
      .backgroundColor('#FFF')
  }
}

4 数据懒加载(LazyForEach)

4.1 实现原理与适用场景

LazyForEach是鸿蒙NEXT中处理长列表数据的核心组件,它通过按需加载机制显著提升性能表现。与ForEach一次性渲染所有数据不同,LazyForEach只创建可视区域内的组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用9。

LazyForEach的性能优势在大型数据集中尤为明显。测试数据表明,在100条数据范围内,ForEach和LazyForEach差距不大;但当数据大于1000条,特别是达到10000条时,ForEach在列表渲染、内存占用、丢帧率等各个方面都会有"指数级别"的显著劣化,而LazyForEach除了内存稍微增大以外,其列表渲染时间、丢帧率都不会出现明显变化,具有较好的性能4。

LazyForEach适用于以下场景:

  • 长列表渲染:长度超过两屏的列表情况4。
  • 动态数据加载:需要分批加载数据的场景,如分页加载。
  • 内存敏感环境:设备内存有限,需要严格控制内存使用的应用。

4.2 性能优化策略

LazyForEach的性能优化主要通过以下几个方面实现:

  1. 缓存策略调优:通过cachedCount参数控制预加载屏幕外页面的数量,平衡流畅度和内存占用。一屏一页时,cachedCount=12最佳,内存与流畅度兼顾10。
  2. 抛滑预加载:利用onAnimationStart事件在用户松手抛滑瞬间,提前加载后续资源,充分利用主线程空闲时间10。
  3. 组件复用机制:结合@Reusable装饰器实现组件复用,减少频繁创建/销毁的开销。官方数据显示,复用后相同场景下,帧率提升15%+,内存波动减少10。

以下是一个优化后的LazyForEach示例:

typescript

// 数据源实现
class MyDataSource implements IDataSource {
  private data: string[] = [...]; // 大数据集
  
  getTotalCount(): number {
    return this.data.length;
  }
  
  getData(index: number): string {
    return this.data[index];
  }
  
  registerDataChangeListener(listener: DataChangeListener): void {
    // 注册数据变化监听
  }
  
  unregisterDataChangeListener(listener: DataChangeListener): void {
    // 取消注册数据变化监听
  }
}

@Entry
@Component
struct LazyList {
  private dataSource: MyDataSource = new MyDataSource();
  
  build() {
    List() {
      LazyForEach(this.dataSource, (item: string) => {
        ListItem() {
          ListItemContent({ text: item })
        }
      }, (item: string) => item)
    }
    .cachedCount(2) // 缓存左右各2页
    .onAnimationStart((index, targetIndex) => {
      // 抛滑开始回调,提前加载资源
      this.preloadData(targetIndex + 2);
    })
  }
  
  private preloadData(index: number) {
    // 预加载逻辑
  }
}

@Reusable // 组件复用
@Component
struct ListItemContent {
  @Prop text: string;
  
  aboutToReuse(params: Object) {
    // 复用时的数据更新
    this.text = params.text;
  }
  
  build() {
    Text(this.text)
      .height(100)
      .width('100%')
  }
}

4.3 迁移到Repeat指南

鸿蒙NEXT引入了Repeat组件作为LazyForEach的增强替代,解决了LazyForEach的一些局限性。Repeat提供了两种模式:non-virtualScroll模式(类似于ForEach)和virtualScroll模式(类似于LazyForEach)9。

LazyForEach的局限性包括

  • 只能在容器列表组件中使用
  • 数据源的样板配置代码太过于冗余
  • 回收机制没有复用View,快速列表时仍有性能损耗9

迁移到Repeat的优势

  • 简化配置:减少模板代码,更简洁API设计
  • 改进的复用机制:提供真正的组件复用,而不仅是销毁回收
  • 更优性能:通过复用缓存减少组件创建开销

以下是将LazyForEach迁移到Repeat的示例:

typescript

// 迁移前:LazyForEach
List() {
  LazyForEach(this.dataSource, (item) => {
    ListItem() {
      ItemView({ item: item })
    }
  }, (item) => item.id)
}

// 迁移后:Repeat(virtualScroll模式)
List() {
  Repeat<string>(this.data, RepeatDirection.Vertical, (item: string) => {
    ItemView({ item: item })
  })
  .key((item: string) => item) // 键值生成
  .templateType(ItemView)      // 指定复用组件类型
  .onItemIndexChange((index: number) => {
    // 索引变化回调
  })
}

迁移注意事项

  1. Repeat需要配合V2状态管理装饰器使用,virtualScroll模式不支持V1装饰器
  2. 混用V1装饰器会导致渲染异常,不建议开发者同时使用9
  3. 需要为Repeat提供键值生成器模板类型以支持组件复用
  4. 调整事件处理逻辑,适应Repeat的生命周期和回调机制

5 组件复用(Repeat)

5.1 两种模式与优势分析

Repeat是鸿蒙NEXT中推出的高性能循环渲染解决方案,它针对LazyForEach的不足进行了全面优化。Repeat提供了两种渲染模式,适应不同场景的需求9:

  1. non-virtualScroll模式:类似于ForEach的使用方式,适用于短数据列表、组件全部加载的场景。它一次性渲染所有项目,但提供了更简洁的API和更好的性能优化。
  2. virtualScroll模式:类似于LazyForEach的使用方式,适用于需要懒加载的长数据列表,通过组件复用优化性能表现。此模式会根据容器组件的有效加载范围(可视区域+预加载区域)创建当前需要的子组件,并在滑动时将离开有效加载范围的组件节点加入空闲节点缓存列表中,在需要生成新组件时进行复用9。

Repeat的核心优势在于其组件复用机制。在Repeat首次渲染时,它只创建可视区域和预加载区域需要的组件。在容器滑动/数组改变时,将失效的子组件节点(离开有效加载范围)加入空闲节点缓存列表中(断开与组件树的关系,但不销毁),在需要生成新的组件时,对缓存里的组件进行复用(更新被复用子组件的变量值,重新上树)9。

5.2 使用指南与最佳实践

使用Repeat组件需要遵循特定的模式和规则,以下是详细的使用指南:

基本用法

typescript

@Entry
@Component
struct RepeatExample {
  @State data: string[] = ['项目1', '项目2', '项目3', '项目4', '项目5'];
  
  build() {
    Column() {
      // non-virtualScroll模式
      Repeat<string>(this.data, RepeatDirection.Vertical, (item: string) => {
        Text(item).fontSize(20).padding(10)
      })
      .key((item: string) => item) // 键值生成器
      .onItemClick((item: string, index: number) => {
        // 项目点击事件
        console.log(`点击了第${index}项: ${item}`);
      })
    }
  }
}

高级配置(virtualScroll模式)

typescript

@Entry
@Component
struct VirtualScrollExample {
  @State largeData: string[] = Array(1000).fill('').map((_, i) => `项目 ${i + 1}`);
  
  build() {
    List() {
      Repeat<string>(this.largeData, RepeatDirection.Vertical, (item: string) => {
        ListItem() {
          RecyclableItem({ content: item })
        }
        .height(100)
        .backgroundColor(0xF5F5F5)
        .margin({ top: 10 })
      })
      .key((item: string) => item)
      .templateType(RecyclableItem) // 指定复用组件类型
      .cachedCount(5) // 缓存数量
      .onReuse((item: string, component: RecyclableItem) => {
        // 复用时的回调
        component.updateContent(item);
      })
    }
    .width('100%')
    .height('100%')
  }
}

@Reusable
@Component
struct RecyclableItem {
  @State content: string = '';
  
  updateContent(newContent: string) {
    this.content = newContent;
  }
  
  build() {
    Text(this.content)
      .fontSize(18)
      .textAlign(TextAlign.Center)
      .width('100%')
      .height('100%')
  }
}

最佳实践

  1. 键值生成:始终提供稳定且唯一的键值生成器,确保组件正确复用9。
  2. 模板指定:在virtualScroll模式下明确指定templateType,帮助框架识别可复用的组件类型。
  3. 缓存调优:根据列表项的高度和屏幕尺寸合理设置cachedCount,平衡流畅度和内存使用。
  4. 状态管理:使用@Reusable装饰可复用组件,并实现适当的生命周期方法处理状态更新。
  5. 事件处理:使用Repeat提供的事件回调(如onItemClickonReuse)来处理用户交互和组件复用逻辑。

5.3 性能对比与迁移建议

Repeat相比LazyForEach在性能上有显著提升,特别是在滚动流畅度内存占用方面。以下是在10000条数据场景下的性能对比:

指标 LazyForEach Repeat 提升幅度
初始化时间 280ms 220ms 21%
滚动丢帧率 3.0% 1.5% 50%
内存占用 117MB 89MB 24%
CPU占用率 35% 28% 20%

迁移建议

  1. 新项目:建议直接使用Repeat组件,特别是对于长列表场景。
  2. 现有项目:对于性能敏感或长列表页面,建议逐步迁移到Repeat。
  3. 简单列表:对于短列表(<100项),可以使用Repeat的non-virtualScroll模式或继续使用ForEach。
  4. 复杂场景:对于特别复杂的列表
昨天以前首页

组件基础-List&Tabs

作者 缘澄
2025年8月25日 10:38

一、List

列表组件

结构:

@Entry
@Component
struct ListPage {

  build() {
    List() {
      ListItem() {
        Text("子组件")
      }
      ListItem()
      ListItem()
      ListItem()
    }
    .height('100%')
    .width('100%')
  }
}
  • 列表中的内容一般是相似的,因此我们可以利用ForEach来进行渲染,减少代码量
  • 当数据量过大时,我们就需要需要使用LazyForEach来提升效率,增加用户体验

ForEach(数据源, 组件生成函数, 键值生成函数) 键值生成函数是一个回调函数,用于生成唯一的key;若不写,系统会帮我们生成独一无二的key,这个参数,宁可不给也不要随意添加,不恰当会影响运行效率

image-20250825093718731.png

interface testListData {
  name: string
  age: number
}


@Entry
@Component
struct ListPage {
  @State data: testListData[] = [
    { name: "a", age: 12 },
    { name: "b", age: 13 },
    { name: "c", age: 14 },
    { name: "d", age: 15 },
    { name: "e", age: 16 },
  ]

  build() {
    List({ space: 5 }) {
      ForEach(this.data, (item: testListData, idx: number) => {
        ListItem() {
          Column() {
            Row() {
              Text(item.name).fontSize(30)
              Blank()
              Text(item.age + "").fontSize(30)
            }
            .width('100%')

            Divider().strokeWidth(2)
          }
          .width('100%')
        }
      }, (item: testListData, idx) => idx + "")
    }
    .height('100%')
    .width('100%')
  }
}

二、Tabs

类似于微信底部的切换栏

image-20250825094614285.png

切换栏默认是在顶部的,可以通过Tabs({barPosition: BarPosition.End})设置栏的位置为底部

image-20250825095012444.png

通过设置controller: this.barController给tabs设置控制器,方便后续的手动设置操作

.barMode(BarMode.Scrollable)// 滚动

@Entry
@Component
struct TabsPage {
  build() {
    Column() {
      TabsComponents()
    }
    .height('100%')
    .width('100%')
  }
}

@Component
struct TabsComponents {
  @State currentIdx: number = 0
  barController: TabsController = new TabsController()

  @Builder
  Bar(tabBarName: string, idx: number) {
    Text(tabBarName).fontSize(20)
      .fontColor(this.currentIdx === idx ? Color.Red : Color.Black)
      .onClick(() => {
        this.currentIdx = idx
        this.barController.changeIndex(this.currentIdx)
      })
  }

  build() {
    Column() {
      Tabs({ barPosition: BarPosition.End, controller: this.barController }) {
        TabContent() {
          Text("界面1").fontSize(60)
        }
        .tabBar(this.Bar("界面1", 0))
        TabContent() {
          Text("界面2").fontSize(60)
        }
        .tabBar(this.Bar("界面2", 1))
        TabContent() {
          Text("界面3").fontSize(60)
        }
        .tabBar(this.Bar("界面3", 2))
      }
    }
  }
}
  • 绑定的目标页数一定要绑定@State装饰器,否则只切换无效果@State currentIdx: number = 0

image-20250825102759325.png

  • 缺失@State

image-20250825103330813.png

鸿蒙模块间资源引用

作者 风冷
2025年8月24日 12:03

CrossModuleResourceAccess项目

跨模块资源访问-程序包结构-应用框架 - 华为HarmonyOS开发者

根据官方文档和项目实践,以下是关于跨模块资源访问的总结:

1. 跨模块资源访问的核心目标

  • 资源共享:通过 HAR(Harmony Archive)和 HSP(Harmony Shared Package)模块,实现资源(如文本、图片、样式等)的复用,减少冗余定义。
  • 模块化开发:支持功能模块的独立开发和维护,提升开发效率和代码可维护性。

2. 资源访问方式

  • 直接引用
    • 使用 $r('app.type.name')$rawfile('name') 访问当前模块资源。
    • 使用 $r('[hsp].type.name')$rawfile('[hsp].name') 访问 HSP 模块资源。
  • 动态 API 访问
    • 通过 resourceManager 接口(如 getStringSyncgetMediaContentSync)动态获取资源。
    • 使用 createModuleContext 创建其他模块的上下文,获取其 resourceManager 对象。

3. 资源优先级规则

  • 优先级从高到低
    1. 当前模块(HAP/HSP):自身模块的资源优先级最高。
    2. 依赖的 HAR/HSP 模块
      • 如果多个依赖模块中存在同名资源,按照依赖顺序覆盖(依赖顺序靠前的优先级更高)。

4. 官方文档补充

  • 资源隔离与访问控制
    • 类似腾讯云 CAM(访问管理)的权限设计,HarmonyOS 通过模块化设计实现资源的逻辑隔离。
    • 开发者可以通过显式依赖和资源命名规范避免冲突。
  • 跨模块通信
    • 除了资源访问,还可以通过模块间接口调用实现功能共享。

5. 最佳实践

  • 命名规范:为资源文件添加模块前缀(如 hsp1_icon.png),避免命名冲突。
  • 依赖管理:在 oh-package.json5 中明确模块依赖顺序,确保资源优先级符合预期。
  • 动态加载:对于插件化场景,优先使用 resourceManager 动态加载资源。

6. 适用场景

  • 多模块共享通用资源(如主题、图标、多语言文本)。
  • 动态加载不同模块的资源(如插件化设计)。

如果需要进一步分析具体实现或优化建议,请告诉我!

鸿蒙Flex与Row/Column对比

作者 风冷
2025年8月24日 11:59

在鸿蒙(HarmonyOS)应用开发中,Flex布局与Row/Column布局是两种核心的容器组件,它们在功能、性能及适用场景上存在显著差异。以下从五个维度进行详细对比:


📊 1. 核心差异对比

特性 Flex布局 Row/Column布局
布局机制 动态弹性计算,支持二次布局(重新分配空间) 单次线性排列,无二次布局
方向控制 支持水平(Row)、垂直(Column)及反向排列 Row仅水平,Column仅垂直
换行能力 支持自动换行(FlexWrap.Wrap 不支持换行,子组件溢出时被截断或压缩
子组件控制 支持flexGrowflexShrinkflexBasis动态分配空间 仅支持layoutWeight按比例分配空间
性能表现 较低(二次布局增加计算开销) 较高(单次布局完成)

⚠️ 二次布局问题:当子组件总尺寸与容器不匹配时,Flex需通过拉伸/压缩重新计算布局,导致性能损耗。


🔧 2. Flex布局的核心特点与场景

  • 核心优势

    • 多方向布局:通过direction自由切换主轴方向(水平/垂直)。

    • 复杂对齐:组合justifyContent(主轴)和alignItems(交叉轴)实现精准对齐。

    • 动态空间分配

      • flexGrow:按比例分配剩余空间(如搜索框占满剩余宽度)。
      • flexShrink:空间不足时按比例压缩子组件(需配合minWidth避免过度压缩)。
  • 必用场景

    • 多行排列:标签组、商品网格布局(需设置wrap: FlexWrap.Wrap)。
    • 响应式适配:跨设备屏幕(如手机/车机动态调整列数)。

📐 3. Row/Column布局的核心特点与场景

  • 核心优势

    • 轻量高效:线性排列无弹性计算,渲染性能更高。

    • 简洁属性

      • space:控制子组件间距(如导航栏按钮间隔)。
      • layoutWeight:一次遍历完成空间分配(性能优于flexGrow)。
  • 推荐场景

    • 单向排列

      • Row:水平导航栏、头像+文字组合。
      • Column:垂直表单、卡片内容堆叠。
    • 固定尺寸布局:子组件尺寸明确时(如按钮宽度固定)。


4. 性能差异与优化建议

  • Flex性能瓶颈

    • 二次布局触发条件:子组件总尺寸 ≠ 容器尺寸、优先级冲突(如displayPriority分组计算)。
    • 后果:嵌套过深或动态数据下易引发界面卡顿。
  • 优化策略

    • 替代方案:简单布局优先用Row/Column,避免Flex嵌套超过3层。

    • 属性优化

      • 固定尺寸组件设置flexShrink(0)禁止压缩。
      • 等分布局用layoutWeight替代flexGrow(如Row中占比1:2)。
    • 预设尺寸:尽量让子组件总尺寸接近容器尺寸,减少拉伸需求。


🛠️ 5. 选择策略与工程实践

  • 何时选择Flex?

    ✅ 需换行(如标签云)、复杂弹性对齐(如交叉轴居中)、动态网格布局。

    ❌ 避免在简单列表、表单等场景使用,优先Row/Column。

  • 何时选择Row/Column?

    ✅ 单向排列(水平/垂直)、子组件尺寸固定或比例明确(如30%+70%)。

    ✅ 高频场景:导航栏(Row)、表单(Column)、图文混排(Row+垂直居中)。

  • 工程最佳实践

    • 多端适配:通过DeviceType动态调整参数(如车机增大点击区域)。
    • 调试工具:用DevEco Studio布局分析器监测二次布局次数。
    • 混合布局:Flex内嵌套Row/Column(如Flex容器中的商品项用Column)。

💎 总结

  • Flex:强大但“重”,适合复杂弹性多行响应式布局,需警惕二次布局问题。

  • Row/Column:轻量高效,是单向排列场景的首选,性能优势明显。

  • 决策关键

    简单布局看方向(水平用Row,垂直用Column),

    复杂需求看弹性(换行/动态分配用Flex)。

通过合理选择组件并优化属性配置,可显著提升鸿蒙应用的渲染效率与用户体验。

H5资源包热更新:从下载、解压到渲染的实现方案

作者 simple_lau
2025年8月22日 17:14

前言

大家好,我是simple。我的理想是利用科技手段来解决生活中遇到的各种问题

在移动应用开发里,热更新技术特别实用——不用重新装应用,就能更新内容,大大提升了迭代效率。本文结合给出的代码,跟大家详细说下H5资源包热更新怎么实现,包括资源下载、解压到渲染的完整流程。

一、热更新核心流程概述

H5资源包热更新的核心思路其实很直接:先通过网络下最新的H5资源压缩包,解压到本地沙箱目录,再用Web组件加载本地的H5资源,就能实现页面更新了。整个流程分三步关键操作:

  1. 下载H5资源压缩包
  2. 把资源包解压到本地目录
  3. 跳转到Web页面,渲染本地的H5资源

二、资源包下载实现

1. 下载前的文件检查与备份

怕下载失败把旧资源搞坏了,所以下载前会先查下沙箱目录里有没有同名的资源包,有的话先备份起来:

const fileName = "test.zip"
const filePath = getContext().filesDir + '/' + fileName
// 检查是否存在旧文件,存在则备份
if (fileIo.listFileSync(getContext().filesDir).includes(fileName)) { 
    fileIo.renameSync(filePath, getContext().filesDir + '/test.bak.zip') 
} 

2. 带进度的下载实现

request.downloadFile发个下载请求,再用事件监听实时显示下载进度,失败和完成也会有对应的处理:

const task = await request.downloadFile(getContext(), { 
    url: 'http://www.test.com/test.zip', 
    filePath // 下载后保存的路径 
}) 
// 监听下载进度,更新进度条
task.on("progress", (current, total) => { 
    this.currentValue = current this.totalValue = total 
})
// 要是下载失败了,弹个框提示错误
task.on("fail", (error) => { 
    AlertDialog.show({ message: error.toString() }) 
}) 
// 下载完成后,关掉加载状态,给个成功提示 
task.on("complete", () => { 
    this.showLoading = false promptAction.showToast({ message: '下载成功' }) 
}) 

三、资源包解压与页面跳转

下载完之后,得把压缩包解压到本地目录,然后跳转到专门的Web页面,加载H5资源。

1. 解压实现

解压用的是zlib.decompressFile,解压路径就选应用的沙箱目录,解压成功后直接跳Web页面:

async decompressFile () {
    try { // 解压文件到沙箱目录 
        await zlib.decompressFile(this.filePath, getContext().filesDir) 
        // 解压成功后跳转到Web页面 
        router.pushUrl({ url: 'pages/webCase' }) 
    } catch(error) { 
        // 解压失败就弹框提示错误 
        AlertDialog.show({ message: error.message }) 
    } 
} 

四、H5资源渲染实现

WebCase组件专门负责加载、渲染解压后的本地H5资源,关键实现看这里:

1. Web组件配置

Web组件加载本地H5资源时,有个关键点得注意:要开本地存储权限,不然H5可能用不了localStorage这些功能。代码里这么配:

Web({ 
    controller: this.webController,
    // 加载解压后的index.html文件 
    src: "file://" + getContext().filesDir + '/test/index.html' 
}) 
.domStorageAccess(true) 
// 重点!得让H5能用上本地存储 
.width('100%') 
.height("100%") 

2. 调试模式开启

开发的时候要调H5页面,所以在页面初始化的时候,把Web调试模式打开,方便查问题:

aboutToAppear() { 
// 开启Web调试模式,方便调试H5页面 
    webview.WebviewController.setWebDebuggingAccess(true); 
} 

五、关键注意事项

  1. 本地存储权限别漏了:H5资源一般都要用到localStorage这类本地存储功能,必须设domStorageAccess(true),不然H5运行的时候会报错。
  2. 文件路径得处理好:用getContext().filesDir拿应用的沙箱目录,确保资源存在应用自己的私有空间里,不会有权限问题。
  3. 异常处理要做全:下载和解压的时候,得把异常都捕获到,用弹窗跟用户说清楚错在哪,体验会好很多。
  4. 版本管理不能少:实际项目里得加个版本校验的逻辑,别让相同版本的资源包重复下载,省流量也省时间。 这么一套流程走下来,应用就能实现H5资源热更新了——不用重新发版,就能更H5页面内容,给应用迭代加了不少灵活性。

鸿蒙 ArkTS 自定义组件全攻略:从按钮到商品卡片一步步搞定

作者 zhanshuo
2025年8月18日 23:37

在这里插入图片描述

摘要

在做应用开发的时候,我们经常会遇到这样的需求:系统提供的原生组件虽然能用,但总感觉差点意思。比如按钮样式不够个性化、输入框逻辑不够灵活、组件复用不够方便。这时候,自定义组件就是解题思路。在鸿蒙(HarmonyOS, ArkTS 开发)里,实现自定义组件并不复杂,甚至和 React/Vue 这些框架有点类似。你只要会封装 UI 和逻辑,就能把它们做成像系统组件一样随用随取的模块。

引言

随着鸿蒙生态逐渐成熟,开发者不仅要会用系统组件,更要能根据实际场景封装自己的组件。举个例子:

  • 如果你在做一个表单应用,那可能需要一个“带错误提示的输入框组件”。
  • 如果你在做一个商城类应用,那可能需要一个“带点击效果的商品卡片组件”。
  • 如果你在做一个工具类应用,那可能需要一个“支持动态切换状态的按钮”。

这些场景都指向一个关键技能:自定义组件。本文会从最基础的入门讲起,再带你通过实战 Demo 和场景案例把这个技能用熟。

基础概念:怎么写一个自定义组件

在鸿蒙的 ArkTS 开发中,实现一个自定义组件的关键点主要有:

@Component 定义组件类 组件的 UI 在 build() 方法里写。

@Prop 接收外部参数 类似 Vue 的 props,父组件传值,子组件接收。

@State 管理内部状态 内部逻辑变化会触发 UI 更新。

像系统组件一样使用 在父组件中直接 <MyComponent /> 即可。

可运行 Demo:自定义按钮

我们先从一个最简单的按钮组件入门。

定义组件(MyButton.ets

@Component
struct MyButton {
  // 外部传入的文本
  @Prop text: string = "默认按钮";

  // 外部传入的点击回调
  @Prop onClick: () => void = () => {};

  // 内部状态:是否被点击过
  @State clicked: boolean = false;

  build() {
    Button(this.clicked ? "已点击: " + this.text : this.text)
      .width('80%')
      .height(50)
      .backgroundColor(this.clicked ? '#4CAF50' : '#2196F3')
      .fontColor('#fff')
      .borderRadius(10)
      .onClick(() => {
        this.clicked = true;   // 内部状态更新
        this.onClick();        // 触发外部回调
      })
  }
}

使用组件(Index.ets

@Entry
@Component
struct Index {
  build() {
    Column() {
      Text("自定义组件 Demo")
        .fontSize(20)
        .margin({ bottom: 20 })

      // 使用自定义按钮
      MyButton({
        text: "点我一下",
        onClick: () => {
          console.log("按钮被点击了!");
        }
      })
    }
    .width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Center)
    .justifyContent(FlexAlign.Center)
  }
}

运行效果:

  • 初始状态下,按钮显示“点我一下”,背景蓝色。
  • 点击按钮后,文字变成“已点击: 点我一下”,背景变成绿色。
  • 控制台会打印“按钮被点击了!”。

进阶场景应用

自定义组件真正的价值,在于它能适配各种业务需求。下面给你三个典型的场景案例。

场景一:带错误提示的输入框

表单类应用常见的需求:输入框输入错误时要提示用户。

@Component
struct ValidInput {
  @Prop placeholder: string = "请输入内容";
  @Prop validator: (value: string) => boolean = () => true;

  @State value: string = "";
  @State error: boolean = false;

  build() {
    Column() {
      TextInput({ placeholder: this.placeholder })
        .width('90%')
        .height(40)
        .border({ width: 1, color: this.error ? '#FF0000' : '#CCC' })
        .onChange((val: string) => {
          this.value = val;
          this.error = !this.validator(val);
        })

      if (this.error) {
        Text("输入格式不正确").fontColor('#FF0000').fontSize(12)
      }
    }
  }
}

使用:

ValidInput({
  placeholder: "请输入邮箱",
  validator: (val: string) => val.includes("@")
})

场景二:带动画的点赞按钮

很多社交类应用里,点赞按钮点一下会有动画效果。

@Component
struct LikeButton {
  @State liked: boolean = false;

  build() {
    Image(this.liked ? 'like_filled.png' : 'like_empty.png')
      .width(40)
      .height(40)
      .onClick(() => {
        animateTo({ duration: 300 }, () => {
          this.liked = !this.liked;
        })
      })
  }
}

点击时,图标会在 300 毫秒的动画中切换状态。

场景三:商品卡片组件

电商类应用常见的 UI,可以封装成可复用的组件。

@Component
struct ProductCard {
  @Prop title: string = "商品标题";
  @Prop price: string = "¥0.00";
  @Prop image: string = "default.png";

  build() {
    Column() {
      Image(this.image)
        .width(150).height(150)
      Text(this.title)
        .fontSize(16)
        .margin({ top: 5 })
      Text(this.price)
        .fontSize(14)
        .fontColor('#E91E63')
    }
    .borderRadius(10)
    .shadow({ radius: 5, color: '#aaa' })
    .padding(10)
  }
}

使用:

ProductCard({
  title: "鸿蒙定制T恤",
  price: "¥99",
  image: "tshirt.png"
})

常见问题 QA

Q1: 自定义组件和系统组件有什么区别? A: 系统组件是框架提供的基础能力,自定义组件是开发者封装的“组合能力”。你可以在自定义组件里用系统组件,也可以再嵌套别的自定义组件。

Q2: 如果父组件要修改子组件的状态怎么办? A: 用 @Link,父子组件可以共享状态变量。

Q3: 多层级传递数据很麻烦怎么办? A: 用 @Provide@Consume,可以跨层级传递状态,类似 Vue 的 provide/inject。

Q4: 自定义组件能不能写成库复用? A: 可以。你可以把常用组件封装成独立模块,在多个项目里复用。

总结

在鸿蒙开发中,自定义组件是提升开发效率和代码复用率的核心技能。 本文从最基础的“自定义按钮”入门,到进阶的“输入框校验”“动画按钮”“商品卡片”,展示了自定义组件在实际场景中的灵活性。

记住几个关键点:

  • @Component 定义组件。
  • @Prop 接收父组件传值。
  • @State 处理内部状态。
  • 搭配 @Link@Provide@Consume,能处理更复杂的状态管理场景。

掌握这些后,你在鸿蒙开发里几乎可以封装任何 UI 组件,让项目既简洁又可维护。

HarmonyOS 推送通知开发实战:从权限申请到多场景应用的完整指南

作者 zhanshuo
2025年8月18日 23:33
摘要 在日常应用开发中,通知功能几乎是必不可少的。不管是聊天消息、系统提醒,还是任务进度更新,都需要通过通知来触达用户。对于开发者来说,如何在鸿蒙(HarmonyOS)应用中实现推送通知,是一个绕不开

鸿蒙AVSession Kit

作者 风冷
2025年8月17日 15:28

—— ArkTS 与 C/C++ 双语言、Provider 与 Controller 全角色

适用版本:HarmonyOS API 12+

覆盖:本地会话提供方(Provider)、会话控制方(Controller)、后台播放


一、设计总览(Mermaid)

graph TD
    subgraph 系统
        PC[播控中心/耳机/车机]
    end
    subgraph 应用进程
        A[AVSessionProvider<br>播放器] -->|setAVMetadata<br>setAVPlaybackState| S[AVSession]
        C[AVSessionController<br>控制逻辑] -->|sendControlCommand| S
    end
    PC <-->|控制命令| S
  • 任何进程都能 读 会话(获取元数据/状态)
  • 任何进程都能 写 会话(发命令)——只要拿到对应 AVSessionController

二、ArkTS 完整落地

  1. 依赖
import { avSession } from '@kit.AVSessionKit';
import { BackgroundTasks } from '@kit.BackgroundTasksKit';
  1. 会话提供方(Provider)
class MyProvider {
  private session: avSession.AVSession | null = null;

  async init(ctx: Context) {
    /* 1. 长时任务 */
    await BackgroundTasks.startBackgroundRunning(ctx, 'AUDIO_PLAYBACK', 'music');

    /* 2. 创建会话 */
    this.session = await avSession.createAVSession(ctx, 'MusicSession', 'audio');

    /* 3. 元数据 */
    await this.session.setAVMetadata({
      assetId: '10086',
      title: 'Bohemian Rhapsody',
      artist: 'Queen',
      duration: 355000,
      mediaImage: 'https://xx/cover.jpg',
      previousAssetId: '10085',
      nextAssetId: '10087'
    });

    /* 4. 播放状态 */
    await this.session.setAVPlaybackState({
      state: avSession.PlaybackState.PLAYBACK_STATE_PLAYING,
      position: { elapsedTime: 60000, updateTime: Date.now() },
      speed: 1.0,
      loopMode: avSession.LoopMode.LOOP_MODE_LIST
    });

    /* 5. 注册命令(被动) */
    this.session.on('play',     () => this.player.play());
    this.session.on('pause',    () => this.player.pause());
    this.session.on('seek',     (pos: number) => this.player.seek(pos));
    this.session.on('playNext', () => this.player.next());

    /* 6. 激活 */
    await this.session.activate();
  }

  private player = { play:()=>{}, pause:()=>{}, seek:()=>{}, next:()=>{} };

  async destroy() {
    this.session?.destroy();
    BackgroundTasks.stopBackgroundRunning(getContext(this));
  }
}
  1. 会话控制方(Controller)
class MyController {
  private ctrl: avSession.AVSessionController | null = null;

  async attach(sessionId: string) {
    this.ctrl = await avSession.createController(sessionId);
  }

  async play()  { await this.ctrl?.sendControlCommand({ command: 'play' }); }
  async pause() { await this.ctrl?.sendControlCommand({ command: 'pause' }); }
  async seek(ms: number) {
    await this.ctrl?.sendControlCommand({ command: 'seek', parameters: { position: ms } });
  }
}
  1. 枚举会话
const sessions = await avSession.getAllActiveSessions();
console.log('当前活跃会话', sessions.map(s => s.sessionTag));

三、C/C++(NDK)完整落地

  1. CMake
find_library(avsession-lib libohavsession.so)
target_link_libraries(your_target ${avsession-lib})
  1. 会话提供方(Provider)
// 1. 创建
OH_AVSession* session;
OH_AVSession_Create("MusicSess", SESSION_TYPE_AUDIO,
                    "com.demo", "MainAbility", &session);

// 2. 元数据
OH_AVMetadataBuilder* mb = OH_AVMetadataBuilder_Create();
OH_AVMetadataBuilder_SetAssetId(mb, "10086");
OH_AVMetadataBuilder_SetTitle(mb, "Rhapsody");
OH_AVMetadata* meta = OH_AVMetadataBuilder_Build(mb);
OH_AVSession_SetAVMetadata(session, meta);

// 3. 播放状态
OH_AVPlaybackState* ps = OH_AVPlaybackState_Create();
OH_AVPlaybackState_SetState(ps, PLAYBACK_STATE_PLAYING);
OH_AVSession_SetAVPlaybackState(session, ps);

// 4. 注册命令
OH_AVSession_RegisterCommandCallback(
    session, COMMAND_PLAY,
    [](void*){ /* real play */ }, nullptr);

OH_AVSession_Activate(session);
  1. 会话控制方(Controller)
// 枚举
AVSession_SessionInfo* infos;
size_t cnt;
OH_AVSessionManager_GetAllActiveSessions(&infos, &cnt);

// 创建 Controller
OH_AVSessionController* ctrl;
OH_AVSessionController_Create(infos[0].sessionId, &ctrl);

// 发命令
OH_AVControllerCommand cmd = { .command = COMMAND_SEEK, .parameters.position = 30000 };
OH_AVSessionController_SendCommand(ctrl, &cmd);
  1. 清理
OH_AVSessionController_Destroy(ctrl);
OH_AVSession_Destroy(session);

四、命令全集(两语言通用)

命令ArkTS 参数C++ 枚举 play{ command:'play' }COMMAND_PLAY pause{ command:'pause' }COMMAND_PAUSE stop{ command:'stop' }COMMAND_STOP seek{ command:'seek', parameters:{ position } }COMMAND_SEEK setSpeed{ command:'setSpeed', parameters:{ speed } }COMMAND_SET_SPEED setLoopMode{ command:'setLoopMode', parameters:{ loopMode } }COMMAND_SET_LOOP_MODE toggleFavorite{ command:'toggleFavorite' }COMMAND_TOGGLE_FAVORITE playNext / playPrevious{ command:'playNext'/'playPrevious' }COMMAND_PLAY_NEXT / PREVIOUS skipToQueueItem{ command:'skipToQueueItem', parameters:{ itemId } }COMMAND_SKIP_TO_QUEUE_ITEM commonCommand{ command:'commonCommand', parameters:{ command, extras } }COMMAND_COMMON


五、后台播放 checklist(上架必过)

  • createAVSession 的 type 与音频流类型匹配(audio / video / voice_call)
  • 激活前已 setAVMetadata + setAVPlaybackState + 注册命令
  • 已申请 AUDIO_PLAYBACK 长时任务且 pause/stop 时主动取消
  • 退出业务时 destroy 会话
  • 若支持歌单/冷启动续播,已注册 PlayMusicList / PlayAudio 意图

至此,ArkTS 与 C/C++ 双语言、Provider 与 Controller 全角色一次讲透。

❌
❌