概述
在应用中,界面通常都是动态的。如图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状态变化,列表项会自动更新。
- @Component
- export default struct TargetListItem {
- @State isExpanded: boolean = false;
- ...
- build() {
- ...
- Column() {
- ...
- if (this.isExpanded) {
- Blank()
- ProgressEditPanel(...)
- }
- }
- .height(this.isExpanded ? $r('app.float.expanded_item_height')
- : $r('app.float.list_item_height'))
- .onClick(() => {
- ...
- this.isExpanded = !this.isExpanded;
- ...
- })
- ...
- }
- }
从父组件单向同步状态:@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,定义编辑模式状态。然后利用条件渲染实现根据是否进入编辑模式,显示不同的文本和按钮。同时,在父组件中需要在用户点击时改变状态,触发界面更新。
当点击“编辑”事件发生时,进入编辑模式,显示取消、全选文本和勾选框,同时显示删除按钮;当点击“取消”事件发生时,退出编辑模式,显示“编辑”文本和“添加子目标”按钮。
- @Component
- export default struct TargetList {
- @State isEditMode: boolean = false;
- ...
- build() {
- Column() {
- Row() {
- ...
- if (this.isEditMode) {
- Text($r('app.string.cancel_button'))
- .onClick(() => {
- this.isEditMode = false;
- ...
- })
- ...
- Text($r('app.string.select_all_button'))...
- Checkbox()...
- } else {
- Text($r('app.string.edit_button'))
- .onClick(() => {
- this.isEditMode = true;
- })
- ...
- }
- ...
- }
- ...
- List({ space: CommonConstants.LIST_SPACE }) {
- ForEach(this.targetData, (item: TaskItemBean, index: number) => {
- ListItem() {
- TargetListItem({
- isEditMode: this.isEditMode,
- ...
- })
- }
- }, (item, index) => JSON.stringify(item) + index)
- }
- ...
- if (this.isEditMode) {
- Button($r('app.string.delete_button'))
- } else {
- Button($r('app.string.add_task'))
- }
- }
- ...
- }
- }
在子组件TargetListItem中,使用@Prop修饰子组件的isEditMode变量,定义子组件的编辑模式状态。然后同样根据是否进入编辑模式,控制目标项最右侧是否预留位置和显示勾选框。
- @Component
- export default struct TargetListItem {
- @Prop isEditMode: boolean;
- ...
- Column() {
- ...
- }
- .padding({
- ...
- right: this.isEditMode ? $r('app.float.list_edit_padding')
- : $r('app.float.list_padding')
- })
- ...
- if (this.isEditMode) {
- Row() {
- Checkbox()...
- }
- }
- ...
- }
最后,最关键的一步就是要在父组件中使用子组件时,将父组件的编辑模式状态this.isEditMode传递给子组件的编辑模式状态isEditMode。
- @Component
- export default struct TargetList {
- @State isEditMode: boolean = false;
- ...
- build() {
- Column() {
- ...
- List({ space: CommonConstants.LIST_SPACE }) {
- ForEach(this.targetData, (item: TaskItemBean, index: number) => {
- ListItem() {
- TargetListItem({
- isEditMode: this.isEditMode,
- ...
- })
- }
- }, (item, index) => JSON.stringify(item) + index)
- }
- ...
- }
- ...
- }
- }
与父组件双向同步状态:@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建立双向同步关系。
- @Component
- export default struct TargetList {
- @State clickIndex: number = CommonConstants.DEFAULT_CLICK_INDEX;
- ...
- TargetListItem({
- clickIndex: $clickIndex,
- ...
- })
- ...
- }
首先,在父组件TargetList中用@State装饰器定义点击的目标项索引状态。然后,在子组件TargetListItem中用@Link装饰器定义clickIndex,当点击目标项时,clickIndex更新为当前目标索引值。
完成在父子组件中定义状态后,最关键的就是要建立父子组件的双向关联关系。在父组件中使用子组件时,将父组件的clickIndex传递给子组件的clickIndex。其中父组件的clickIndex加上$表示传递的是引用。
- @Component
- export default struct TargetListItem {
- @Link @Watch('onClickIndexChanged') clickIndex: number;
- @State isExpanded: boolean = false
- ...
- onClickIndexChanged() {
- if (this.clickIndex != this.index) {
- this.isExpanded = false;
- }
- }
- build() {
- ...
- Column() {
- ...
- }
- .onClick(() => {
- ...
- this.clickIndex = this.index;
- ...
- })
- ...
- }
- }
当目标一感知到点击了目标三时,还需要将目标一收起,切换列表项的功能才是完整的。此时,目标一感知到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管理页面级变量的状态,实现对页面数据的增加、删除、修改。要求完成以下功能:
- 实现一个自定义弹窗,完成添加子目标的功能。
- 实现一个可编辑列表,可点击指定行展开调节工作目标进度,可多选、全选删除指定行。
相关概念
完整示例
源码下载 目标管理(ArkTS).zip 环境搭建 我们首先需要完成HarmonyOS开发环境搭建,可参照如下步骤进行。软件要求
- DevEco Studio版本:DevEco Studio 3.1 Release。
- HarmonyOS SDK版本:API version 9。
硬件要求
- 设备类型:华为手机或运行在DevEco Studio上的华为手机设备模拟器。
- HarmonyOS系统:3.1.0 Developer Release。
环境搭建
- 安装DevEco Studio,详情请参考下载和安装软件。
- 设置DevEco Studio开发环境,DevEco Studio开发环境需要依赖于网络环境,需要连接上网络才能确保工具的正常使用,可以根据如下两种情况来配置开发环境:
- 如果可以直接访问Internet,只需进行下载HarmonyOS SDK操作。
- 如果网络不能直接访问Internet,需要通过代理服务器才可以访问,请参考配置开发环境。
- 开发者可以参考以下链接,完成设备调试的相关配置:
本篇Codelab只对核心代码进行讲解,对于完整代码,我们会在源码下载或gitee中提供。
No Preview- ├──entry/src/main/ets // ArkTS代码区
- │ ├──common
- │ │ ├──constants
- │ │ │ └──CommonConstants.ets // 公共常量类
- │ │ └──utils
- │ │ ├──DateUtil.ets // 获取格式化日期工具
- │ │ └──Logger.ets // 日志打印工具类
- │ ├──entryability
- │ │ └──EntryAbility.ts // 程序入口类
- │ ├──pages
- │ │ └──MainPage.ets // 主页面
- │ ├──view
- │ │ ├──TargetInformation.ets // 整体目标详情自定义组件
- │ │ ├──AddTargetDialog.ets // 自定义弹窗
- │ │ ├──ProgressEditPanel.ets // 进展调节自定义组件
- │ │ ├──TargetList.ets // 工作目标列表
- │ │ └──TargetListItem.ets // 工作目标列表子项
- │ └──viewmodel
- │ └──DataModel.ets // 工作目标数据操作类
- └──entry/src/main/resources // 资源文件目录
MainPage作为本应用的主界面,从上至下由三个自定义组件组成。
- 标题titleBar。
- 目标整体进展详情TargetInformation。
- 子目标列表TargetList。
MainPage主要维护五个参数:子目标数组targetData、子目标总数totalTasksNumber、已完成子目标数completedTasksNumber、最近更新时间latestUpdateDate、监听数据变化的参数overAllProgressChanged。具体作用有以下三个方面:
- 子组件TargetInformation接收三个参数totalTasksNumber、completedTasksNumber、latestUpdateDate,渲染整体目标详情。
- 子组件TargetList接收参数targetData渲染列表。
- 使用@Watch监听overAllProgressChanged的变化。当overAllProgressChanged改变时,回调onProgressChanged方法,刷新整体进展TargetInformation。
- // MainPage.ets
- @Entry
- @Component
- struct MainPage {
- // 子目标数组
- @State targetData: Array<TaskItemBean> = DataModel.getData();
- // 子目标总数
- @State totalTasksNumber: number = 0;
- // 已完成子目标数
- @State completedTasksNumber: number = 0;
- // 最近更新时间
- @State latestUpdateDate: string = CommonConstants.DEFAULT_PROGRESS_VALUE;
- // 监听数据变化的参数
- @Provide @Watch('onProgressChanged') overAllProgressChanged: boolean = false;
- ...
- /**
- * overAllProgressChanged改变时的回调
- */
- onProgressChanged() {
- this.totalTasksNumber = this.targetData.length;
- this.completedTasksNumber = this.targetData.filter((item: TaskItemBean) => {
- return item.progressValue === CommonConstants.SLIDER_MAX_VALUE;
- }).length;
- this.latestUpdateDate = getCurrentTime();
- }
- build() {
- Column() {
- // 标题
- this.titleBar()
- // 目标整体进展详情
- TargetInformation({
- latestUpdateDate: this.latestUpdateDate,
- totalTasksNumber: this.totalTasksNumber,
- completedTasksNumber: this.completedTasksNumber
- })
- // 子目标列表
- TargetList({
- targetData: $targetData,
- onAddClick: (): void => this.dialogController.open()
- })
- ...
- }
- ...
- }
- @Builder
- titleBar() {
- Text($r('app.string.title'))
- ...
- }
- }
本章节主要介绍如何实现一个自定义弹窗,完成添加子目标的功能。效果如图所示:
在MainPage.ets中,创建dialogController对象控制弹窗隐显,传入自定义组件AddTargetDialog和点击确定的回调方法saveTask。
在AddTargetDialog.ets中,参数onClickOk为function类型,接收MainPage传入的saveTask方法。点击确定,调用onClickOk执行saveTask方法,关闭弹窗。
在MainPage.ets中,实现saveTask方法:保存数据至DataModel中,并更新targetData的值,完成添加子目标功能。 No Preview- // MainPage.ets
- @Entry
- @Component
- struct MainPage {
- ...
- saveTask(taskName: string) {
- if (taskName === '') {
- promptAction.showToast({
- message: $r('app.string.cannot_input_empty'),
- duration: CommonConstants.TOAST_TIME,
- bottom: CommonConstants.TOAST_MARGIN_BOTTOM
- });
- return;
- }
- // 保存数据
- DataModel.addData(new TaskItemBean(taskName, 0, getCurrentTime()));
- // 更新targetData刷新页面
- this.targetData = DataModel.getData();
- this.overAllProgressChanged = !this.overAllProgressChanged;
- this.dialogController.close();
- }
- }
本章节主要介绍子目标列表TargetList的实现,包括以下功能:
- 列表项展开。
- 列表子项点击下拉,滑动滑块更新进展。
- 列表进入编辑状态,单选、多选、全选、删除子项。
实现以下步骤完成点击列表项展开功能:
- 使用@State 管理参数isExpanded,表示当前项是否展开,具体表现为自定义组件ProgressEditPanel的显示或隐藏。
- 使用@Link和@Watch管理参数clickIndex,表示当前点击ListItem的Index索引。clickIndex值的改变将会传递至所有的ListItem。
- 完成onClick点击事件,将isExpanded 值置反,修改clickIndex值为当前点击的索引。
- // TargetListItem.ets
- @Component
- export default struct TargetListItem {
- @State latestProgress?: number = 0;
- @Link @Watch('onClickIndexChanged') clickIndex: number;
- @State isExpanded: boolean = false;
- ...
- // clickIndex改变的回调方法
- onClickIndexChanged() {
- if (this.clickIndex !== this.index) {
- this.isExpanded = false;
- }
- }
- build() {
- ...
- Column() {
- this.TargetItem()
- if (this.isExpanded) {
- Blank()
- // 自定义组件:编辑面板
- ProgressEditPanel({
- slidingProgress: this.latestProgress,
- onCancel: () => this.isExpanded = false,
- onClickOK: (progress: number): void => {
- this.latestProgress = progress;
- this.updateDate = getCurrentTime();
- let result = DataModel.updateProgress(this.index, this.latestProgress, this.updateDate);
- if (result) {
- this.overAllProgressChanged = !this.overAllProgressChanged;
- }
- this.isExpanded = false;
- },
- sliderMode: $sliderMode
- })
- ...
- }
- }
- ...
- .onClick(() => {
- ...
- if (!this.isEditMode) {
- animateTo({ duration: CommonConstants.DURATION }, () => {
- this.isExpanded = !this.isExpanded;
- })
- this.clickIndex = this.index;
- }
- })
- }
- ...
- }
列表某项被展开后,实现以下步骤完成更新进展功能:
- Slider实现滑动条,滑动滑块调节进展,使用slidingProgress保存滑动值。
- 点击确定调用onClickOK方法,将数据slidingProgress回调至TargetListItem。
- 在TargetListItem中获取回调的数据并刷新页面。
在TargetListItem.ets中,完成onClickOK方法的实现,将依次完成以下步骤。
- 重新渲染TargetListItem的进度值和最近更新时间。
- 更新缓存的数据。
- 修改overAllProgressChanged的值,通知主页刷新整体进展详情TargetInformation。
- // TargetListItem.ets
- @Component
- export default struct TargetListItem {
- ...
- build() {
- ...
- Column() {
- ...
- if (this.isExpanded) {
- Blank()
- // 自定义组件:编辑面板
- ProgressEditPanel({
- ...
- onClickOK: (progress: number): void => {
- this.latestProgress = progress;
- this.updateDate = getCurrentTime();
- let result = DataModel.updateProgress(this.index, this.latestProgress, this.updateDate);
- if (result) {
- this.overAllProgressChanged = !this.overAllProgressChanged;
- }
- this.isExpanded = false;
- },
- ...
- })
- ...
- }
- }
- }
- }
列表进入编辑模式才可单选、多选。实现以下步骤完成列表多选功能:
- 维护一个boolean类型的数组selectArray,其长度始终与数据列表的长度相等,且初始值均为false。表示进入编辑状态时列表均未选中。
- 定义一个boolean类型的值isEditMode,表示是否进入了编辑模式。
- TargetListItem选中状态的初始化和点击Checkbox改变TargetListItem的选中状态。
点击全选Checkbox,将selectArray数组的值全赋值true或false,重新渲染列表为全选或者取消全选状态。
在TargetListItem中,实现以下步骤改变ListItem的选中状态:
- 使用@Link定义selectArr数组接收TargetList传入的selectArray。
- 在TargetListItem渲染时,使用this.selectArr[this.index]获取初始选中状态。
- 点击Checkbox时,按照当前ListItem的索引,将选中状态保存至selectArr,重新渲染列表完成单选和多选功能
- // TargetListItem.ets
- export default struct TargetListItem {
- ...
- @Link selectArr: Array<boolean>;
- public index: number = 0;
- build() {
- Stack({ alignContent: Alignment.Start }) {
- ...
- this.TargetItem()
- ...
- Checkbox()
- // 获取初始选中状态
- .select(this.selectArr[this.index])
- ...
- .onChange((isCheck: boolean) => {
- // 改变被点击项的选中状态
- this.selectArr[this.index] = isCheck;
- })
- ...
- ...
- }
- }
- }
当点击“删除”时,调用TargetList.ets的deleteSelected方法,实现以下步骤完成列表项删除功能:
- 调用DataModel的deleteData方法删除数据。
- 更新targetData的数据重新渲染列表。
- 修改overAllProgressChanged的值,通知主页刷新整体进展详情TargetInformation。
- // DataModel.ets
- export class DataModel {
- ...
- // 删除选中的数据
- deleteData(selectArr: Array<boolean>) {
- if (!selectArr) {
- Logger.error(TAG, 'Failed to delete data because selectArr is ' + selectArr);
- }
- let dataLen = this.targetData.length - CommonConstants.ONE_TASK;
- for (let i = dataLen; i >= 0; i--) {
- if (selectArr[i]) {
- this.recordData.splice(i, CommonConstants.ONE_TASK);
- }
- }
- }
- ...
- }
您已经完成了本次Codelab的学习,并了解到以下知识点:
- @State、@Prop、@Link、@Watch、@Provide、@Consume的使用。
- List组件的使用。
- 自定义弹窗的使用。
- Slider组件的使用。
概述
在手机、平板或是智慧屏这些终端设备上,媒体功能可以算作是我们最常用的场景之一。无论是实现音频的播放、录制、采集,还是视频的播放、切换、循环,亦或是相机的预览、拍照等功能,媒体组件都是必不可少的。以视频功能为例,在应用开发过程中,我们需要通过ArkUI提供的Video组件为应用增加基础的视频播放功能。借助Video组件,我们可以实现视频的播放功能并控制其播放状态。常见的视频播放场景包括观看网络上的较为流行的短视频,也包括查看我们存储在本地的视频内容。
本文将结合《简易视频播放器(ArkTS)》这个Codelab,对Video组件的参数、属性及事件进行介绍,然后通过组件的属性调用和事件回调阐明Video组件的基本使用方法,最后结合Video组件使用过程中的常见问题讲解自定义控制器的使用。
Video组件用法介绍
Video组件参数介绍
Video组件的接口表达形式为:
- Video(value: {src?: string | Resource, currentProgressRate?: number | string |PlaybackSpeed, previewUri?: string |PixelMap | Resource, controller?: VideoController})
其中包含四个可选参数,src、currentProgressRate、previewUri和controller。
- src表示视频播放源的路径,可以支持本地视频路径和网络路径。使用网络地址时,如https,需要注意的是需要在module.json5文件中申请网络权限。在使用本地资源播放时,当使用本地视频地址我们可以使用媒体库管理模块medialibrary来查询公共媒体库中的视频文件,示例代码如下:
- import mediaLibrary from '@ohos.multimedia.mediaLibrary';
- async queryMediaVideo() {
- let option = {
- // 根据媒体类型检索
- selections: mediaLibrary.FileKey.MEDIA_TYPE + '=?',
- // 媒体类型为视频
- selectionArgs: [mediaLibrary.MediaType.VIDEO.toString()]
- };
- let media = mediaLibrary.getMediaLibrary(getContext(this));
- // 获取资源文件
- const fetchFileResult = await media.getFileAssets(option);
- // 以获取的第一个文件为例获取视频地址
- let fileAsset = await fetchFileResult.getFirstObject();
- this.source = fileAsset.uri
- }
为了方便功能演示,示例中媒体资源需存放在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。
下面我们通过具体的例子来说明参数的使用方法,我们选择播放本地视频,视频未播放时的预览图片路径也为本地,代码实现如下:
- @Component
- export struct VideoPlayer {
- private source: string | Resource;
- private controller: VideoController;
- private previewUris: Resource = $r('app.media.preview');
- ...
- build() {
- Column() {
- Video({
- src: this.source,
- previewUri: this.previewUris,
- controller: this.controller
- })
- ...
- VideoSlider({ controller: this.controller })
- }
- }
- }
效果如下:
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属性的配置,示例代码如下:
- @Component
- export struct VideoPlayer {
- private source: string | Resource;
- private controller: VideoController;
- ...
- build() {
- Column() {
- Video({
- controller: this.controller
- })
- .controls(false) //不显示控制栏
- .autoPlay(false) // 手动点击播放
- .loop(false) // 关闭循环播放
- ...
- }
- }
- }
效果如下:
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中我们以更新事件、准备事件、失败事件以及点击事件为回调为例进行演示,代码实现如下:
- Video({ ... })
- .onUpdate((event) => {
- this.currentTime = event.time;
- this.currentStringTime = changeSliderTime(this.currentTime); //更新事件
- })
- .onPrepared((event) => {
- prepared.call(this, event); //准备事件
- })
- .onError(() => {
- prompt.showToast({
- duration: COMMON_NUM_DURATION, //播放失败事件
- message: MESSAGE
- });
- ...
- })
其中,onUpdate更新事件在播放进度变化时触发,从event中可以获取当前播放进度,从而更新进度条显示事件,比如视频播放时间从24秒更新到30秒。onError事件在视频播放失败时触发,在CommonConstants.ets中定义了常量类MESSAGE,所以在视频播放失败时会显示“请检查网络”。
- const MESSAGE: string = '请检查网络'
自定义控制器的组成与实现
自定义控制器的组成
Video组件的原生控制器样式相对固定,当我们对页面的布局色调的一致性有所要求,或者在拖动进度条的同时需要显示其百分比进度时,原生控制器就无法满足需要了。如下图右侧的效果需要使用自定义控制器实现,接下来我们看一下自定义控制器的组成。
为了实现自定义控制器的进度显示等功能,我们需要通过Row容器实现控制器的整体布局,然后借由Text组件来显示视频的播放起始时间、进度时间以及视频总时长,最后通过滑动进度条Slider组件来实现视频进度条的效果,代码如下:
- @Component
- export struct VideoSlider {
- ...
- build() {
- Row(...) {
- Image(...)
- Text(...)
- Slider(...)
- Text(...)
- }
- ...
- }
- }
自定义控制器的实现
自定义控制器容器内嵌套了视频播放时间Text组件、滑动器Slider组件以及视频总时长Text组件 3个横向排列的组件,其中Text组件在之前的基础组件课程中已经有过详细介绍,这里就不再进行赘述。需要强调的是两个Text组件显示的时长是由Slider组件的onChange(callback: (value: number, mode: SliderChangeMode) => void)回调事件来进行传递的,而Text组件的数值与视频播放进度数值value则是通过@Provide与 @Consume装饰器进行的数据联动,实现效果可见图片下方黑色控制栏部分,具体代码步骤及代码如下:
获取/计算视频时长
- export function prepared(event) {
- this.durationTime = event.duration;
- let second: number = event.duration % COMMON_NUM_MINUTE;
- let min: number = parseInt((event.duration / COMMON_NUM_MINUTE).toString());
- let head = min < COMMON_NUM_DOUBLE ? `${ZERO_STR}${min}` : min;
- let end = second < COMMON_NUM_DOUBLE ? `${ZERO_STR}${second}` : second;
- this.durationStringTime = `${head}${SPLIT}${end}`;
- ...
- };
设置进度条参数及属性
- Slider({
- value: this.currentTime,
- min: 0,
- max: this.durationTime,
- step: 1,
- style: SliderStyle.OutSet
- })
- .blockColor($r('app.color.white'))
- .width(STRING_PERCENT.SLIDER_WITH)
- .trackColor(Color.Gray)
- .selectedColor($r('app.color.white'))
- .showSteps(true)
- .showTips(true)
- .trackThickness(this.isOpacity ? SMALL_TRACK_THICK_NESS : BIG_TRACK_THICK_NESS)
- .onChange((value: number, mode: SliderChangeMode) => {...})
计算当前进度播放时间及添加onUpdate回调
最后,在我们播放视频时还需要更新显示播放的时间进度,也就是左侧的Text组件。在视频开始播放前,播放时间默认为00:00,随着视频播放,时间需要不断更新为当前进度时间。所以左侧的Text组件我们不仅需要读取时间,还需要为其添加数据联动。这里,我们就是通过为Video组件添加onUpdate事件来实现的,在视频播放过程中会不断调用changeSliderTime方法获取当前的播放时间并进行计算及单位转化,从而不断刷新进度条的值,也就是控制器左侧的播放进度时间Text组件。
- Video({...})
- ...
- .onUpdate((event) => {
- this.currentTime = event.time;
- this.currentStringTime = changeSliderTime(this.currentTime)
- })
- export function changeSliderTime(value: number): string {
- let second: number = value % COMMON_NUM_MINUTE;
- let min: number = parseInt((value / COMMON_NUM_MINUTE).toString());
- let head = min < COMMON_NUM_DOUBLE ? `${ZERO_STR}${min}` : min;
- let end = second < COMMON_NUM_DOUBLE ? `${ZERO_STR}${second}` : second;
- let nowTime = `${head}${SPLIT}${end}`;
- return nowTime;
- };
指定视频播放进度及添加onChange事件回调
如需手动进行进度条的拖动,则需要在Slider组件中指定播放进度,并为Slider组件添加onChange事件回调。Slider滑动时就会触发该事件回调,从而实现将视频定位到进度条当前刷新位置,完成时长组件渲染与视频播放进度数据联动。
- Slider({...})
- .onChange((value: number, mode: SliderChangeMode) => {
- sliderOnchange.call(this, value, mode);
- })
- export function sliderOnchange(value: number, mode: SliderChangeMode) {
- this.currentTime = parseInt(value.toString());
- this.controller.setCurrentTime(parseInt(value.toString()), SeekMode.Accurate);
- ...
- };
到这里我们就实现了自定义控制器的构建,两个Text组件显示的时长是由Slider组件的onChange回调事件来进行传递的,而Text组件的数值与视频播放进度数值value则通过是onUpdate与onChange事件并借由@Provide @Consume装饰器进行的数据联动。
参考链接
- Video组件的更多属性和参数的使用,可以参考API:Video。
概述
在我们日常使用应用的时候,可能会进行一些敏感的操作,比如删除联系人,这时候我们给应用添加弹窗来提示用户是否需要执行该操作,如下图所示:
弹窗是一种模态窗口,通常用来展示用户当前需要的或用户必须关注的信息或操作。在弹出框消失之前,用户无法操作其他界面内容。ArkUI为我们提供了丰富的弹窗功能,弹窗按照功能可以分为以下两类:
- 确认类:例如警告弹窗AlertDialog。
- 选择类:包括文本选择弹窗TextPickerDialog 、日期滑动选择弹窗DatePickerDialog、时间滑动选择弹窗TimePickerDialog等。
您可以根据业务场景,选择不同类型的弹窗。部分弹窗效果图如下:
此外,如果上述弹窗还不能满足您的需求,或者需要对弹窗的布局和样式进行自定义,您还可以使用自定义弹窗CustomDialog。
下文将分别介绍AlertDialog 、TextPickerDialog 、DatePickerDialog以及CustomDialog的使用。
警告弹窗
警告弹窗AlertDialog由以下三部分区域构成,对应下面的示意图:
- 标题区:为可选的。
- 内容区:显示提示消息。
- 操作按钮区:用户做”确认“或者”取消“等操作。
以下示例代码,演示了如何使用AlertDialog 实现上图所示的警告弹窗。AlertDialog可以设置两个操作按钮,示例代码中分别使用primaryButton和secondaryButton实现了“取消”和“删除”操作按钮,操作按钮可以通过action响应点击事件。
- Button('点击显示弹窗')
- .onClick(() => {
- AlertDialog.show(
- {
- title: '删除联系人', // 标题
- message: '是否需要删除所选联系人?', // 内容
- autoCancel: false, // 点击遮障层时,是否关闭弹窗。
- alignment: DialogAlignment.Bottom, // 弹窗在竖直方向的对齐方式
- offset: { dx: 0, dy: -20 }, // 弹窗相对alignment位置的偏移量
- primaryButton: {
- value: '取消',
- action: () => {
- console.info('Callback when the first button is clicked');
- }
- },
- secondaryButton: {
- value: '删除',
- fontColor: '#D94838',
- action: () => {
- console.info('Callback when the second button is clicked');
- }
- },
- cancel: () => { // 点击遮障层关闭dialog时的回调
- console.info('Closed callbacks');
- }
- }
- )
- })
此外,您还可以使用AlertDialog,构建只包含一个操作按钮的确认弹窗,使用confirm响应操作按钮回调。
- AlertDialog.show(
- {
- title: '提示',
- message: '提示信息',
- autoCancel: true,
- alignment: DialogAlignment.Bottom,
- offset: { dx: 0, dy: -20 },
- confirm: {
- value: '确认',
- action: () => {
- console.info('Callback when confirm button is clicked');
- }
- },
- cancel: () => {
- console.info('Closed callbacks')
- }
- }
- )
选择类弹窗
选择类弹窗用于方便用户选择相关数据,比如选择喜欢吃的水果、出生日期等等。下面我们以TextPickerDialog和DatePickerDialog为例,来介绍选择类弹窗的使用。
文本选择弹窗
TextPickerDialog为文本滑动选择器弹窗,根据指定的选择范围创建文本选择器,展示在弹窗上,例如下面这段示例代码使用TextPickerDialog实现了一个水果选择弹窗。示例代码中使用selected指定了弹窗的初始选择项索引为2,对应的数据为“香蕉”。当用户点击“确定”操作按钮后,会触发onAccept事件回调,在回调中将选中的值,传递给宿主中的select变量。
- @Entry
- @Component
- struct TextPickerDialogDemo {
- @State select: number = 2;
- private fruits: string[] = ['苹果', '橘子', '香蕉', '猕猴桃', '西瓜'];
- build() {
- Column() {
- Button('TextPickerDialog')
- .margin(20)
- .onClick(() => {
- TextPickerDialog.show({
- range: this.fruits, // 设置文本选择器的选择范围
- selected: this.select, // 设置初始选中项的索引值。
- onAccept: (value: TextPickerResult) => { // 点击弹窗中的“确定”按钮时触发该回调。
- // 设置select为按下确定按钮时候的选中项index,这样当弹窗再次弹出时显示选中的是上一次确定的选项
- this.select = value.index;
- console.info("TextPickerDialog:onAccept()" + JSON.stringify(value));
- },
- onCancel: () => { // 点击弹窗中的“取消”按钮时触发该回调。
- console.info("TextPickerDialog:onCancel()");
- },
- onChange: (value: TextPickerResult) => { // 滑动弹窗中的选择器使当前选中项改变时触发该回调。
- console.info('TextPickerDialog:onChange()' + JSON.stringify(value));
- }
- })
- })
- }
- .width('100%')
- }
- }
效果图如下:
日期选择弹窗
下面我们介绍另一种常用的选择类弹窗DatePickerDialog,它是日期滑动选择器弹窗,根据指定的日期范围创建日期滑动选择器,展示在弹窗上。DatePickerDialog的使用非常广泛,比如当我们需要输入个人出生日期的时候,就可以使用DatePickerDialog。下面的示例代码实现了一个日期选择弹窗:
- @Entry
- @Component
- struct DatePickerDialogDemo {
- selectedDate: Date = new Date('2010-1-1');
- build() {
- Column() {
- Button("DatePickerDialog")
- .margin(20)
- .onClick(() => {
- DatePickerDialog.show({
- start: new Date('1900-1-1'), // 设置选择器的起始日期
- end: new Date('2023-12-31'), // 设置选择器的结束日期
- selected: this.selectedDate, // 设置当前选中的日期
- lunar: false,
- onAccept: (value: DatePickerResult) => { // 点击弹窗中的“确定”按钮时触发该回调
- // 通过Date的setFullYear方法设置按下确定按钮时的日期,这样当弹窗再次弹出时显示选中的是上一次确定的日期
- this.selectedDate.setFullYear(value.year, value.month, value.day)
- console.info('DatePickerDialog:onAccept()' + JSON.stringify(value))
- },
- onCancel: () => { // 点击弹窗中的“取消”按钮时触发该回调
- console.info('DatePickerDialog:onCancel()')
- },
- onChange: (value: DatePickerResult) => { // 滑动弹窗中的滑动选择器使当前选中项改变时触发该回调
- console.info('DatePickerDialog:onChange()' + JSON.stringify(value))
- }
- })
- })
- }
- .width('100%')
- }
- }
效果图如下:
自定义弹窗
自定义弹窗的使用更加灵活,适用于更多的业务场景,在自定义弹窗中您可以自定义弹窗内容,构建更加丰富的弹窗界面。自定义弹窗的界面可以通过装饰器@CustomDialog定义的组件来实现,然后结合CustomDialogController来控制自定义弹窗的显示和隐藏。下面我们通过一个兴趣爱好的选择框来介绍自定义弹窗的使用。
从上面的效果图可以看出,这个选择框是一个多选的列表弹窗,我们可以使用装饰器@CustomDialog,结合List组件来完成这个弹窗布局,实现步骤如下:
- 初始化弹窗数据。
先准备好资源文件和数据实体类。其中资源文件stringarray.json创建在resources/base/element目录下,文件根节点为strarray。
- {
- "strarray": [
- {
- "name": "hobbies_data",
- "value": [
- {
- "value": "Soccer"
- },
- {
- "value": "Badminton"
- },
- {
- "value": "Travelling"
- },
- ...
- ]
- }
- ]
- }
实体类HobbyBean用来封装自定义弹窗中的"兴趣爱好"数据。
- export default class HobbyBean {
- label: string;
- isChecked: boolean;
- }
然后创建一个ArkTS文件CustomDialogWidget,用来封装自定义弹窗,使用装饰器@CustomDialog修饰CustomDialogWidget表示这是一个自定义弹窗。使用资源管理对象manager获取数据,并将数据封装到hobbyBeans。
- @CustomDialog
- export default struct CustomDialogWidget {
- @State hobbyBeans: HobbyBean[] = [];
- aboutToAppear() {
- let context: Context = getContext(this);
- let manager = context.resourceManager;
- manager.getStringArrayValue($r('app.strarray.hobbies_data'), (error, hobbyResult) => {
- ...
- hobbyResult.forEach((hobbyItem: string) => {
- let hobbyBean = new HobbyBean();
- hobbyBean.label = hobbyItem;
- hobbyBean.isChecked = false;
- this.hobbyBeans.push(hobbyBean);
- });
- });
- }
- build() {...}
- }
- 创建弹窗组件。
controller对象用于控制弹窗的控制和隐藏,hobbies表示弹窗选中的数据结果。setHobbiesValue方法用于筛选出被选中的数据,赋值给hobbies。
- @CustomDialog
- export default struct CustomDialogWidget {
- @State hobbyBeans: HobbyBean[] = [];
- @Link hobbies: string;
- private controller?: CustomDialogController;
- aboutToAppear() {...}
- setHobbiesValue(hobbyBeans: HobbyBean[]) {
- let hobbiesText: string = '';
- hobbiesText = hobbyBeans.filter((isCheckItem: HobbyBean) =>
- isCheckItem?.isChecked)
- .map((checkedItem: HobbyBean) => {
- return checkedItem.label;
- }).join(',');
- this.hobbies = hobbiesText;
- }
- build() {
- Column() {
- Text($r('app.string.text_title_hobbies'))...
- List() {
- ForEach(this.hobbyBeans, (itemHobby: HobbyBean) => {
- ListItem() {
- Row() {
- Text(itemHobby.label)...
- Toggle({ type: ToggleType.Checkbox, isOn: false })...
- .onChange((isCheck) => {
- itemHobby.isChecked = isCheck;
- })
- }
- }
- }, itemHobby => itemHobby.label)
- }
- Row() {
- Button($r('app.string.cancel_button'))...
- .onClick(() => {
- this.controller?.close();
- })
- Button($r('app.string.definite_button'))...
- .onClick(() => {
- this.setHobbiesValue(this.hobbyBeans);
- this.controller?.close();
- })
- }
- }
- }
- }
- 使用自定义弹窗。
在自定义弹窗的使用页面HomePage中先定义一个变量hobbies,使用装饰器@State修饰,和自定义弹窗中的@Link 装饰器修饰的变量进行双向绑定。然后我们使用alignment和offset设置弹窗的位置在屏幕底部,并且距离底部20vp。最后我们在自定义组件TextCommonWidget(具体实现可以参考《构建多种样式弹窗》Codelab源码)的点击事件中,调用customDialogController的open方法,用于显示弹窗。
- @Entry
- @Component
- struct HomePage {
- customDialogController: CustomDialogController = new CustomDialogController({
- builder: CustomDialogWidget({
- onConfirm: this.setHobbiesValue.bind(this),
- }),
- alignment: DialogAlignment.Bottom,
- customStyle: true,
- offset: { dx: 0,dy: -20 }
- });
- setHobbiesValue(hobbyArray: HobbyBean[]) {...}
- build() {
- ...
- TextCommonWidget({
- ...
- title: $r('app.string.title_hobbies'),
- content: $hobby,
- onItemClick: () => {
- this.customDialogController.open();
- }
- })
- ...
- }
- }
参考
关于更多弹窗,您可以参考:
介绍 本篇Codelab使用ArkTS语言实现视频播放器,主要包括主界面和视频播放界面,我们将一起完成以下功能:
-
主界面顶部使用Swiper组件实现视频海报轮播。
-
主界面下方使用List组件实现视频列表。
-
播放界面使用Video组件实现视频播放。
-
在不使用视频组件默认控制器的前提下,实现自定义控制器。
-
播放界面底部使用图标控制视频播放/暂停。
-
播放界面底部使用Slider组件控制和实现视频播放进度。
-
播放界面使用Stack容器组件的Z序控制在视频播放画面上展示开始/暂停/加载图标。
相关概念
- Swiper组件:滑块视图容器,提供子组件滑动轮播显示的能力。
- List组件:列表包含一系列相同宽度的列表项。适合连续、多行呈现同类数据,例如图片和文本。
- Video组件:用于播放视频文件并控制其播放状态的组件。
- Navigator组件:路由容器组件,提供路由跳转能力。
- ForEach组件:ForEach基于数组类型数据执行循环渲染。
完整示例
源码下载 简易视频播放器(ArkTS).zip 环境搭建 我们首先需要完成HarmonyOS开发环境搭建,可参照如下步骤进行。软件要求
- DevEco Studio版本:DevEco Studio 3.1 Release。
- HarmonyOS SDK版本:API version 9。
硬件要求
- 设备类型:华为手机或运行在DevEco Studio上的华为手机设备模拟器。
- HarmonyOS系统:3.1.0 Developer Release。
环境搭建
- 安装DevEco Studio,详情请参考下载和安装软件。
- 设置DevEco Studio开发环境,DevEco Studio开发环境需要依赖于网络环境,需要连接上网络才能确保工具的正常使用,可以根据如下两种情况来配置开发环境:
- 如果可以直接访问Internet,只需进行下载HarmonyOS SDK操作。
- 如果网络不能直接访问Internet,需要通过代理服务器才可以访问,请参考配置开发环境。
- 开发者可以参考以下链接,完成设备调试的相关配置:
本篇Codelab只对核心代码进行讲解,对于完整代码,我们会在源码下载或gitee中提供。
No Preview- ├──entry/src/main/ets // 代码区
- │ ├──common
- │ │ └──constants
- │ │ └──CommonConstants.ets // 样式常量类
- │ ├──entryability
- │ │ └──EntryAbility.ts // 程序入口类
- │ ├──model
- │ │ └──VideoControll.ets // 视频播放控制相关方法类
- │ ├──pages
- │ │ ├──SimpleVideoIndex.ets // 主界面
- │ │ └──SimpleVideoPlay.ets // 视频播放界面
- │ ├──view
- │ │ ├──IndexModule.ets // 自定义首页List模块组件文件
- │ │ ├──IndexSwiper.ets // 自定义首页Swiper组件文件
- │ │ ├──VideoPlayer.ets // 自定义播放界面视频组件文件
- │ │ └──VideoPlaySlider.ets // 自定义播放界面视频进度条组件文件
- │ └──viewmodel
- │ ├──HorizontalVideoItem.ets // 水平视频类
- │ ├──ParamItem.ets // 参数类
- │ ├──SwiperVideoItem.ets // banner视频类
- │ └──VideoData.ets // 首页相关数据
- └──entry/src/main/resource // 应用静态资源目录
主界面由视频轮播模块和多个视频列表模块组成,效果图如图:
VideoData.ets中定义的视频轮播图数组SWIPER_VIDEOS和视频列表图片数组HORIZONTAL_VIDEOS。 IndexSwiper.ets文件中定义的轮播图子组件SwiperVideo,点击轮播图片,页面跳转到视频播放页面,并携带本地视频flag,效果图如图:IndexModule.ets文件中定义的视频列表图片子组件VideoModule,点击子组件中的图片,页面跳转到视频播放页面,并携带网络视频flag,效果图如图:
在SimpleVideoIndex.ets主界面中引用SwiperVideo和VideoModule子组件。 No Preview- // SimpleVideoIndex.ets
- @Entry
- @Component
- struct SimpleVideoIndex {
- build() {
- Column({ space: MARGIN_FONT_SIZE.FOURTH_MARGIN }) {
- // 视频轮播组件
- SwiperVideo()
- List() {
- ForEach(LIST, (item: string) => {
- ListItem() {
- VideoModule({ moduleName: item })
- .margin({ top: MARGIN_FONT_SIZE.FIRST_MARGIN })
- }
- }, (item: string) => JSON.stringify(item))
- }
- .listDirection(Axis.Vertical)
- .margin({ top: MARGIN_FONT_SIZE.THIRD_MARGIN })
- }
- ...
- }
- }
VideoPlayer.ets其中定义了视频播放子组件VideoPlayer ,onPrepared回调方法中可以获取视频总时长,onUpdate回调方法中可实时获取到视频播放的当前时间戳,onFinish是视频播放结束后的回调方法,onError是视频播放出错的回调方法。
在自定义组件VideoPlayer底部使用了自定义子组件VideoSlider,VideoSlider自定义组件中显示和控制视频播放进度,效果图如图:
在VideoController.ets中的视频控制和回调的相关方法。在SimpleVideoPlay.ets播放界面,引用VideoPlayer子组件,并在视频播放页面使用堆叠容器,在视频播放画面中心堆叠控制、视频加载图标,效果图如图:
Preview- // SimpleVideoPlay.ets
- @Entry
- @Component
- struct Play {
- // 取到Index页面跳转来时携带的source对应的数据。
- private source: string = (router.getParams() as Record<string, Object>).source as string;
- private startIconResource: Resource = $r('app.media.ic_public_play');
- private backIconResource: Resource = $r('app.media.ic_back');
- @Provide isPlay: boolean = false;
- @Provide isOpacity: boolean = false;
- controller: VideoController = new VideoController();
- @Provide isLoading: boolean = false;
- @Provide progressVal: number = 0;
- @Provide flag: boolean = false;
- ...
- onPageHide() {
- this.controller.pause();
- }
- build() {
- Column() {
- // 顶部返回以及标题
- ...
- Stack() {
- // 不同的播放状态渲染不同得控制图片
- if (!this.isPlay && !this.isLoading) {
- Image(this.startIconResource)
- .width(MARGIN_FONT_SIZE.FIFTH_MARGIN)
- .height(MARGIN_FONT_SIZE.FIFTH_MARGIN)
- // 同一容器中兄弟组件显示层级关系,z值越大,显示层级越高 用于控制图片在视频上。
- .zIndex(STACK_STYLE.IMAGE_Z_INDEX)
- }
- if (this.isLoading) {
- Progress({
- value: STACK_STYLE.PROGRESS_VALUE,
- total: STACK_STYLE.PROGRESS_TOTAL,
- type: ProgressType.ScaleRing
- })
- .color(Color.Grey)
- .value(this.progressVal)
- .width(STACK_STYLE.PROGRESS_WIDTH)
- .style({
- strokeWidth: STACK_STYLE.PROGRESS_STROKE_WIDTH,
- scaleCount: STACK_STYLE.PROGRESS_SCALE_COUNT,
- scaleWidth: STACK_STYLE.PROGRESS_SCALE_WIDTH
- })
- .zIndex(STACK_STYLE.PROGRESS_Z_INDEX)
- }
- VideoPlayer({
- source: this.source,
- controller: this.controller
- })
- .zIndex(0)
- }
- }
- .height(ALL_PERCENT)
- .backgroundColor(Color.Black)
- }
- }
您已经完成了本次Codelab的学习,并了解到以下知识点:
- Swiper组件的使用。
- List组件的使用。
- Video组件的使用。
- Slider组件的使用。
- 如何实现自定义视频控制器。
本篇Codelab将介绍如何使用弹窗功能,实现四种类型弹窗。分别是:警告弹窗、自定义弹窗、日期滑动选择器弹窗、文本滑动选择器弹窗。需要完成以下功能:
- 点击左上角返回按钮展示警告弹窗。
- 点击出生日期展示日期滑动选择器弹窗。
- 点击性别展示文本滑动选择器弹窗。
- 点击兴趣爱好(多选)展示自定义弹窗。
相关概念
- 警告弹窗:显示警告弹窗组件,可设置文本内容与响应回调。
- 自定义弹窗: 通过CustomDialogController类显示自定义弹窗。
- 日期滑动选择器弹窗:根据指定范围的Date创建可以选择日期的滑动选择器,展示在弹窗上。
- 文本滑动选择器弹窗:根据指定的选择范围创建文本选择器,展示在弹窗上。
完整示例
源码下载 构建多种样式弹窗(ArkTS).zip 环境搭建我们首先需要完成HarmonyOS开发环境搭建,可参照如下步骤进行。
软件要求
- DevEco Studio版本:DevEco Studio 3.1 Release。
- HarmonyOS SDK版本:API version 9。
硬件要求
- 设备类型:华为手机或运行在DevEco Studio上的华为手机设备模拟器。
- HarmonyOS系统:3.1.0 Developer Release。
环境搭建
- 安装DevEco Studio,详情请参考下载和安装软件。
- 设置DevEco Studio开发环境,DevEco Studio开发环境需要依赖于网络环境,需要连接上网络才能确保工具的正常使用,可以根据如下两种情况来配置开发环境:
- 如果可以直接访问Internet,只需进行下载HarmonyOS SDK操作。
- 如果网络不能直接访问Internet,需要通过代理服务器才可以访问,请参考配置开发环境。
- 开发者可以参考以下链接,完成设备调试的相关配置:
本篇Codelab只对核心代码进行讲解,对于完整代码,我们会在源码下载或gitee中提供。
No Preview- ├──entry/src/main/ets // 代码区
- │ ├──common
- │ │ ├──constants
- │ │ │ └──CommonConstants.ets // 常量类
- │ │ └──utils
- │ │ ├──CommonUtils.ets // 弹窗操作工具类
- │ │ └──Logger.ets // 日志打印工具类
- │ ├──entryability
- │ │ └──EntryAbility.ets // 程序入口类
- │ ├──pages
- │ │ └──HomePage.ets // 主页面
- │ ├──view
- │ │ ├──CustomDialogWidget.ets // 自定义弹窗组件
- │ │ ├──TextCommonWidget.ets // 自定义Text组件
- │ │ └──TextInputWidget.ets // 自定义TextInput组件
- │ └──viewmodel
- │ └──HobbyItem.ets // 兴趣爱好类
- └──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- // HomePage.ets
- @Entry
- @Component
- struct HomePage {
- @State birthdate: string = '';
- @State sex: string = '';
- @State hobbies: string = '';
- ...
- build() {
- Column() {
- ...
- TextInputWidget({
- inputImage: $r('app.media.ic_nickname'),
- hintText: $r('app.string.text_input_hint')
- })
- TextCommonWidget({
- textImage: $r('app.media.ic_birthdate'),
- title: $r('app.string.title_birthdate'),
- content: $birthdate,
- onItemClick: () => {
- CommonUtils.datePickerDialog((birthValue: string) => {
- this.birthdate = birthValue;
- });
- }
- })
- TextCommonWidget({
- textImage: $r('app.media.ic_sex'),
- title: $r('app.string.title_sex'),
- content: $sex,
- onItemClick: () => {
- CommonUtils.textPickerDialog(this.sexArray, (sexValue: string) => {
- this.sex = sexValue;
- });
- }
- })
- TextInputWidget({
- inputImage: $r('app.media.ic_signature'),
- hintText: $r('app.string.text_input_signature')
- })
- TextCommonWidget({
- textImage: $r('app.media.ic_hobbies'),
- title: $r('app.string.title_hobbies'),
- content: $hobbies,
- onItemClick: () => {
- this.customDialogController.open();
- }
- })
- }
- ...
- }
- }
点击主页面左上角返回按钮,通过CommonUtils.alertDialog方法弹出警告弹窗,提醒用户是否进行当前操作,效果如图所示:
Preview- // CommonUtils.ets
- alertDialog(context: Context.UIAbilityContext) {
- AlertDialog.show({
- // 提示信息
- message: $r('app.string.alert_dialog_message'),
- // 弹窗显示位置
- alignment: DialogAlignment.Bottom,
- // 弹窗偏移位置
- offset: {
- dx: 0,
- dy: CommonConstants.DY_OFFSET
- },
- primaryButton: {
- value: $r('app.string.cancel_button'),
- action: () => {
- ...
- }
- },
- secondaryButton: {
- value: $r('app.string.definite_button'),
- action: () => {
- // 退出应用
- context.terminateSelf();
- ...
- }
- }
- });
- }
点击出生日期选项,通过CommonUtils.datePickerDialog方法弹出日期选择器弹窗,根据需要选择相应时间,效果如图所示:
Preview- // CommonUtils.ets
- datePickerDialog(dateCallback: Function) {
- DatePickerDialog.show({
- // 开始时间
- start: new Date(CommonConstants.START_TIME),
- // 结束时间
- end: new Date(),
- // 当前选中时间
- selected: new Date(CommonConstants.SELECT_TIME),
- // 是否显示农历
- lunar: false,
- onAccept: (value: DatePickerResult) => {
- let year: number = Number(value.year);
- let month: number = Number(value.month) + CommonConstants.PLUS_ONE;
- let day: number = Number(value.day);
- let birthdate: string = this.getBirthDateValue(year, month, day);
- dateCallback(birthdate);
- }
- });
- }
- // 获取出生日期值
- getBirthDateValue(year: number, month: number, day: number): string {
- let birthdate: string = `${year}${CommonConstants.DATE_YEAR}${month}` +
- `${CommonConstants.DATE_MONTH}${day}${CommonConstants.DATE_DAY}`;
- return birthdate;
- }
- // HomePage.ets
- build() {
- Column() {
- ...
- TextCommonWidget({
- textImage: $r('app.media.ic_birthdate'),
- title: $r('app.string.title_birthdate'),
- content: $birthdate,
- onItemClick: () => {
- CommonUtils.datePickerDialog((birthValue: string) => {
- this.birthdate = birthValue;
- });
- }
- })
- ...
- }
- ...
- }
点击性别选项,通过CommonUtils.textPickerDialog方法弹出性别选择器弹窗,根据需要选择相应性别,效果如图所示:
Preview- // CommonUtils.ets
- textPickerDialog(sexArray: Resource, sexCallback: Function) {
- ...
- TextPickerDialog.show({
- range: sexArray,
- selected: 0,
- onAccept: (result: TextPickerResult) => {
- sexCallback(result.value);
- },
- onCancel: () => {
- ...
- }
- });
- }
- // HomePage.ets
- build() {
- Column() {
- ...
- TextCommonWidget({
- textImage: $r('app.media.ic_sex'),
- title: $r('app.string.title_sex'),
- content: $sex,
- onItemClick: () => {
- CommonUtils.textPickerDialog(this.sexArray, (sexValue: string) => {
- this.sex = sexValue;
- });
- }
- })
- ...
- }
- ...
- }
点击兴趣爱好选项,通过customDialogController.open方法弹出自定义弹窗,根据需要选择相应的兴趣爱好,效果如图所示:
在view目录下,点击鼠标右键 > New > ArkTS File,新建一个ArkTS文件,然后命名为CustomDialogWidget子组件。
在CustomDialogWidget的aboutToAppear方法,通过manager.getStringArrayValue方法获取本地资源数据进行初始化。
当用户点击确定按钮时,通过setHobbiesValue方法处理自定义弹窗选项结果。
通过@Link修饰的hobbies把值赋给HomePage的hobbies,然后hobbies刷新显示内容。
No Preview- // HomePage.ets
- @State hobbies: string = '';
- customDialogController: CustomDialogController = new CustomDialogController({
- builder: CustomDialogWidget({
- hobbies: $hobbies
- }),
- alignment: DialogAlignment.Bottom,
- customStyle: true,
- offset: {
- dx: 0,
- dy: CommonConstants.DY_OFFSET
- }
- });
- build() {
- Column() {
- ...
- TextCommonWidget({
- textImage: $r('app.media.ic_hobbies'),
- title: $r('app.string.title_hobbies'),
- content: $hobbies,
- onItemClick: () => {
- // 打开自定义弹窗
- this.customDialogController.open();
- }
- })
- }
- ...
- }
您已经完成了本次Codelab的学习,并了解到以下知识点:
- 使用CustomDialogController实现自定义弹窗。
- 使用AlertDialog实现警告弹窗。
- 使用DatePickerDialog实现日期滑动选择弹窗。
- 使用TextPickerDialog实现文本滑动选择弹窗。