1 准备
1.1 新建工程
DevEcoStudio 创建空白工程,只关注 entry/src/main
路径即可,IDE默认已创建好应用入口和入口页面,不需要其他操作,准备按照项目逻辑来创建页面。
1.2 数据类型定义
完成工程创建后,我们再来创建一些数据类型的定义。将此动作归到 『准备』 一节,只是由于在复刻这个Demo的时候没办法对这些数据类型做什么变动,从Demo源码里复制粘贴即可。
// src/main/ets/common/bean/RankData.ets
export class RankData {
name: Resource;
vote: string; // Number of votes
id: string;
constructor(id: string, name: Resource, vote: string) {
this.id = id;
this.name = name;
this.vote = vote;
}
}
这里的 RankData.name
的类型是 Resource
,这应该是SDK提供的一种现成数据类型,此处先不管,后文可以看到这个类型的用法。
至于另一份数据类型文件 src/main/ets/common/constants/Constants.ets
,也直接从开源Demo工程拷贝即可,这文件里主要是一些常量的定义,比如页面宽度、高度、padding等。
1.3 准备资源
应用程序的资源目录在 src/main/resources
路径下,这里直接把开源Demo的资源整个复制到我们的 Demo
里即可,这些资源包括字符定义、多媒体文件等。
例如
src/main/resources/base/element/color.json
和src/main/resources/base/element/string.json
,这两个文件提供的部分配置会在Constants.ets
代码中通过$r()
表达式取用。
1.4 准备数据源
程序打桩了数据源,把数据写死在 src/main/ets/model/DataModel.ts
里面,并在 src/main/ets/viewmodel/RankViewModel.ets
提供了获取数据的接口。因此把这两个文件也拷贝到我们的工程中。
2 页面组件
2.1 标题
首先实现标题栏组件。
@Component
export struct TitleComponent {
@Link isRefreshData: boolean;
@State title: Resource = $r('app.string.title_default');
// ===== snip =====
}
src/main/ets/view/TitleComponent.ets
文件的实现,体现了ArkTs的若干知识点:
-
@Component
装饰器,表明这个类型是用户自定义的UI组件; -
@State
装饰器,表明这个数据是组件内部的状态数据,当这个状态数据被修改时,会调用 所在组件 的build
方法进行UI刷新 1 ; -
@Link
装饰器,表明这个变量会与父组件的一个@State
变量建立双向绑定,注意@Link
变量 不能 在组件内部进行初始化; -
$r
表达式,从resource
里面获取配置信息,早先也见到过了。
继续看:
build() {
Row() {
Row() {
Image($r('app.media.ic_public_back'))
// ===== snip =====
.onClick(() => {
let handler = getContext(this) as AppContext.UIAbilityContext;
handler.terminateSelf();
})
Text(this.title)
.fontSize(FontSize.LARGE)
}
// ===== snip =====
Row() {
Image($r('app.media.loading'))
// ===== snip =====
.onClick(() => {
this.isRefreshData = !this.isRefreshData;
})
}
// ===== snip =====
}
// ===== snip =====
}
}
-
标题栏的
build()
的实现,只介绍重点,代码显示整个标题栏实际上是由两个Row
组件组成的,第一个Row
包含返回图标和标题文字,第二个Row
包含刷新图标; -
第一个
Image
的onClick
,实现返回动作。
2.2 列表头
@Component
export struct ListHeaderComponent {
paddingValue: Padding | Length = 0;
widthValue: Length = 0;
build() {
Row() {
Text($r('app.string.page_number'))
// ===== snip =====
Text($r('app.string.page_type'))
// ===== snip =====
Text($r('app.string.page_vote'))
// ===== snip =====
}
}
}
这个组件其实没什么好讲,纯文本组件。
这里的三份 Text
其实只有 width
的区别,可以考虑用 @Styles
装饰器来提取公共代码 2 。
2.3 列表项
首先是列表项的数据类型的定义:
@Component
export struct ListItemComponent {
index?: number;
private name?: Resource;
@Prop vote: string = '';
@Prop isSwitchDataSource: boolean = false;
// The state is related to the font color of ListItemComponent.
@State isChange: boolean = false;
// ===== snip =====
}
-
这里的
?
用于表示 可选参数 3 4 ,个人觉得这里用上这个操作,有点逆天,增加了Demo的理解难度,并且从场景上看,index
和name
也并不是可选的; -
@Prop
装饰器,与@State
有相同语义,但初始化方式不同。@Prop
装饰的变量需使用父组件提供的@State
变量进行初始化,允许组件内部修改@Prop
变量,但更改不会通知父组件,即@Prop
属于单向数据绑定。其实这里没太理解为啥要用
@Prop
而不是@State
,大约这就是Demo吧(x
再看列表项的 build()
实现:
build() {
Row() {
Column() {
if (this.isRenderCircleText()) {
if (this.index !== undefined) {
this.CircleText(this.index);
}
} else {
Text(this.index?.toString())
// ===== snip =====
}
}
// ===== snip =====
Text(this.name)
// ===== snip =====
Text(this.vote)
// ===== snip =====
}
// ===== snip =====
.onClick(() => {
this.isSwitchDataSource = !this.isSwitchDataSource;
this.isChange = !this.isChange;
})
}
@Builder CircleText(index: number) {
Row() {
Text(this.index?.toString())
.fontWeight(FontWeight.BOLD)
.fontSize(FontSize.SMALL)
.fontColor(Color.White);
}
.justifyContent(FlexAlign.Center)
.borderRadius(ItemStyle.CIRCLE_TEXT_BORDER_RADIUS)
.size({ width: ItemStyle.CIRCLE_TEXT_SIZE,
height: ItemStyle.CIRCLE_TEXT_SIZE })
.backgroundColor($r('app.color.circle_text_background'))
}
isRenderCircleText(): boolean {
// Just render the element before the fourth in the list.
return this.index === 1 || this.index === 2 || this.index === 3;
}
这一段实现涉及的知识点是有点说法的:
-
条件渲染 5 ,对于前三个
index
的元素做了特殊渲染处理。 -
@Builder
装饰器,在一个组件内快速生成多个布局内容。由于每个组件的总入口是build()
,为了不让build
太冗长,允许在build
内调用@Builder
方法,来进一步抽取出粒度更细的自定义组件;给我的感觉就是轻量@Component
。 -
isSwitchDataSource
这里没啥用处,是个冗余成员。
3 主页
3.1 数据加载
完成了三个组件的定义后,打开创建工程时自动生成的 src/main/ets/pages/Index.ets
,编辑主页。
@Entry
@Component
struct RankPage {
@State dataSource1: RankData[] = [];
@State dataSource2: RankData[] = [];
// The State is used to decide whether to switch the data of RankList.
@State isSwitchDataSource: boolean = true;
// It will record the time of clicking back button of system navigation.
private clickBackTimeRecord: number = 0;
aboutToAppear() {
this.dataSource1 = rankModel.loadRankDataSource1();
this.dataSource2 = rankModel.loadRankDataSource2();
}
// ===== snip =====
}
dataSource1/2
一开始默认是空数组;在aboutToAppear
方法里加载数据。页面生命周期及方法如下图:
isSwitchDataSource
,这个是要跟标题栏里面的@Link
成员绑定的,用于实现数据源切换;
3.2 返回确认
通过实现 onBackPress
来支持返回确认,防止用户误触返回键时应用被关闭,这段实现参考价值极大:
onBackPress() {
if (this.isShowToast()) {
prompt.showToast({
message: $r('app.string.prompt_text'),
duration: TIME
});
this.clickBackTimeRecord = new Date().getTime();
return true;
}
return false;
}
isShowToast(): boolean {
return new Date().getTime() - this.clickBackTimeRecord > APP_EXIT_INTERVAL;
}
-
这里弹窗显示时间通过
duration
指定; -
onBackPress
返回false
的时候,系统继续执行返回动作,返回true
时反之; -
通过内部一个成员来记录用户上次点击返回按钮的时间,以此实现「再按一次返回退出」的逻辑;
-
返回按钮在某些应用(头条、B站等)上实际上是一次返回刷新、两次返回退出的逻辑,可以参考这段代码实现。
3.3 页面构建
build() {
Column() {
// Title component in the top.
TitleComponent({ isRefreshData: $isSwitchDataSource, title: TITLE })
// The head style of List component.
ListHeaderComponent({
paddingValue: {
left: Style.RANK_PADDING,
right: Style.RANK_PADDING
},
widthValue: Style.CONTENT_WIDTH
})
.margin({
top: Style.HEADER_MARGIN_TOP,
bottom: Style.HEADER_MARGIN_BOTTOM
})
// The style of List component.
this.RankList(Style.CONTENT_WIDTH)
}
// ===== snip =====
}
@Builder RankList(widthValue: Length) {
Column() {
List() {
ForEach(this.isSwitchDataSource ? this.dataSource1 : this.dataSource2,
(item: RankData, index?: number) => {
ListItem() {
ListItemComponent({ index: (Number(index) + 1), name: item.name, vote: item.vote,
isSwitchDataSource: this.isSwitchDataSource
})
}
}, (item: RankData) => JSON.stringify(item))
}
// ===== snip =====
}
// ===== snip =====
}
页面核心通过 @Builder RankList(widthValue: Length)
方法来构建,这里通过循环渲染 5 来创建多个 ListItemComponent
。
4 小结
至此,ArkTs第一个demo就逐一拆解分析完毕了。
实际上Demo的页面效果这么好,除了脚本语言外,很大一部分要归功于现成的CSS样式(第一节)。
这里确实只是一个入门Demo,分析下来,自觉学到不少,笔记也写出不少,算是很有些内容了。
参考资料
-
https://developer.huawei.com/consumer/cn/training/course/slightMooc/C101667356568959645?ha_linker=eyJ0cyI6MTY5NjU1MjEzNzA5MiwiaWQiOiJjZTA3YjQ0MWZkNjQ5ZjQ3ZTUwNDQ5MjY4ZjVmZTRlZSJ9 ↩︎
-
https://developer.harmonyos.com/cn/docs/documentation/doc-guides/ets-dynamic-ui-element-building-0000001366154244#ZH-CN_TOPIC_0000001366154244__styles ↩︎
-
https://www.codecademy.com/courses/learn-typescript/lessons/typescript-advanced-object-types/exercises/optional-type-members# ↩︎
-
https://developer.harmonyos.com/cn/docs/documentation/doc-guides/ets-rendering-control-0000001149698611#ZH-CN_TOPIC_0000001157228877 ↩︎ ↩︎