在应用中,界面通常都是动态的。如图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管理页面级变量的状态,实现对页面数据的增加、删除、修改。要求完成以下功能:
实现一个自定义弹窗,完成添加子目标的功能。
实现一个可编辑列表,可点击指定行展开调节工作目标进度,可多选、全选删除指定行。
相关概念
页面状态管理:用于管理页面级变量的状态。
自定义弹窗: 通过CustomDialogController类显示自定义弹窗。
List列表:列表包含一系列相同宽度的列表项。
完整示例
源码下载
环境搭建
我们首先需要完成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。
No Preview
// 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的实现,包括以下功能:
列表项展开。
列表子项点击下拉,滑动滑块更新进展。
列表进入编辑状态,单选、多选、全选、删除子项。
6.1 实现列表项展开
实现以下步骤完成点击列表项展开功能:
使用@State 管理参数isExpanded,表示当前项是否展开,具体表现为自定义组件ProgressEditPanel的显示或隐藏。
使用@Link和@Watch管理参数clickIndex,表示当前点击ListItem的Index索引。clickIndex值的改变将会传递至所有的ListItem。
完成onClick点击事件,将isExpanded 值置反,修改clickIndex值为当前点击的索引。
No Preview
// 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;
}
})
}
...
}
6.2 实现更新进展
列表某项被展开后,实现以下步骤完成更新进展功能:
Slider实现滑动条,滑动滑块调节进展,使用slidingProgress保存滑动值。
点击确定调用onClickOK方法,将数据slidingProgress回调至TargetListItem。
在TargetListItem中获取回调的数据并刷新页面。
在TargetListItem.ets中,完成onClickOK方法的实现,将依次完成以下步骤。
重新渲染TargetListItem的进度值和最近更新时间。
更新缓存的数据。
修改overAllProgressChanged的值,通知主页刷新整体进展详情TargetInformation。
No Preview
// 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;
},
...
})
...
}
}
}
}
6.3 实现列表多选
列表进入编辑模式才可单选、多选。实现以下步骤完成列表多选功能:
维护一个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,重新渲染列表完成单选和多选功能
No Preview
// 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;
})
...
...
}
}
}
6.4 实现删除选中列表项
当点击“删除”时,调用TargetList.ets的deleteSelected方法,实现以下步骤完成列表项删除功能:
调用DataModel的deleteData方法删除数据。
更新targetData的数据重新渲染列表。
修改overAllProgressChanged的值,通知主页刷新整体进展详情TargetInformation。
在DataModel.ets中,遍历数据列表,删除被选中的数据项。
No Preview
// 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序控制在视频播放画面上展示开始/暂停/加载图标。
主界面中最近播放和为你推荐列表播放网络视频,需将CommonConstants.ets中的NET属性修改为网络视频地址。
相关概念
Swiper组件:滑块视图容器,提供子组件滑动轮播显示的能力。
List组件:列表包含一系列相同宽度的列表项。适合连续、多行呈现同类数据,例如图片和文本。
Video组件:用于播放视频文件并控制其播放状态的组件。
Navigator组件:路由容器组件,提供路由跳转能力。
ForEach组件:ForEach基于数组类型数据执行循环渲染。
完整示例
源码下载
环境搭建
我们首先需要完成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创建可以选择日期的滑动选择器,展示在弹窗上。
文本滑动选择器弹窗:根据指定的选择范围创建文本选择器,展示在弹窗上。
完整示例
源码下载
环境搭建
我们首先需要完成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实现文本滑动选择弹窗。