首页 > 其他分享 >管理组件状态

管理组件状态

时间:2024-01-21 21:13:48浏览次数:22  
标签:状态 自定义 ... 视频 管理 组件 弹窗 ets

概述

在应用中,界面通常都是动态的。如图1所示,在子目标列表中,当用户点击目标一,目标一会呈现展开状态,再次点击目标一,目标一呈现收起状态。界面会根据不同的状态展示不一样的效果。

图1 展开/收起目标项

ArkUI作为一种声明式UI,具有状态驱动UI更新的特点。当用户进行界面交互或有外部事件引起状态改变时,状态的变化会触发组件自动更新。所以在ArkUI中,我们只需要通过一个变量来记录状态。当改变状态的时候,ArkUI就会自动更新界面中受影响的部分。

ArkUI框架提供了多种管理状态的装饰器来修饰变量,使用这些装饰器修饰的变量即称为状态变量。

在组件范围传递的状态管理常见的场景如下:

场景

装饰器

组件内的状态管理

@State

从父组件单向同步状态

@Prop

与父组件双向同步状态

@Link

跨组件层级双向同步状态

@Provide和@Consume

在组件内使用@State装饰器来修饰变量,可以使组件根据不同的状态来呈现不同的效果。若当前组件的状态需要通过其父组件传递而来,此时需要使用@Prop装饰器;若是父子组件状态需要相互绑定进行双向同步,则需要使用@Link装饰器。使用@Provide和@Consume装饰器可以实现跨组件层级双向同步状态。

在实际应用开发中,应用会根据需要封装数据模型。如果需要观察嵌套类对象属性变化,需要使用@Observed和@ObjectLink装饰器,因为上述表格中的装饰器只能观察到对象的第一层属性变化。@Observed和@ObjectLink装饰器的具体使用方法可参考@Observed装饰器和@ObjectLink装饰器:嵌套类对象属性变化

另外,当状态改变,需要对状态变化进行监听做一些相应的操作时,可以使用@Watch装饰器来修饰状态。

组件内的状态管理:@State

实际开发中由于交互,组件的内容呈现可能产生变化。当需要在组件内使用状态来控制UI的不同呈现方式时,可以使用@State装饰器。以任务管理应用为例,当点击子目标列表的其中一项,列表项会展开。当再次点击同一项,列表项会收起。所以,对于某一个列表项来说,它的呈现方式会受列表项是否展开这个状态影响。

图2 展开/收起目标项

将是否展开这个状态定义为isExpanded变量,当其值为false表示目标项收起,值为true时表示目标项展开。

此时,需要使用@State装饰器修饰isExpanded,使其成为目标项内部的状态变量。通过@State装饰后,框架内部会建立数据与视图间的绑定,

当isExpanded状态变化时,目标项会随之展开或收起。

图3 定义是否展开状态

其具体实现只要用@State修饰isExpanded变量,定义是否展开状态。然后通过条件渲染,实现是否显示进度调整面板和列表项的高度变化。最后,监听列表项的点击事件,在onClick回调中改变isExpanded状态。

这样就实现了对相同列表项点击时,列表项的展开和收起功能。当用户反复点击同一个列表项时,组件内的isExpanded状态变化,列表项会自动更新。

   
  1. @Component
  2. export default struct TargetListItem {
  3. @State isExpanded: boolean = false;
  4. ...
  5. build() {
  6. ...
  7. Column() {
  8. ...
  9. if (this.isExpanded) {
  10. Blank()
  11. ProgressEditPanel(...)
  12. }
  13. }
  14. .height(this.isExpanded ? $r('app.float.expanded_item_height')
  15. : $r('app.float.list_item_height'))
  16. .onClick(() => {
  17. ...
  18. this.isExpanded = !this.isExpanded;
  19. ...
  20. })
  21. ...
  22. }
  23. }

从父组件单向同步状态:@Prop

当子组件中的状态依赖从父组件传递而来时,需要使用@Prop装饰器,@Prop修饰的变量可以和其父组件中的状态建立单向同步关系。当父组件中状态变化时,该状态值也会更新至@Prop修饰的变量;对@Prop修饰的变量的修改不会影响其父组件中的状态。

图4 列表的编辑模式

如图4所示,在目标管理应用中,当用户点击子目标列表的“编辑”文本,列表进入编辑模式,点击取消,列表退出编辑模式。

整个列表是自定义组件TargetList,顶部是文本显示区域,主要是Text组件,底部是一个Button组件。中间区域则是用来显示每个目标项,目标项是自定义组件TargetListItem。

从图中可以看出,TargetListItem是TargetList的子组件。TargetList是TargetListItem父组件。

图5 TargetList和TargetListItem

对于父组件TargetList,其顶部显示的文本和底部按钮会随编辑模式的变化而变化,因此父组件拥有编辑模式状态。

对于子组件TargetListItem,其最右侧是否预留位置和显示勾选框也会随编辑模式变化,因此子组件也拥有编辑模式状态。

但是是否进入编辑模式,其触发点是在用户点击列表的“编辑”或取消按钮,状态变化的源头仅在于父组件TargetList。当父组件TargetList中的编辑模式变化时,子组件TargetListItem的编辑模式状态需要随之变化。

图6 从父组件单向同步isEditMode状态

在父组件TargetList中可以定义一个是否进入编辑模式的状态,即用@State修饰isEditMode。@State修饰的变量不仅是组件内部的状态,也可以作为子组件单向或双向同步的数据源。ArkUI提供了@Prop装饰器,@Prop修饰的变量可以和其父组件中的状态建立单向同步关系,所以用@Prop修饰子组件TargetListItem中的isEditMode变量。

在父组件TargetList中,用@State修饰isEditMode,定义编辑模式状态。然后利用条件渲染实现根据是否进入编辑模式,显示不同的文本和按钮。同时,在父组件中需要在用户点击时改变状态,触发界面更新。

当点击“编辑”事件发生时,进入编辑模式,显示取消、全选文本和勾选框,同时显示删除按钮;当点击“取消”事件发生时,退出编辑模式,显示“编辑”文本和“添加子目标”按钮。

   
  1. @Component
  2. export default struct TargetList {
  3. @State isEditMode: boolean = false;
  4. ...
  5. build() {
  6. Column() {
  7. Row() {
  8. ...
  9. if (this.isEditMode) {
  10. Text($r('app.string.cancel_button'))
  11. .onClick(() => {
  12. this.isEditMode = false;
  13. ...
  14. })
  15. ...
  16. Text($r('app.string.select_all_button'))...
  17. Checkbox()...
  18. } else {
  19. Text($r('app.string.edit_button'))
  20. .onClick(() => {
  21. this.isEditMode = true;
  22. })
  23. ...
  24. }
  25. ...
  26. }
  27. ...
  28. List({ space: CommonConstants.LIST_SPACE }) {
  29. ForEach(this.targetData, (item: TaskItemBean, index: number) => {
  30. ListItem() {
  31. TargetListItem({
  32. isEditMode: this.isEditMode,
  33. ...
  34. })
  35. }
  36. }, (item, index) => JSON.stringify(item) + index)
  37. }
  38. ...
  39. if (this.isEditMode) {
  40. Button($r('app.string.delete_button'))
  41. } else {
  42. Button($r('app.string.add_task'))
  43. }
  44. }
  45. ...
  46. }
  47. }

在子组件TargetListItem中,使用@Prop修饰子组件的isEditMode变量,定义子组件的编辑模式状态。然后同样根据是否进入编辑模式,控制目标项最右侧是否预留位置和显示勾选框。

   
  1. @Component
  2. export default struct TargetListItem {
  3. @Prop isEditMode: boolean;
  4. ...
  5. Column() {
  6. ...
  7. }
  8. .padding({
  9. ...
  10. right: this.isEditMode ? $r('app.float.list_edit_padding')
  11. : $r('app.float.list_padding')
  12. })
  13. ...
  14. if (this.isEditMode) {
  15. Row() {
  16. Checkbox()...
  17. }
  18. }
  19. ...
  20. }

最后,最关键的一步就是要在父组件中使用子组件时,将父组件的编辑模式状态this.isEditMode传递给子组件的编辑模式状态isEditMode。

   
  1. @Component
  2. export default struct TargetList {
  3. @State isEditMode: boolean = false;
  4. ...
  5. build() {
  6. Column() {
  7. ...
  8. List({ space: CommonConstants.LIST_SPACE }) {
  9. ForEach(this.targetData, (item: TaskItemBean, index: number) => {
  10. ListItem() {
  11. TargetListItem({
  12. isEditMode: this.isEditMode,
  13. ...
  14. })
  15. }
  16. }, (item, index) => JSON.stringify(item) + index)
  17. }
  18. ...
  19. }
  20. ...
  21. }
  22. }

与父组件双向同步状态:@Link

若是父子组件状态需要相互绑定进行双向同步时,可以使用@Link装饰器。父组件中用于初始化子组件@Link变量的必须是在父组件中定义的状态变量。

图7 切换目标项

在目标管理应用中,当用户点击同一个目标,目标项会展开或者收起。当用户点击不同的目标项时,除了被点击的目标项展开,同时前一次被点击的目标项会收起。

如图7所示,当目标一展开时,点击目标三,目标三会展开,同时目标一会收起。再点击目标一时,目标一展开,同时目标三会收起。

从目标一切换到目标三的流程中,关键在于最后目标一的收起,当点击目标三时,目标一需要知道点击了目标三,目标一才会收起。

图8 子目标列表目标项位置索引

在子目标列表中,每个列表项都有其位置索引值index属性,表示目标项在列表中的位置。index从0开始,即第一个目标项的索引值为0,第二个目标项的索引值为1,以此类推。此外,clickIndex用来记录被点击的目标项索引。当点击目标一时,clickIndex为0,点击目标三时,clickIndex为2。

在父组件子目标列表和每个子组件目标项中都拥有clickIndex状态。当目标一展开时,clickIndex为0。此时点击目标三,目标三的clickIndex变为2,只要其父组件子目标列表感知到clickIndex状态变化,同时将此变化传递给目标一。目标一的clickIndex即可同步改变为2,即目标一感知到此时点击了目标三。

图9 与父组件双向同步clickIndex状态

将列表和目标项对应到列表组件TargetList和列表项TargetListItem。首先,需要在父组件TargetList中定义clickIndex状态。

若此时子组件中的clickIndex用@Prop装饰器修饰,当子组件中clickIndex变化时,父组件无法感知,因为@Prop装饰器建立的是从父组件到子组件的单向同步关系。

ArkUI提供了@Link装饰器,用于与父组件双向同步状态。当子组件TargetListItem中的clickIndex用@Link修饰,可与父组件TargetList中的clickIndex建立双向同步关系。

   
  1. @Component
  2. export default struct TargetList {
  3. @State clickIndex: number = CommonConstants.DEFAULT_CLICK_INDEX;
  4. ...
  5. TargetListItem({
  6. clickIndex: $clickIndex,
  7. ...
  8. })
  9. ...
  10. }

首先,在父组件TargetList中用@State装饰器定义点击的目标项索引状态。然后,在子组件TargetListItem中用@Link装饰器定义clickIndex,当点击目标项时,clickIndex更新为当前目标索引值。

完成在父子组件中定义状态后,最关键的就是要建立父子组件的双向关联关系。在父组件中使用子组件时,将父组件的clickIndex传递给子组件的clickIndex。其中父组件的clickIndex加上$表示传递的是引用。

   
  1. @Component
  2. export default struct TargetListItem {
  3. @Link @Watch('onClickIndexChanged') clickIndex: number;
  4. @State isExpanded: boolean = false
  5. ...
  6. onClickIndexChanged() {
  7. if (this.clickIndex != this.index) {
  8. this.isExpanded = false;
  9. }
  10. }
  11. build() {
  12. ...
  13. Column() {
  14. ...
  15. }
  16. .onClick(() => {
  17. ...
  18. this.clickIndex = this.index;
  19. ...
  20. })
  21. ...
  22. }
  23. }

当目标一感知到点击了目标三时,还需要将目标一收起,切换列表项的功能才是完整的。此时,目标一感知到clickIndex变为2,需要判断与目标一本身的位置索引值0不相等,从而将目标一收起。此时,就需要用到ArkUI中监听状态变化@Watch的能力。用@Watch修饰的状态,当状态发生变化时,会触发声明时定义的回调。

我们给TargetListItem的中的clickIndex状态加上@Watch("onClickIndexChanged")。这表示需要监听clickIndex状态的变化。当clickIndex状态变化时,将触发onClickIndexChanged回调:如果点击的列表项索引不等于当前列表项索引,则将isExpanded状态置为false,从而收起该目标项。

跨组件层级双向同步状态:@Provide和@Consume

跨组件层级双向同步状态是指@Provide修饰的状态变量自动对提供者组件的所有后代组件可用,后代组件通过使用@Consume装饰的变量来获得对提供的状态变量的访问。@Provide作为数据的提供方,可以更新其子孙节点的数据,并触发页面渲染。@Consume在感知到@Provide数据的更新后,会触发当前自定义组件的重新渲染。

使用@Provide的好处是开发者不需要多次将变量在组件间传递。@Provide和@Consume的具体使用方法请参见开发指南:@Provide装饰器和@Consume装饰器:与后代组件双向同步

参考

更多状态管理场景和相关知识请参考开发指南:状态管理

介绍

本篇Codelab将介绍如何使用@State、@Prop、@Link、@Watch、@Provide、@Consume管理页面级变量的状态,实现对页面数据的增加、删除、修改。要求完成以下功能:

  1. 实现一个自定义弹窗,完成添加子目标的功能。
  2. 实现一个可编辑列表,可点击指定行展开调节工作目标进度,可多选、全选删除指定行。

相关概念

完整示例

gitee源码地址

源码下载   目标管理(ArkTS).zip   环境搭建 我们首先需要完成HarmonyOS开发环境搭建,可参照如下步骤进行。

软件要求

硬件要求

  • 设备类型:华为手机或运行在DevEco Studio上的华为手机设备模拟器。
  • HarmonyOS系统:3.1.0 Developer Release。

环境搭建

  1. 安装DevEco Studio,详情请参考下载和安装软件
  2. 设置DevEco Studio开发环境,DevEco Studio开发环境需要依赖于网络环境,需要连接上网络才能确保工具的正常使用,可以根据如下两种情况来配置开发环境:
    • 如果可以直接访问Internet,只需进行下载HarmonyOS SDK操作。
    • 如果网络不能直接访问Internet,需要通过代理服务器才可以访问,请参考配置开发环境
  3. 开发者可以参考以下链接,完成设备调试的相关配置:
  代码结构解读

本篇Codelab只对核心代码进行讲解,对于完整代码,我们会在源码下载或gitee中提供。

    No Preview
  1. ├──entry/src/main/ets // ArkTS代码区
  2. │ ├──common
  3. │ │ ├──constants
  4. │ │ │ └──CommonConstants.ets // 公共常量类
  5. │ │ └──utils
  6. │ │ ├──DateUtil.ets // 获取格式化日期工具
  7. │ │ └──Logger.ets // 日志打印工具类
  8. │ ├──entryability
  9. │ │ └──EntryAbility.ts // 程序入口类
  10. │ ├──pages
  11. │ │ └──MainPage.ets // 主页面
  12. │ ├──view
  13. │ │ ├──TargetInformation.ets // 整体目标详情自定义组件
  14. │ │ ├──AddTargetDialog.ets // 自定义弹窗
  15. │ │ ├──ProgressEditPanel.ets // 进展调节自定义组件
  16. │ │ ├──TargetList.ets // 工作目标列表
  17. │ │ └──TargetListItem.ets // 工作目标列表子项
  18. │ └──viewmodel
  19. │ └──DataModel.ets // 工作目标数据操作类
  20. └──entry/src/main/resources // 资源文件目录
构建主界面

MainPage作为本应用的主界面,从上至下由三个自定义组件组成。

  1. 标题titleBar。
  2. 目标整体进展详情TargetInformation。
  3. 子目标列表TargetList。

MainPage主要维护五个参数:子目标数组targetData、子目标总数totalTasksNumber、已完成子目标数completedTasksNumber、最近更新时间latestUpdateDate、监听数据变化的参数overAllProgressChanged。具体作用有以下三个方面:

  1. 子组件TargetInformation接收三个参数totalTasksNumber、completedTasksNumber、latestUpdateDate,渲染整体目标详情。
  2. 子组件TargetList接收参数targetData渲染列表。
  3. 使用@Watch监听overAllProgressChanged的变化。当overAllProgressChanged改变时,回调onProgressChanged方法,刷新整体进展TargetInformation。
    No Preview
  1. // MainPage.ets
  2. @Entry
  3. @Component
  4. struct MainPage {
  5. // 子目标数组
  6. @State targetData: Array<TaskItemBean> = DataModel.getData();
  7. // 子目标总数
  8. @State totalTasksNumber: number = 0;
  9. // 已完成子目标数
  10. @State completedTasksNumber: number = 0;
  11. // 最近更新时间
  12. @State latestUpdateDate: string = CommonConstants.DEFAULT_PROGRESS_VALUE;
  13. // 监听数据变化的参数
  14. @Provide @Watch('onProgressChanged') overAllProgressChanged: boolean = false;
  15. ...
  16. /**
  17. * overAllProgressChanged改变时的回调
  18. */
  19. onProgressChanged() {
  20. this.totalTasksNumber = this.targetData.length;
  21. this.completedTasksNumber = this.targetData.filter((item: TaskItemBean) => {
  22. return item.progressValue === CommonConstants.SLIDER_MAX_VALUE;
  23. }).length;
  24. this.latestUpdateDate = getCurrentTime();
  25. }
  26. build() {
  27. Column() {
  28. // 标题
  29. this.titleBar()
  30. // 目标整体进展详情
  31. TargetInformation({
  32. latestUpdateDate: this.latestUpdateDate,
  33. totalTasksNumber: this.totalTasksNumber,
  34. completedTasksNumber: this.completedTasksNumber
  35. })
  36. // 子目标列表
  37. TargetList({
  38. targetData: $targetData,
  39. onAddClick: (): void => this.dialogController.open()
  40. })
  41. ...
  42. }
  43. ...
  44. }
  45. @Builder
  46. titleBar() {
  47. Text($r('app.string.title'))
  48. ...
  49. }
  50. }
添加任务子目标

本章节主要介绍如何实现一个自定义弹窗,完成添加子目标的功能。效果如图所示:

在MainPage.ets中,创建dialogController对象控制弹窗隐显,传入自定义组件AddTargetDialog和点击确定的回调方法saveTask。

在AddTargetDialog.ets中,参数onClickOk为function类型,接收MainPage传入的saveTask方法。点击确定,调用onClickOk执行saveTask方法,关闭弹窗。

在MainPage.ets中,实现saveTask方法:保存数据至DataModel中,并更新targetData的值,完成添加子目标功能。     No Preview
  1. // MainPage.ets
  2. @Entry
  3. @Component
  4. struct MainPage {
  5. ...
  6. saveTask(taskName: string) {
  7. if (taskName === '') {
  8. promptAction.showToast({
  9. message: $r('app.string.cannot_input_empty'),
  10. duration: CommonConstants.TOAST_TIME,
  11. bottom: CommonConstants.TOAST_MARGIN_BOTTOM
  12. });
  13. return;
  14. }
  15. // 保存数据
  16. DataModel.addData(new TaskItemBean(taskName, 0, getCurrentTime()));
  17. // 更新targetData刷新页面
  18. this.targetData = DataModel.getData();
  19. this.overAllProgressChanged = !this.overAllProgressChanged;
  20. this.dialogController.close();
  21. }
  22. }
实现可编辑列表

本章节主要介绍子目标列表TargetList的实现,包括以下功能:

  • 列表项展开。
  • 列表子项点击下拉,滑动滑块更新进展。
  • 列表进入编辑状态,单选、多选、全选、删除子项。
  6.1 实现列表项展开

实现以下步骤完成点击列表项展开功能:

  1. 使用@State 管理参数isExpanded,表示当前项是否展开,具体表现为自定义组件ProgressEditPanel的显示或隐藏。
  2. 使用@Link和@Watch管理参数clickIndex,表示当前点击ListItem的Index索引。clickIndex值的改变将会传递至所有的ListItem。
  3. 完成onClick点击事件,将isExpanded 值置反,修改clickIndex值为当前点击的索引。
    No Preview
  1. // TargetListItem.ets
  2. @Component
  3. export default struct TargetListItem {
  4. @State latestProgress?: number = 0;
  5. @Link @Watch('onClickIndexChanged') clickIndex: number;
  6. @State isExpanded: boolean = false;
  7. ...
  8. // clickIndex改变的回调方法
  9. onClickIndexChanged() {
  10. if (this.clickIndex !== this.index) {
  11. this.isExpanded = false;
  12. }
  13. }
  14. build() {
  15. ...
  16. Column() {
  17. this.TargetItem()
  18. if (this.isExpanded) {
  19. Blank()
  20. // 自定义组件:编辑面板
  21. ProgressEditPanel({
  22. slidingProgress: this.latestProgress,
  23. onCancel: () => this.isExpanded = false,
  24. onClickOK: (progress: number): void => {
  25. this.latestProgress = progress;
  26. this.updateDate = getCurrentTime();
  27. let result = DataModel.updateProgress(this.index, this.latestProgress, this.updateDate);
  28. if (result) {
  29. this.overAllProgressChanged = !this.overAllProgressChanged;
  30. }
  31. this.isExpanded = false;
  32. },
  33. sliderMode: $sliderMode
  34. })
  35. ...
  36. }
  37. }
  38. ...
  39. .onClick(() => {
  40. ...
  41. if (!this.isEditMode) {
  42. animateTo({ duration: CommonConstants.DURATION }, () => {
  43. this.isExpanded = !this.isExpanded;
  44. })
  45. this.clickIndex = this.index;
  46. }
  47. })
  48. }
  49. ...
  50. }
6.2 实现更新进展

列表某项被展开后,实现以下步骤完成更新进展功能:

  1. Slider实现滑动条,滑动滑块调节进展,使用slidingProgress保存滑动值。
  2. 点击确定调用onClickOK方法,将数据slidingProgress回调至TargetListItem。
  3. 在TargetListItem中获取回调的数据并刷新页面。

在TargetListItem.ets中,完成onClickOK方法的实现,将依次完成以下步骤。

  1. 重新渲染TargetListItem的进度值和最近更新时间。
  2. 更新缓存的数据。
  3. 修改overAllProgressChanged的值,通知主页刷新整体进展详情TargetInformation。
    No Preview
  1. // TargetListItem.ets
  2. @Component
  3. export default struct TargetListItem {
  4. ...
  5. build() {
  6. ...
  7. Column() {
  8. ...
  9. if (this.isExpanded) {
  10. Blank()
  11. // 自定义组件:编辑面板
  12. ProgressEditPanel({
  13. ...
  14. onClickOK: (progress: number): void => {
  15. this.latestProgress = progress;
  16. this.updateDate = getCurrentTime();
  17. let result = DataModel.updateProgress(this.index, this.latestProgress, this.updateDate);
  18. if (result) {
  19. this.overAllProgressChanged = !this.overAllProgressChanged;
  20. }
  21. this.isExpanded = false;
  22. },
  23. ...
  24. })
  25. ...
  26. }
  27. }
  28. }
  29. }
6.3 实现列表多选

列表进入编辑模式才可单选、多选。实现以下步骤完成列表多选功能:

  1. 维护一个boolean类型的数组selectArray,其长度始终与数据列表的长度相等,且初始值均为false。表示进入编辑状态时列表均未选中。
  2. 定义一个boolean类型的值isEditMode,表示是否进入了编辑模式。
  3. TargetListItem选中状态的初始化和点击Checkbox改变TargetListItem的选中状态。

点击全选Checkbox,将selectArray数组的值全赋值true或false,重新渲染列表为全选或者取消全选状态。

在TargetListItem中,实现以下步骤改变ListItem的选中状态:

  1. 使用@Link定义selectArr数组接收TargetList传入的selectArray。
  2. 在TargetListItem渲染时,使用this.selectArr[this.index]获取初始选中状态。
  3. 点击Checkbox时,按照当前ListItem的索引,将选中状态保存至selectArr,重新渲染列表完成单选和多选功能
    No Preview
  1. // TargetListItem.ets
  2. export default struct TargetListItem {
  3. ...
  4. @Link selectArr: Array<boolean>;
  5. public index: number = 0;
  6. build() {
  7. Stack({ alignContent: Alignment.Start }) {
  8. ...
  9. this.TargetItem()
  10. ...
  11. Checkbox()
  12. // 获取初始选中状态
  13. .select(this.selectArr[this.index])
  14. ...
  15. .onChange((isCheck: boolean) => {
  16. // 改变被点击项的选中状态
  17. this.selectArr[this.index] = isCheck;
  18. })
  19. ...
  20. ...
  21. }
  22. }
  23. }
6.4 实现删除选中列表项

当点击“删除”时,调用TargetList.ets的deleteSelected方法,实现以下步骤完成列表项删除功能:

  1. 调用DataModel的deleteData方法删除数据。
  2. 更新targetData的数据重新渲染列表。
  3. 修改overAllProgressChanged的值,通知主页刷新整体进展详情TargetInformation。
在DataModel.ets中,遍历数据列表,删除被选中的数据项。     No Preview
  1. // DataModel.ets
  2. export class DataModel {
  3. ...
  4. // 删除选中的数据
  5. deleteData(selectArr: Array<boolean>) {
  6. if (!selectArr) {
  7. Logger.error(TAG, 'Failed to delete data because selectArr is ' + selectArr);
  8. }
  9. let dataLen = this.targetData.length - CommonConstants.ONE_TASK;
  10. for (let i = dataLen; i >= 0; i--) {
  11. if (selectArr[i]) {
  12. this.recordData.splice(i, CommonConstants.ONE_TASK);
  13. }
  14. }
  15. }
  16. ...
  17. }
总结

您已经完成了本次Codelab的学习,并了解到以下知识点:

  1. @State、@Prop、@Link、@Watch、@Provide、@Consume的使用。
  2. List组件的使用。
  3. 自定义弹窗的使用。
  4. Slider组件的使用。

概述

在手机、平板或是智慧屏这些终端设备上,媒体功能可以算作是我们最常用的场景之一。无论是实现音频的播放、录制、采集,还是视频的播放、切换、循环,亦或是相机的预览、拍照等功能,媒体组件都是必不可少的。以视频功能为例,在应用开发过程中,我们需要通过ArkUI提供的Video组件为应用增加基础的视频播放功能。借助Video组件,我们可以实现视频的播放功能并控制其播放状态。常见的视频播放场景包括观看网络上的较为流行的短视频,也包括查看我们存储在本地的视频内容。

本文将结合《简易视频播放器(ArkTS)》这个Codelab,对Video组件的参数、属性及事件进行介绍,然后通过组件的属性调用和事件回调阐明Video组件的基本使用方法,最后结合Video组件使用过程中的常见问题讲解自定义控制器的使用。

Video组件用法介绍

Video组件参数介绍

Video组件的接口表达形式为:

   
  1. Video(value: {src?: string | Resource, currentProgressRate?: number | string |PlaybackSpeed, previewUri?: string |PixelMap | Resource, controller?: VideoController})

其中包含四个可选参数,src、currentProgressRate、previewUri和controller。

  • src表示视频播放源的路径,可以支持本地视频路径和网络路径。使用网络地址时,如https,需要注意的是需要在module.json5文件中申请网络权限。在使用本地资源播放时,当使用本地视频地址我们可以使用媒体库管理模块medialibrary来查询公共媒体库中的视频文件,示例代码如下:
   
  1. import mediaLibrary from '@ohos.multimedia.mediaLibrary';
  2. async queryMediaVideo() {
  3. let option = {
  4. // 根据媒体类型检索
  5. selections: mediaLibrary.FileKey.MEDIA_TYPE + '=?',
  6. // 媒体类型为视频
  7. selectionArgs: [mediaLibrary.MediaType.VIDEO.toString()]
  8. };
  9. let media = mediaLibrary.getMediaLibrary(getContext(this));
  10. // 获取资源文件
  11. const fetchFileResult = await media.getFileAssets(option);
  12. // 以获取的第一个文件为例获取视频地址
  13. let fileAsset = await fetchFileResult.getFirstObject();
  14. this.source = fileAsset.uri
  15. }

为了方便功能演示,示例中媒体资源需存放在resources下的rawfile文件夹里。

  • currentProgressRate表示视频播放倍速,其参数类型为number,取值支持0.75,1.0,1.25,1.75,2.0,默认值为1.0倍速;
  • previewUri表示视频未播放时的预览图片路径;
  • controller表示视频控制器。

参数的具体描述如下表:

参数名

参数类型

必填

src

string | Resource

currentProgressRate

number | string | PlaybackSpeed8+

previewUri

string | PixelMap8+ | Resource

controller

VideoController

  说明

视频支持的规格是:mp4、mkv、webm、TS。

 

下面我们通过具体的例子来说明参数的使用方法,我们选择播放本地视频,视频未播放时的预览图片路径也为本地,代码实现如下:

   
  1. @Component
  2. export struct VideoPlayer {
  3. private source: string | Resource;
  4. private controller: VideoController;
  5. private previewUris: Resource = $r('app.media.preview');
  6. ...
  7. build() {
  8. Column() {
  9. Video({
  10. src: this.source,
  11. previewUri: this.previewUris,
  12. controller: this.controller
  13. })
  14. ...
  15. VideoSlider({ controller: this.controller })
  16. }
  17. }
  18. }

效果如下:

Video组件属性介绍

除了支持组件的尺寸设置、位置设置等通用属性外,Video组件还支持是否静音、是否自动播放、控制栏是否显示、视频显示模式以及单个视频是否循环播放五个私有属性。

名称

参数类型

描述

muted

boolean

是否静音。默认值:false

autoPlay

boolean

是否自动播放。默认值:false

controls

boolean

控制视频播放的控制栏是否显示。默认值:true

objectFit

ImageFit

设置视频显示模式。默认值:Cover

loop

boolean

是否单个视频循环播放。默认值:false

其中,objectFit 中视频显示模式包括Contain、Cover、Auto、Fill、ScaleDown、None 6种模式,默认情况下使用ImageFit.Cover(保持宽高比进行缩小或者放大,使得图片两边都大于或等于显示边界),其他效果(如自适应显示、保持原有尺寸显示、不保持宽高比进行缩放等)可以根据具体使用场景/设备来进行选择。

在Codelab示例中体现了controls、autoplay和loop属性的配置,示例代码如下:

   
  1. @Component
  2. export struct VideoPlayer {
  3. private source: string | Resource;
  4. private controller: VideoController;
  5. ...
  6. build() {
  7. Column() {
  8. Video({
  9. controller: this.controller
  10. })
  11. .controls(false) //不显示控制栏
  12. .autoPlay(false) // 手动点击播放
  13. .loop(false) // 关闭循环播放
  14. ...
  15. }
  16. }
  17. }

效果如下:

Video组件回调事件介绍

Video组件能够支持常规的点击、触摸等通用事件,同时也支持onStart、onPause、onFinish、onError等事件,具体事件的功能描述见下表:

事件名称

功能描述

onStart(event:() => void)

播放时触发该事件。

onPause(event:() => void)

暂停时触发该事件。

onFinish(event:() => void)

播放结束时触发该事件。

onError(event:() => void)

播放失败时触发该事件。

onPrepared(callback:(event?: { duration: number }) => void)

视频准备完成时触发该事件,通过duration可以获取视频时长,单位为s。

onSeeking(callback:(event?: { time: number }) => void)

操作进度条过程时上报时间信息,单位为s。

onSeeked(callback:(event?: { time: number }) => void)

操作进度条完成后,上报播放时间信息,单位为s。

onUpdate(callback:(event?: { time: number }) => void)

播放进度变化时触发该事件,单位为s,更新时间间隔为250ms。

onFullscreenChange(callback:(event?: { fullscreen: boolean }) => void)

在全屏播放与非全屏播放状态之间切换时触发该事件

在Codelab中我们以更新事件、准备事件、失败事件以及点击事件为回调为例进行演示,代码实现如下:

   
  1. Video({ ... })
  2. .onUpdate((event) => {
  3. this.currentTime = event.time;
  4. this.currentStringTime = changeSliderTime(this.currentTime); //更新事件
  5. })
  6. .onPrepared((event) => {
  7. prepared.call(this, event); //准备事件
  8. })
  9. .onError(() => {
  10. prompt.showToast({
  11. duration: COMMON_NUM_DURATION, //播放失败事件
  12. message: MESSAGE
  13. });
  14. ...
  15. })

其中,onUpdate更新事件在播放进度变化时触发,从event中可以获取当前播放进度,从而更新进度条显示事件,比如视频播放时间从24秒更新到30秒。onError事件在视频播放失败时触发,在CommonConstants.ets中定义了常量类MESSAGE,所以在视频播放失败时会显示“请检查网络”。

   
  1. const MESSAGE: string = '请检查网络'

自定义控制器的组成与实现

自定义控制器的组成

Video组件的原生控制器样式相对固定,当我们对页面的布局色调的一致性有所要求,或者在拖动进度条的同时需要显示其百分比进度时,原生控制器就无法满足需要了。如下图右侧的效果需要使用自定义控制器实现,接下来我们看一下自定义控制器的组成。

 

为了实现自定义控制器的进度显示等功能,我们需要通过Row容器实现控制器的整体布局,然后借由Text组件来显示视频的播放起始时间、进度时间以及视频总时长,最后通过滑动进度条Slider组件来实现视频进度条的效果,代码如下:

   
  1. @Component
  2. export struct VideoSlider {
  3. ...
  4. build() {
  5. Row(...) {
  6. Image(...)
  7. Text(...)
  8. Slider(...)
  9. Text(...)
  10. }
  11. ...
  12. }
  13. }

自定义控制器的实现

自定义控制器容器内嵌套了视频播放时间Text组件、滑动器Slider组件以及视频总时长Text组件 3个横向排列的组件,其中Text组件在之前的基础组件课程中已经有过详细介绍,这里就不再进行赘述。需要强调的是两个Text组件显示的时长是由Slider组件的onChange(callback: (value: number, mode: SliderChangeMode) => void)回调事件来进行传递的,而Text组件的数值与视频播放进度数值value则是通过@Provide与 @Consume装饰器进行的数据联动,实现效果可见图片下方黑色控制栏部分,具体代码步骤及代码如下:

获取/计算视频时长

   
  1. export function prepared(event) {
  2. this.durationTime = event.duration;
  3. let second: number = event.duration % COMMON_NUM_MINUTE;
  4. let min: number = parseInt((event.duration / COMMON_NUM_MINUTE).toString());
  5. let head = min < COMMON_NUM_DOUBLE ? `${ZERO_STR}${min}` : min;
  6. let end = second < COMMON_NUM_DOUBLE ? `${ZERO_STR}${second}` : second;
  7. this.durationStringTime = `${head}${SPLIT}${end}`;
  8. ...
  9. };

设置进度条参数及属性

   
  1. Slider({
  2. value: this.currentTime,
  3. min: 0,
  4. max: this.durationTime,
  5. step: 1,
  6. style: SliderStyle.OutSet
  7. })
  8. .blockColor($r('app.color.white'))
  9. .width(STRING_PERCENT.SLIDER_WITH)
  10. .trackColor(Color.Gray)
  11. .selectedColor($r('app.color.white'))
  12. .showSteps(true)
  13. .showTips(true)
  14. .trackThickness(this.isOpacity ? SMALL_TRACK_THICK_NESS : BIG_TRACK_THICK_NESS)
  15. .onChange((value: number, mode: SliderChangeMode) => {...})

计算当前进度播放时间及添加onUpdate回调

最后,在我们播放视频时还需要更新显示播放的时间进度,也就是左侧的Text组件。在视频开始播放前,播放时间默认为00:00,随着视频播放,时间需要不断更新为当前进度时间。所以左侧的Text组件我们不仅需要读取时间,还需要为其添加数据联动。这里,我们就是通过为Video组件添加onUpdate事件来实现的,在视频播放过程中会不断调用changeSliderTime方法获取当前的播放时间并进行计算及单位转化,从而不断刷新进度条的值,也就是控制器左侧的播放进度时间Text组件。

   
  1. Video({...})
  2. ...
  3. .onUpdate((event) => {
  4. this.currentTime = event.time;
  5. this.currentStringTime = changeSliderTime(this.currentTime)
  6. })
   
  1. export function changeSliderTime(value: number): string {
  2. let second: number = value % COMMON_NUM_MINUTE;
  3. let min: number = parseInt((value / COMMON_NUM_MINUTE).toString());
  4. let head = min < COMMON_NUM_DOUBLE ? `${ZERO_STR}${min}` : min;
  5. let end = second < COMMON_NUM_DOUBLE ? `${ZERO_STR}${second}` : second;
  6. let nowTime = `${head}${SPLIT}${end}`;
  7. return nowTime;
  8. };

指定视频播放进度及添加onChange事件回调

如需手动进行进度条的拖动,则需要在Slider组件中指定播放进度,并为Slider组件添加onChange事件回调。Slider滑动时就会触发该事件回调,从而实现将视频定位到进度条当前刷新位置,完成时长组件渲染与视频播放进度数据联动。

   
  1. Slider({...})
  2. .onChange((value: number, mode: SliderChangeMode) => {
  3. sliderOnchange.call(this, value, mode);
  4. })
   
  1. export function sliderOnchange(value: number, mode: SliderChangeMode) {
  2. this.currentTime = parseInt(value.toString());
  3. this.controller.setCurrentTime(parseInt(value.toString()), SeekMode.Accurate);
  4. ...
  5. };

到这里我们就实现了自定义控制器的构建,两个Text组件显示的时长是由Slider组件的onChange回调事件来进行传递的,而Text组件的数值与视频播放进度数值value则通过是onUpdate与onChange事件并借由@Provide @Consume装饰器进行的数据联动。

参考链接

  • Video组件的更多属性和参数的使用,可以参考API:Video

 

概述

在我们日常使用应用的时候,可能会进行一些敏感的操作,比如删除联系人,这时候我们给应用添加弹窗来提示用户是否需要执行该操作,如下图所示:

弹窗是一种模态窗口,通常用来展示用户当前需要的或用户必须关注的信息或操作。在弹出框消失之前,用户无法操作其他界面内容。ArkUI为我们提供了丰富的弹窗功能,弹窗按照功能可以分为以下两类:

  • 确认类:例如警告弹窗AlertDialog。
  • 选择类:包括文本选择弹窗TextPickerDialog 、日期滑动选择弹窗DatePickerDialog、时间滑动选择弹窗TimePickerDialog等。

您可以根据业务场景,选择不同类型的弹窗。部分弹窗效果图如下:

此外,如果上述弹窗还不能满足您的需求,或者需要对弹窗的布局和样式进行自定义,您还可以使用自定义弹窗CustomDialog。

下文将分别介绍AlertDialog 、TextPickerDialog 、DatePickerDialog以及CustomDialog的使用。

警告弹窗

警告弹窗AlertDialog由以下三部分区域构成,对应下面的示意图:

  1. 标题区:为可选的。
  2. 内容区:显示提示消息。
  3. 操作按钮区:用户做”确认“或者”取消“等操作。

以下示例代码,演示了如何使用AlertDialog 实现上图所示的警告弹窗。AlertDialog可以设置两个操作按钮,示例代码中分别使用primaryButton和secondaryButton实现了“取消”和“删除”操作按钮,操作按钮可以通过action响应点击事件。

   
  1. Button('点击显示弹窗')
  2. .onClick(() => {
  3. AlertDialog.show(
  4. {
  5. title: '删除联系人', // 标题
  6. message: '是否需要删除所选联系人?', // 内容
  7. autoCancel: false, // 点击遮障层时,是否关闭弹窗。
  8. alignment: DialogAlignment.Bottom, // 弹窗在竖直方向的对齐方式
  9. offset: { dx: 0, dy: -20 }, // 弹窗相对alignment位置的偏移量
  10. primaryButton: {
  11. value: '取消',
  12. action: () => {
  13. console.info('Callback when the first button is clicked');
  14. }
  15. },
  16. secondaryButton: {
  17. value: '删除',
  18. fontColor: '#D94838',
  19. action: () => {
  20. console.info('Callback when the second button is clicked');
  21. }
  22. },
  23. cancel: () => { // 点击遮障层关闭dialog时的回调
  24. console.info('Closed callbacks');
  25. }
  26. }
  27. )
  28. })

此外,您还可以使用AlertDialog,构建只包含一个操作按钮的确认弹窗,使用confirm响应操作按钮回调。

   
  1. AlertDialog.show(
  2. {
  3. title: '提示',
  4. message: '提示信息',
  5. autoCancel: true,
  6. alignment: DialogAlignment.Bottom,
  7. offset: { dx: 0, dy: -20 },
  8. confirm: {
  9. value: '确认',
  10. action: () => {
  11. console.info('Callback when confirm button is clicked');
  12. }
  13. },
  14. cancel: () => {
  15. console.info('Closed callbacks')
  16. }
  17. }
  18. )

选择类弹窗

选择类弹窗用于方便用户选择相关数据,比如选择喜欢吃的水果、出生日期等等。下面我们以TextPickerDialog和DatePickerDialog为例,来介绍选择类弹窗的使用。

文本选择弹窗

TextPickerDialog为文本滑动选择器弹窗,根据指定的选择范围创建文本选择器,展示在弹窗上,例如下面这段示例代码使用TextPickerDialog实现了一个水果选择弹窗。示例代码中使用selected指定了弹窗的初始选择项索引为2,对应的数据为“香蕉”。当用户点击“确定”操作按钮后,会触发onAccept事件回调,在回调中将选中的值,传递给宿主中的select变量。

   
  1. @Entry
  2. @Component
  3. struct TextPickerDialogDemo {
  4. @State select: number = 2;
  5. private fruits: string[] = ['苹果', '橘子', '香蕉', '猕猴桃', '西瓜'];
  6. build() {
  7. Column() {
  8. Button('TextPickerDialog')
  9. .margin(20)
  10. .onClick(() => {
  11. TextPickerDialog.show({
  12. range: this.fruits, // 设置文本选择器的选择范围
  13. selected: this.select, // 设置初始选中项的索引值。
  14. onAccept: (value: TextPickerResult) => { // 点击弹窗中的“确定”按钮时触发该回调。
  15. // 设置select为按下确定按钮时候的选中项index,这样当弹窗再次弹出时显示选中的是上一次确定的选项
  16. this.select = value.index;
  17. console.info("TextPickerDialog:onAccept()" + JSON.stringify(value));
  18. },
  19. onCancel: () => { // 点击弹窗中的“取消”按钮时触发该回调。
  20. console.info("TextPickerDialog:onCancel()");
  21. },
  22. onChange: (value: TextPickerResult) => { // 滑动弹窗中的选择器使当前选中项改变时触发该回调。
  23. console.info('TextPickerDialog:onChange()' + JSON.stringify(value));
  24. }
  25. })
  26. })
  27. }
  28. .width('100%')
  29. }
  30. }

效果图如下:

日期选择弹窗

下面我们介绍另一种常用的选择类弹窗DatePickerDialog,它是日期滑动选择器弹窗,根据指定的日期范围创建日期滑动选择器,展示在弹窗上。DatePickerDialog的使用非常广泛,比如当我们需要输入个人出生日期的时候,就可以使用DatePickerDialog。下面的示例代码实现了一个日期选择弹窗:

   
  1. @Entry
  2. @Component
  3. struct DatePickerDialogDemo {
  4. selectedDate: Date = new Date('2010-1-1');
  5. build() {
  6. Column() {
  7. Button("DatePickerDialog")
  8. .margin(20)
  9. .onClick(() => {
  10. DatePickerDialog.show({
  11. start: new Date('1900-1-1'), // 设置选择器的起始日期
  12. end: new Date('2023-12-31'), // 设置选择器的结束日期
  13. selected: this.selectedDate, // 设置当前选中的日期
  14. lunar: false,
  15. onAccept: (value: DatePickerResult) => { // 点击弹窗中的“确定”按钮时触发该回调
  16. // 通过Date的setFullYear方法设置按下确定按钮时的日期,这样当弹窗再次弹出时显示选中的是上一次确定的日期
  17. this.selectedDate.setFullYear(value.year, value.month, value.day)
  18. console.info('DatePickerDialog:onAccept()' + JSON.stringify(value))
  19. },
  20. onCancel: () => { // 点击弹窗中的“取消”按钮时触发该回调
  21. console.info('DatePickerDialog:onCancel()')
  22. },
  23. onChange: (value: DatePickerResult) => { // 滑动弹窗中的滑动选择器使当前选中项改变时触发该回调
  24. console.info('DatePickerDialog:onChange()' + JSON.stringify(value))
  25. }
  26. })
  27. })
  28. }
  29. .width('100%')
  30. }
  31. }

效果图如下:

自定义弹窗

自定义弹窗的使用更加灵活,适用于更多的业务场景,在自定义弹窗中您可以自定义弹窗内容,构建更加丰富的弹窗界面。自定义弹窗的界面可以通过装饰器@CustomDialog定义的组件来实现,然后结合CustomDialogController来控制自定义弹窗的显示和隐藏。下面我们通过一个兴趣爱好的选择框来介绍自定义弹窗的使用。

从上面的效果图可以看出,这个选择框是一个多选的列表弹窗,我们可以使用装饰器@CustomDialog,结合List组件来完成这个弹窗布局,实现步骤如下:

  1. 初始化弹窗数据。

    先准备好资源文件和数据实体类。其中资源文件stringarray.json创建在resources/base/element目录下,文件根节点为strarray。

       
    1. {
    2. "strarray": [
    3. {
    4. "name": "hobbies_data",
    5. "value": [
    6. {
    7. "value": "Soccer"
    8. },
    9. {
    10. "value": "Badminton"
    11. },
    12. {
    13. "value": "Travelling"
    14. },
    15. ...
    16. ]
    17. }
    18. ]
    19. }

    实体类HobbyBean用来封装自定义弹窗中的"兴趣爱好"数据。

       
    1. export default class HobbyBean {
    2. label: string;
    3. isChecked: boolean;
    4. }

    然后创建一个ArkTS文件CustomDialogWidget,用来封装自定义弹窗,使用装饰器@CustomDialog修饰CustomDialogWidget表示这是一个自定义弹窗。使用资源管理对象manager获取数据,并将数据封装到hobbyBeans。

       
    1. @CustomDialog
    2. export default struct CustomDialogWidget {
    3. @State hobbyBeans: HobbyBean[] = [];
    4. aboutToAppear() {
    5. let context: Context = getContext(this);
    6. let manager = context.resourceManager;
    7. manager.getStringArrayValue($r('app.strarray.hobbies_data'), (error, hobbyResult) => {
    8. ...
    9. hobbyResult.forEach((hobbyItem: string) => {
    10. let hobbyBean = new HobbyBean();
    11. hobbyBean.label = hobbyItem;
    12. hobbyBean.isChecked = false;
    13. this.hobbyBeans.push(hobbyBean);
    14. });
    15. });
    16. }
    17. build() {...}
    18. }
  2. 创建弹窗组件。 controller对象用于控制弹窗的控制和隐藏,hobbies表示弹窗选中的数据结果。setHobbiesValue方法用于筛选出被选中的数据,赋值给hobbies。    
    1. @CustomDialog
    2. export default struct CustomDialogWidget {
    3. @State hobbyBeans: HobbyBean[] = [];
    4. @Link hobbies: string;
    5. private controller?: CustomDialogController;
    6. aboutToAppear() {...}
    7. setHobbiesValue(hobbyBeans: HobbyBean[]) {
    8. let hobbiesText: string = '';
    9. hobbiesText = hobbyBeans.filter((isCheckItem: HobbyBean) =>
    10. isCheckItem?.isChecked)
    11. .map((checkedItem: HobbyBean) => {
    12. return checkedItem.label;
    13. }).join(',');
    14. this.hobbies = hobbiesText;
    15. }
    16. build() {
    17. Column() {
    18. Text($r('app.string.text_title_hobbies'))...
    19. List() {
    20. ForEach(this.hobbyBeans, (itemHobby: HobbyBean) => {
    21. ListItem() {
    22. Row() {
    23. Text(itemHobby.label)...
    24. Toggle({ type: ToggleType.Checkbox, isOn: false })...
    25. .onChange((isCheck) => {
    26. itemHobby.isChecked = isCheck;
    27. })
    28. }
    29. }
    30. }, itemHobby => itemHobby.label)
    31. }
    32. Row() {
    33. Button($r('app.string.cancel_button'))...
    34. .onClick(() => {
    35. this.controller?.close();
    36. })
    37. Button($r('app.string.definite_button'))...
    38. .onClick(() => {
    39. this.setHobbiesValue(this.hobbyBeans);
    40. this.controller?.close();
    41. })
    42. }
    43. }
    44. }
    45. }
  3. 使用自定义弹窗。

    在自定义弹窗的使用页面HomePage中先定义一个变量hobbies,使用装饰器@State修饰,和自定义弹窗中的@Link 装饰器修饰的变量进行双向绑定。然后我们使用alignment和offset设置弹窗的位置在屏幕底部,并且距离底部20vp。最后我们在自定义组件TextCommonWidget(具体实现可以参考《构建多种样式弹窗》Codelab源码)的点击事件中,调用customDialogController的open方法,用于显示弹窗。

       
    1. @Entry
    2. @Component
    3. struct HomePage {
    4. customDialogController: CustomDialogController = new CustomDialogController({
    5. builder: CustomDialogWidget({
    6. onConfirm: this.setHobbiesValue.bind(this),
    7. }),
    8. alignment: DialogAlignment.Bottom,
    9. customStyle: true,
    10. offset: { dx: 0,dy: -20 }
    11. });
    12. setHobbiesValue(hobbyArray: HobbyBean[]) {...}
    13. build() {
    14. ...
    15. TextCommonWidget({
    16. ...
    17. title: $r('app.string.title_hobbies'),
    18. content: $hobby,
    19. onItemClick: () => {
    20. this.customDialogController.open();
    21. }
    22. })
    23. ...
    24. }
    25. }

参考

关于更多弹窗,您可以参考:

警告弹窗

列表选择弹窗

自定义弹窗

日期滑动选择弹窗

时间滑动选择弹窗

文本滑动选择弹窗

 

 

介绍 本篇Codelab使用ArkTS语言实现视频播放器,主要包括主界面和视频播放界面,我们将一起完成以下功能:
  1. 主界面顶部使用Swiper组件实现视频海报轮播。

  2. 主界面下方使用List组件实现视频列表。

  3. 播放界面使用Video组件实现视频播放。

  4. 在不使用视频组件默认控制器的前提下,实现自定义控制器。

  5. 播放界面底部使用图标控制视频播放/暂停。

  6. 播放界面底部使用Slider组件控制和实现视频播放进度。

  7. 播放界面使用Stack容器组件的Z序控制在视频播放画面上展示开始/暂停/加载图标。

主界面中最近播放和为你推荐列表播放网络视频,需将CommonConstants.ets中的NET属性修改为网络视频地址。

相关概念

  • Swiper组件:滑块视图容器,提供子组件滑动轮播显示的能力。
  • List组件:列表包含一系列相同宽度的列表项。适合连续、多行呈现同类数据,例如图片和文本。
  • Video组件:用于播放视频文件并控制其播放状态的组件。
  • Navigator组件:路由容器组件,提供路由跳转能力。
  • ForEach组件:ForEach基于数组类型数据执行循环渲染。

完整示例

gitee源码地址

源码下载   简易视频播放器(ArkTS).zip   环境搭建 我们首先需要完成HarmonyOS开发环境搭建,可参照如下步骤进行。

软件要求

硬件要求

  • 设备类型:华为手机或运行在DevEco Studio上的华为手机设备模拟器。
  • HarmonyOS系统:3.1.0 Developer Release。

环境搭建

  1. 安装DevEco Studio,详情请参考下载和安装软件
  2. 设置DevEco Studio开发环境,DevEco Studio开发环境需要依赖于网络环境,需要连接上网络才能确保工具的正常使用,可以根据如下两种情况来配置开发环境:
    • 如果可以直接访问Internet,只需进行下载HarmonyOS SDK操作。
    • 如果网络不能直接访问Internet,需要通过代理服务器才可以访问,请参考配置开发环境
  3. 开发者可以参考以下链接,完成设备调试的相关配置:
  代码结构解读

本篇Codelab只对核心代码进行讲解,对于完整代码,我们会在源码下载或gitee中提供。

    No Preview
  1. ├──entry/src/main/ets // 代码区
  2. │ ├──common
  3. │ │ └──constants
  4. │ │ └──CommonConstants.ets // 样式常量类
  5. │ ├──entryability
  6. │ │ └──EntryAbility.ts // 程序入口类
  7. │ ├──model
  8. │ │ └──VideoControll.ets // 视频播放控制相关方法类
  9. │ ├──pages
  10. │ │ ├──SimpleVideoIndex.ets // 主界面
  11. │ │ └──SimpleVideoPlay.ets // 视频播放界面
  12. │ ├──view
  13. │ │ ├──IndexModule.ets // 自定义首页List模块组件文件
  14. │ │ ├──IndexSwiper.ets // 自定义首页Swiper组件文件
  15. │ │ ├──VideoPlayer.ets // 自定义播放界面视频组件文件
  16. │ │ └──VideoPlaySlider.ets // 自定义播放界面视频进度条组件文件
  17. │ └──viewmodel
  18. │ ├──HorizontalVideoItem.ets // 水平视频类
  19. │ ├──ParamItem.ets // 参数类
  20. │ ├──SwiperVideoItem.ets // banner视频类
  21. │ └──VideoData.ets // 首页相关数据
  22. └──entry/src/main/resource // 应用静态资源目录
构建主界面

主界面由视频轮播模块和多个视频列表模块组成,效果图如图:

VideoData.ets中定义的视频轮播图数组SWIPER_VIDEOS和视频列表图片数组HORIZONTAL_VIDEOS。 IndexSwiper.ets文件中定义的轮播图子组件SwiperVideo,点击轮播图片,页面跳转到视频播放页面,并携带本地视频flag,效果图如图:

IndexModule.ets文件中定义的视频列表图片子组件VideoModule,点击子组件中的图片,页面跳转到视频播放页面,并携带网络视频flag,效果图如图:

在SimpleVideoIndex.ets主界面中引用SwiperVideo和VideoModule子组件。     No Preview
  1. // SimpleVideoIndex.ets
  2. @Entry
  3. @Component
  4. struct SimpleVideoIndex {
  5. build() {
  6. Column({ space: MARGIN_FONT_SIZE.FOURTH_MARGIN }) {
  7. // 视频轮播组件
  8. SwiperVideo()
  9. List() {
  10. ForEach(LIST, (item: string) => {
  11. ListItem() {
  12. VideoModule({ moduleName: item })
  13. .margin({ top: MARGIN_FONT_SIZE.FIRST_MARGIN })
  14. }
  15. }, (item: string) => JSON.stringify(item))
  16. }
  17. .listDirection(Axis.Vertical)
  18. .margin({ top: MARGIN_FONT_SIZE.THIRD_MARGIN })
  19. }
  20. ...
  21. }
  22. }
构建视频播放界面

VideoPlayer.ets其中定义了视频播放子组件VideoPlayer ,onPrepared回调方法中可以获取视频总时长,onUpdate回调方法中可实时获取到视频播放的当前时间戳,onFinish是视频播放结束后的回调方法,onError是视频播放出错的回调方法。

在自定义组件VideoPlayer底部使用了自定义子组件VideoSlider,VideoSlider自定义组件中显示和控制视频播放进度,效果图如图:

在VideoController.ets中的视频控制和回调的相关方法。

在SimpleVideoPlay.ets播放界面,引用VideoPlayer子组件,并在视频播放页面使用堆叠容器,在视频播放画面中心堆叠控制、视频加载图标,效果图如图:

    Preview
  1. // SimpleVideoPlay.ets
  2. @Entry
  3. @Component
  4. struct Play {
  5. // 取到Index页面跳转来时携带的source对应的数据。
  6. private source: string = (router.getParams() as Record<string, Object>).source as string;
  7. private startIconResource: Resource = $r('app.media.ic_public_play');
  8. private backIconResource: Resource = $r('app.media.ic_back');
  9. @Provide isPlay: boolean = false;
  10. @Provide isOpacity: boolean = false;
  11. controller: VideoController = new VideoController();
  12. @Provide isLoading: boolean = false;
  13. @Provide progressVal: number = 0;
  14. @Provide flag: boolean = false;
  15. ...
  16. onPageHide() {
  17. this.controller.pause();
  18. }
  19. build() {
  20. Column() {
  21. // 顶部返回以及标题
  22.     ...
  23. Stack() {
  24. // 不同的播放状态渲染不同得控制图片
  25. if (!this.isPlay && !this.isLoading) {
  26. Image(this.startIconResource)
  27. .width(MARGIN_FONT_SIZE.FIFTH_MARGIN)
  28. .height(MARGIN_FONT_SIZE.FIFTH_MARGIN)
  29. // 同一容器中兄弟组件显示层级关系,z值越大,显示层级越高 用于控制图片在视频上。
  30. .zIndex(STACK_STYLE.IMAGE_Z_INDEX)
  31. }
  32. if (this.isLoading) {
  33. Progress({
  34. value: STACK_STYLE.PROGRESS_VALUE,
  35. total: STACK_STYLE.PROGRESS_TOTAL,
  36. type: ProgressType.ScaleRing
  37. })
  38. .color(Color.Grey)
  39. .value(this.progressVal)
  40. .width(STACK_STYLE.PROGRESS_WIDTH)
  41. .style({
  42. strokeWidth: STACK_STYLE.PROGRESS_STROKE_WIDTH,
  43. scaleCount: STACK_STYLE.PROGRESS_SCALE_COUNT,
  44. scaleWidth: STACK_STYLE.PROGRESS_SCALE_WIDTH
  45. })
  46. .zIndex(STACK_STYLE.PROGRESS_Z_INDEX)
  47. }
  48. VideoPlayer({
  49. source: this.source,
  50. controller: this.controller
  51. })
  52. .zIndex(0)
  53. }
  54. }
  55. .height(ALL_PERCENT)
  56. .backgroundColor(Color.Black)
  57. }
  58. }
总结

您已经完成了本次Codelab的学习,并了解到以下知识点:

  1. Swiper组件的使用。
  2. List组件的使用。
  3. Video组件的使用。
  4. Slider组件的使用。
  5. 如何实现自定义视频控制器。
介绍

本篇Codelab将介绍如何使用弹窗功能,实现四种类型弹窗。分别是:警告弹窗、自定义弹窗、日期滑动选择器弹窗、文本滑动选择器弹窗。需要完成以下功能:

  1. 点击左上角返回按钮展示警告弹窗。
  2. 点击出生日期展示日期滑动选择器弹窗。
  3. 点击性别展示文本滑动选择器弹窗。
  4. 点击兴趣爱好(多选)展示自定义弹窗。

相关概念

完整示例

gitee源码地址

源码下载   构建多种样式弹窗(ArkTS).zip   环境搭建

我们首先需要完成HarmonyOS开发环境搭建,可参照如下步骤进行。

软件要求

硬件要求

  • 设备类型:华为手机或运行在DevEco Studio上的华为手机设备模拟器。
  • HarmonyOS系统:3.1.0 Developer Release。

环境搭建

  1. 安装DevEco Studio,详情请参考下载和安装软件
  2. 设置DevEco Studio开发环境,DevEco Studio开发环境需要依赖于网络环境,需要连接上网络才能确保工具的正常使用,可以根据如下两种情况来配置开发环境:
    • 如果可以直接访问Internet,只需进行下载HarmonyOS SDK操作。
    • 如果网络不能直接访问Internet,需要通过代理服务器才可以访问,请参考配置开发环境
  3. 开发者可以参考以下链接,完成设备调试的相关配置:
  代码结构解读

本篇Codelab只对核心代码进行讲解,对于完整代码,我们会在源码下载或gitee中提供。

    No Preview
  1. ├──entry/src/main/ets // 代码区
  2. │ ├──common
  3. │ │ ├──constants
  4. │ │ │ └──CommonConstants.ets // 常量类
  5. │ │ └──utils
  6. │ │ ├──CommonUtils.ets // 弹窗操作工具类
  7. │ │ └──Logger.ets // 日志打印工具类
  8. │ ├──entryability
  9. │ │ └──EntryAbility.ets // 程序入口类
  10. │ ├──pages
  11. │ │ └──HomePage.ets // 主页面
  12. │ ├──view
  13. │ │ ├──CustomDialogWidget.ets // 自定义弹窗组件
  14. │ │ ├──TextCommonWidget.ets // 自定义Text组件
  15. │ │ └──TextInputWidget.ets // 自定义TextInput组件
  16. │ └──viewmodel
  17. │ └──HobbyItem.ets // 兴趣爱好类
  18. └──entry/src/main/resources // 资源文件目录
构建主页面

应用主页面采用Column容器嵌套自定义组件形式完成页面整体布局,效果如图所示:

从上面效果图可以看出,主界面由2个相同样式的文本输入框和3个相同样式的文本布局组成。我们可以将文本输入框抽取成TextInputWidget子组件。再将文本布局抽取成TextCommonWidget子组件。

在ets目录下,点击鼠标右键 > New > Directory,新建名为view的自定义子组件目录。然后在view目录下,点击鼠标右键 > New > ArkTS File,新建两个ArkTS文件,分别为TextInputWidget子组件、TextCommonWidget子组件。

文本输入框抽取成TextInputWidget子组件,效果如图所示:

文本布局抽取成TextCommonWidget子组件,效果如图所示:

在HomePage主界面引用TextInputWidget和TextCommonWidget子组件,然后初始化出生日期、性别、兴趣爱好默认数据。

    No Preview
  1. // HomePage.ets
  2. @Entry
  3. @Component
  4. struct HomePage {
  5. @State birthdate: string = '';
  6. @State sex: string = '';
  7. @State hobbies: string = '';
  8. ...
  9. build() {
  10. Column() {
  11. ...
  12. TextInputWidget({
  13. inputImage: $r('app.media.ic_nickname'),
  14. hintText: $r('app.string.text_input_hint')
  15. })
  16. TextCommonWidget({
  17. textImage: $r('app.media.ic_birthdate'),
  18. title: $r('app.string.title_birthdate'),
  19. content: $birthdate,
  20. onItemClick: () => {
  21. CommonUtils.datePickerDialog((birthValue: string) => {
  22. this.birthdate = birthValue;
  23. });
  24. }
  25. })
  26. TextCommonWidget({
  27. textImage: $r('app.media.ic_sex'),
  28. title: $r('app.string.title_sex'),
  29. content: $sex,
  30. onItemClick: () => {
  31. CommonUtils.textPickerDialog(this.sexArray, (sexValue: string) => {
  32. this.sex = sexValue;
  33. });
  34. }
  35. })
  36. TextInputWidget({
  37. inputImage: $r('app.media.ic_signature'),
  38. hintText: $r('app.string.text_input_signature')
  39. })
  40. TextCommonWidget({
  41. textImage: $r('app.media.ic_hobbies'),
  42. title: $r('app.string.title_hobbies'),
  43. content: $hobbies,
  44. onItemClick: () => {
  45. this.customDialogController.open();
  46. }
  47. })
  48. }
  49. ...
  50. }
  51. }
警告弹窗

点击主页面左上角返回按钮,通过CommonUtils.alertDialog方法弹出警告弹窗,提醒用户是否进行当前操作,效果如图所示:

    Preview
  1. // CommonUtils.ets
  2. alertDialog(context: Context.UIAbilityContext) {
  3. AlertDialog.show({
  4. // 提示信息
  5. message: $r('app.string.alert_dialog_message'),
  6. // 弹窗显示位置
  7. alignment: DialogAlignment.Bottom,
  8. // 弹窗偏移位置
  9. offset: {
  10. dx: 0,
  11. dy: CommonConstants.DY_OFFSET
  12. },
  13. primaryButton: {
  14. value: $r('app.string.cancel_button'),
  15. action: () => {
  16. ...
  17. }
  18. },
  19. secondaryButton: {
  20. value: $r('app.string.definite_button'),
  21. action: () => {
  22. // 退出应用
  23. context.terminateSelf();
  24. ...
  25. }
  26. }
  27. });
  28. }
日期滑动选择器弹窗

点击出生日期选项,通过CommonUtils.datePickerDialog方法弹出日期选择器弹窗,根据需要选择相应时间,效果如图所示:

    Preview
  1. // CommonUtils.ets
  2. datePickerDialog(dateCallback: Function) {
  3. DatePickerDialog.show({
  4. // 开始时间
  5. start: new Date(CommonConstants.START_TIME),
  6. // 结束时间
  7. end: new Date(),
  8. // 当前选中时间
  9. selected: new Date(CommonConstants.SELECT_TIME),
  10. // 是否显示农历
  11. lunar: false,
  12. onAccept: (value: DatePickerResult) => {
  13. let year: number = Number(value.year);
  14. let month: number = Number(value.month) + CommonConstants.PLUS_ONE;
  15. let day: number = Number(value.day);
  16. let birthdate: string = this.getBirthDateValue(year, month, day);
  17. dateCallback(birthdate);
  18. }
  19. });
  20. }
  21. // 获取出生日期值
  22. getBirthDateValue(year: number, month: number, day: number): string {
  23. let birthdate: string = `${year}${CommonConstants.DATE_YEAR}${month}` +
  24. `${CommonConstants.DATE_MONTH}${day}${CommonConstants.DATE_DAY}`;
  25. return birthdate;
  26. }
  27. // HomePage.ets
  28. build() {
  29. Column() {
  30. ...
  31. TextCommonWidget({
  32. textImage: $r('app.media.ic_birthdate'),
  33. title: $r('app.string.title_birthdate'),
  34. content: $birthdate,
  35. onItemClick: () => {
  36. CommonUtils.datePickerDialog((birthValue: string) => {
  37. this.birthdate = birthValue;
  38. });
  39. }
  40. })
  41. ...
  42. }
  43. ...
  44. }
文本滑动选择器弹窗

点击性别选项,通过CommonUtils.textPickerDialog方法弹出性别选择器弹窗,根据需要选择相应性别,效果如图所示:

    Preview
  1. // CommonUtils.ets
  2. textPickerDialog(sexArray: Resource, sexCallback: Function) {
  3. ...
  4. TextPickerDialog.show({
  5. range: sexArray,
  6. selected: 0,
  7. onAccept: (result: TextPickerResult) => {
  8. sexCallback(result.value);
  9. },
  10. onCancel: () => {
  11. ...
  12. }
  13. });
  14. }
  15. // HomePage.ets
  16. build() {
  17. Column() {
  18. ...
  19. TextCommonWidget({
  20. textImage: $r('app.media.ic_sex'),
  21. title: $r('app.string.title_sex'),
  22. content: $sex,
  23. onItemClick: () => {
  24. CommonUtils.textPickerDialog(this.sexArray, (sexValue: string) => {
  25. this.sex = sexValue;
  26. });
  27. }
  28. })
  29. ...
  30. }
  31. ...
  32. }
自定义弹窗

点击兴趣爱好选项,通过customDialogController.open方法弹出自定义弹窗,根据需要选择相应的兴趣爱好,效果如图所示:

在view目录下,点击鼠标右键 > New > ArkTS File,新建一个ArkTS文件,然后命名为CustomDialogWidget子组件。

在CustomDialogWidget的aboutToAppear方法,通过manager.getStringArrayValue方法获取本地资源数据进行初始化。

当用户点击确定按钮时,通过setHobbiesValue方法处理自定义弹窗选项结果。

通过@Link修饰的hobbies把值赋给HomePage的hobbies,然后hobbies刷新显示内容。

    No Preview
  1. // HomePage.ets
  2. @State hobbies: string = '';
  3. customDialogController: CustomDialogController = new CustomDialogController({
  4. builder: CustomDialogWidget({
  5. hobbies: $hobbies
  6. }),
  7. alignment: DialogAlignment.Bottom,
  8. customStyle: true,
  9. offset: {
  10. dx: 0,
  11. dy: CommonConstants.DY_OFFSET
  12. }
  13. });
  14. build() {
  15. Column() {
  16. ...
  17. TextCommonWidget({
  18. textImage: $r('app.media.ic_hobbies'),
  19. title: $r('app.string.title_hobbies'),
  20. content: $hobbies,
  21. onItemClick: () => {
  22. // 打开自定义弹窗
  23. this.customDialogController.open();
  24. }
  25. })
  26. }
  27. ...
  28. }
总结

您已经完成了本次Codelab的学习,并了解到以下知识点:

  1. 使用CustomDialogController实现自定义弹窗。
  2. 使用AlertDialog实现警告弹窗。
  3. 使用DatePickerDialog实现日期滑动选择弹窗。
  4. 使用TextPickerDialog实现文本滑动选择弹窗。

标签:状态,自定义,...,视频,管理,组件,弹窗,ets
From: https://www.cnblogs.com/flyingsir/p/17978367

相关文章

  • 案例:常用组件与布局
    介绍HarmonyOSArkUI提供了丰富多样的UI组件,您可以使用这些组件轻松地编写出更加丰富、漂亮的界面。在本篇Codelab中,您将通过一个简单的购物社交应用示例,学习如何使用常用的基础组件和容器组件。本示例主要包含:“登录”、“首页”、“我的”三个页面。相关概念Text:显......
  • List组件和Grid组件的使用
    简介在我们常用的手机应用中,经常会见到一些数据列表,如设置页面、通讯录、商品列表等。下图中两个页面都包含列表,“首页”页面中包含两个网格布局,“商城”页面中包含一个商品列表。上图中的列表中都包含一系列相同宽度的列表项,连续、多行呈现同类数据,例如图片和文本。常见的列......
  • 基于SSM的双星小区物业管理系统的设计与实现
    传统办法管理双星小区物业信息首先需要花费的时间比较多,其次数据出错率比较高,而且对错误的数据进行更改也比较困难,最后,检索数据费事费力。因此,在计算机上安装双星小区物业管理系统软件来发挥其高效地信息处理的作用,可以规范双星小区物业信息管理流程,让管理工作可以系统化和程序化,同......
  • 基于SSM的电动车上牌管理系统的设计与实现
    课题背景早在20世纪80年代,美国就已经开始发展电子商务行业,良好的经济,完备的技术和稳定的社会条件,为信息化管理行业的发展提供了一种很好的发展氛围。1999年,为了每一个需要的用户都实现上网,欧盟委员会制定了电子欧洲计划。相比于国外,我国信息化管理出现的比较晚,但是相关的技术人员......
  • docker镜像管理
    1.查看镜像[root@centos201~]#dockerimagels#查看现有的镜像列表。REPOSITORYTAGIMAGEIDCREATEDSIZEhello-worldlatestfeb5d9fea6a520monthsago13.3kB[root@centos201~]#[root@centos201~]#[root@centos201~]#doc......
  • 基于SSM的公务用车管理智慧云服务监管平台查询统计
    随着信息互联网购物的飞速发展,一般企业都去创建属于自己的管理系统。本文介绍了公务用车管理智慧云服务监管平台的开发全过程。通过分析企业对于公务用车管理智慧云服务监管平台的需求,创建了一个计算机管理公务用车管理智慧云服务监管平台的方案。文章介绍了公务用车管理智慧云服务......
  • 基于SSM的广告管理系统
    随着信息互联网购物的飞速发展,一般企业都去创建属于自己的管理系统。本文介绍了广告管理系统的开发全过程。通过分析企业对于广告管理系统的需求,创建了一个计算机管理广告管理系统的方案。文章介绍了广告管理系统的系统分析部分,包括可行性分析等,系统设计部分主要介绍了系统功能设计......
  • cms和pms分别是什么意思?还有哪些常见的管理系统?
    CMSCMS是内容管理系统的缩写,全称为ContentManagementSystem。它是一种软件工具或平台,用于创建、编辑、组织和发布数字内容,如网站页面、文章、图片、视频等。CMS的主要目的是简化内容管理过程,使非技术人员能够轻松管理和更新网站内容,而无需编写代码或了解复杂的技术知识。通过......
  • i-MES生产制造管理系统-生产过程检验SPC(一)
    说起质量管理,那一定少不了 SPC,SPC中文名叫统计过程控制,对生产过程中记录的数据进行分析,及时了解不良情况出现的几率,并采取必要的措施达到消除影响的目的,这其中有几个关键术语,比如UCL等.  为了方便检验人员操作,SPC模块运行在Android平板电脑上面,检验人员在生产过程中进......
  • Verdi信号平移+研发管理体系+malloc和calloc函数区别+使用__FILE__只打印文件名+使用i
    Verdi信号平移信号左移是将光标移动在双引号以内的信号名左边,然后先输入数字,可以带上单位,如[ns|n]、[ps|p],然后按<<-按键。https://blog.csdn.net/qq_40268672/article/details/132915499信号右移信号右移是数字在右边,信号在左边,用右移符号,其它不变。研发管理体系https://......