ArkTS
。
说到ArkTS
就得说一下DevEco Studio
的演变过程,在我写一篇关于鸿蒙的文章时,DevEco Studio
才刚推出不久,当时所支持的语言是Java、JS、C++等,在后续的版本中逐渐去掉了Java,C++,最终使用到了ArkTS,那么我们下面来了解一下ArkTS的由来。
ArkTS是HarmonyOS主力应用开发语言,它在TypeScript(简称TS)
的基础上,匹配ArkUI
框架,扩展了声明式UI、状态管理等相应的能力,让开发者以更简洁、更自然的方式开发跨端应用,如果你之前接触过Flutter
的Dart
、Kotlin
的Compose
,那么你对于这个ArkTS
的使用应该问题不大。
ArkTS的构成如下图所示
JavaScript
、TypeScript
和 ArkTS
的关系:
JavaScript
是一种属于网络的高级脚本语言,已经被广泛用于Web
应用开发,常用来为网页添加各式各样的动态功能,为用户提供更流畅美观的浏览效果。TypeScript
是JavaScript
的一个超集,它扩展了JavaScript
的语法,通过在JavaScript的基础上添加静态类型定义构建而成,是一个开源的编程语言。ArkTS
基于TypeScript
语言,拓展了声明式UI、状态管理、并发任务等能力。 声明式UI有两个特征分别是:声明式描述
和状态驱动视图更新
,那么怎么体现这一点呢?我们结合代码来说明,首先打开DevEco Studio,这里我使用的版本是DevEco Studio 3.1.1 Release
,创建工程。
点击Next。
输入项目名称和包名,这里会说明使用的方式和语言及SDK版本和使用的设备类型,修改好之后点击Finish
,完成项目的创建。
完成创建之后我们就可以看到index.ets的代码如图所示,下面我们对这段代码进行一个解析:
装饰器
,用来装饰类、结构体、方法以及变量,赋予其特殊的含义,如上述示例中 @Entry
、 @Component
、 @State
都是装饰器。具体而言, @Component
表示这是个自定义组件; @Entry
则表示这是个入口组件; @State
表示组件中的状态变量,此状态变化会引起 UI 变更。自定义组件
,可复用的 UI 单元,可组合其它组件,如上述被 @Component
装饰的 struct Index
。UI 描述
,声明式的方式来描述 UI 的结构,如上述 build() 方法内部的代码块。内置组件
,框架中默认内置的基础和布局组件,可直接被开发者调用,比如示例中的 Row、Column、Text
。事件方法
,用于添加组件对事件的响应逻辑,统一通过事件方法进行设置,比如为组件添加onClick()
。属性方法
,用于组件属性的配置,统一通过属性方法进行设置,如fontSize()、width()、height()、color()
等,可通过链式调用的方式设置多项属性。下面我们预览看一下,点击右侧边栏的Previewer
,等待一小会。
下面我们修改index.ets中的代码,然后Ctrl + S保存一下,右侧的预览画面就会更新。
预览更新后
点击Test按钮之后
这里我们添加了一个按钮,同时添加了点击事件,事件中修改了message的值,而message是由@State
修饰的,那么就会出发UI刷新,刷新后,Text组件所显示的内容就会从Hello World变成Hello ArkTS,这就是声明式UI的另一特征:状态驱动视图更新
。
上面的示例比较简单,下面我们做一个有点难度的示例,该示例源于鸿蒙学堂官网,感兴趣的可以去学习。
首先我们在ets
目录下创建一个model
目录,model
目录下创建一个RankData.ets
文件,代码如下所示:
export class RankData {
id: number | string
name: Resource
vote: string
constructor(id: number | string, name: Resource, vote: string) {
this.id = id;
this.name = name;
this.vote = vote;
}
}
这里的代码简单说明一下,export
表示可以在其他模块中使用,这里的含义就在于我们将RankData反正model目录下,如果我们pages下要使用这个RankData,则RankData本身就需要支持调用才行,因此需要export
进行修饰,调用的地方则使用import
作为插入。
而id属性的定义是一个联合类型,这属于TypeScript的基础数据类型,表示取值可以为多种类型中的一种。number表示数字,string就是字符串,Resource就表示资源,比如string.app_name这种方式。构造函数就没有什么好说的了,就是属性赋值而已。
我们看到和ets
目录平级的是resources
,该目录下毫无疑问就是资源目录,目录下有三个文件夹,base属于基础资源目录里面可以放置文字、颜色、音频、配置文件等,en_US就是英文下的文字资源,zh_CN就是中文下的文字资源,三个目录下的文字资源文件都是json格式的,下面我们修改base/element/string.json
和en_US/element/string.json
中的代码:
{
"string": [
{
"name": "module_desc",
"value": "module description"
},
{
"name": "EntryAbility_desc",
"value": "description"
},
{
"name": "EntryAbility_label",
"value": "label"
},
{
"name": "page_type",
"value": "variety"
},
{
"name": "page_number",
"value": "ranking"
},
{
"name": "page_vote",
"value": "vote"
},
{
"name": "prompt_text",
"value": "Press again to exit the app"
},
{
"name": "title",
"value": "Ranking List"
},
{
"name": "title_default",
"value": " "
},
{
"name": "fruit_watermelon",
"value": "watermelon"
},
{
"name": "fruit_apple",
"value": "apple"
},
{
"name": "fruit_banana",
"value": "banana"
},
{
"name": "fruit_grapes",
"value": "grapes"
},
{
"name": "fruit_red_grape",
"value": "grape"
},
{
"name": "fruit_pears",
"value": "pears"
},
{
"name": "fruit_pineapple",
"value": "pineapple"
},
{
"name": "fruit_durian",
"value": "durian"
},
{
"name": "fruit_guava",
"value": "guava"
},
{
"name": "fruit_carambola",
"value": "carambola"
}
]
}
再修改zh_CN/element/string.json
中的代码:
{
"string": [
{
"name": "module_desc",
"value": "模块描述"
},
{
"name": "EntryAbility_desc",
"value": "description"
},
{
"name": "EntryAbility_label",
"value": "label"
},
{
"name": "page_type",
"value": "种类"
},
{
"name": "page_number",
"value": "排名"
},
{
"name": "page_vote",
"value": "得票数"
},
{
"name": "prompt_text",
"value": "再按一次退出程序"
},
{
"name": "title",
"value": "排行榜"
},
{
"name": "title_default",
"value": " "
},
{
"name": "fruit_watermelon",
"value": "西瓜"
},
{
"name": "fruit_apple",
"value": "苹果"
},
{
"name": "fruit_banana",
"value": "香蕉"
},
{
"name": "fruit_grapes",
"value": "葡萄"
},
{
"name": "fruit_red_grape",
"value": "红提"
},
{
"name": "fruit_pears",
"value": "梨子"
},
{
"name": "fruit_pineapple",
"value": "菠萝"
},
{
"name": "fruit_durian",
"value": "榴莲"
},
{
"name": "fruit_guava",
"value": "番石榴"
},
{
"name": "fruit_carambola",
"value": "杨桃"
}
]
}
下面我们制造一些假数据,在model
包下新建一个DataModel.ets
文件,代码如下所示:
import { RankData } from './RankData'
export {rankData1, rankData2}
const rankData1: RankData[] = [
new RankData(1, $r('app.string.fruit_apple'), "10000"),
new RankData(2, $r('app.string.fruit_grapes'), '10320'),
new RankData(3, $r('app.string.fruit_watermelon'), '9801'),
new RankData(4, $r('app.string.fruit_banana'), '8431'),
new RankData(5, $r('app.string.fruit_pineapple'), '7546'),
new RankData(6, $r('app.string.fruit_durian'), '7431'),
new RankData(7, $r('app.string.fruit_red_grape'), '7187'),
new RankData(8, $r('app.string.fruit_pears'), '7003'),
new RankData(9, $r('app.string.fruit_carambola'), '6794'),
new RankData(10, $r('app.string.fruit_guava'), '6721')
]
const rankData2: RankData[] = [
new RankData('11', $r('app.string.fruit_watermelon'), '8836'),
new RankData('12', $r('app.string.fruit_apple'), '8521'),
new RankData('13', $r('app.string.fruit_banana'), '8431'),
new RankData('14', $r('app.string.fruit_grapes'), '7909'),
new RankData('15', $r('app.string.fruit_red_grape'), '7547'),
new RankData('16', $r('app.string.fruit_pears'), '7433'),
new RankData('17', $r('app.string.fruit_pineapple'), '7186'),
new RankData('18', $r('app.string.fruit_durian'), '7023'),
new RankData('19', $r('app.string.fruit_guava'), '6794'),
new RankData('20', $r('app.string.fruit_carambola'), '6721')
];
这里我们首先导入RankData
,然后创建了两个数组,数组中通过RankData
构建函数进行bean的构建,注意这里的id,我可以使用number也可以使用string,同时资源的引用是 $r
,r
就表示resource
,使用app.string
引用文字资源,你还可以app.color等一些方式引用其他类型资源,构建了两个数组,然后导出这两个数组在其他文件中使用。
现在有了模拟数据之后,我们可以再创建一个类去提供模拟数据,在model
包下新建一个RankViewModel.ets
文件,代码如下所示:
import { RankData } from './RankData';
import { rankData1, rankData2 } from './DataModel';
export class RankViewModel {
loadRankDataSource1(): RankData[] {
return rankData1;
}
loadRankDataSource2(): RankData[] {
return rankData2;
}
}
这里导入了RankData和DataModel,通过在RankViewModel中进行返回数据得到具体的数据数组。这个其实和Android的MVI架构差不多,下面我们再添加一些colors资源,在后面的样式上会用到,修改base/element/color.json
文件,代码如下所示:
{
"color": [
{
"name": "start_window_background",
"value": "#FFFFFF"
},
{
"name": "white",
"value": "#FFFFFF"
},
{
"name": "rank_first_gradient_start",
"value": "#FFFF9A"
},
{
"name": "rank_first_gradient_end",
"value": "#CCA538"
},
{
"name": "rank_first_border",
"value": "#9E8A24"
},
{
"name": "rank_first_text",
"value": "#9E8A24"
},
{
"name": "rank_secondary_gradient_start",
"value": "#B8B8B8"
},
{
"name": "rank_secondary_gradient_end",
"value": "#9C9C9C"
},
{
"name": "rank_secondary_border",
"value": "#7E7E7E"
},
{
"name": "rank_secondary_text",
"value": "#FFFFFF"
},
{
"name": "rank_third_gradient_start",
"value": "#B9A185"
},
{
"name": "rank_third_gradient_end",
"value": "#AE8659"
},
{
"name": "rank_third_border",
"value": "#775C3E"
},
{
"name": "rank_third_text",
"value": "#FFFFFF"
},
{
"name": "rank_view_color_holder",
"value": "#FFFFFF"
},
{
"name": "item_color",
"value": "#007DFF"
},
{
"name": "item_color_black",
"value": "#182431"
},
{
"name": "background",
"value": "#F1F3F5"
},
{
"name": "font_description",
"value": "#989A9C"
},
{
"name": "circle_text_background",
"value": "#007dff"
}
]
}
除此之外还有三个图标,你可以在我的源码中获取,放在resources/base/media
下
其中icon.png是创建工程时自带的图标,如果你觉得Project
模式下文件过多,你可以切换为Ohos
模式。
这样看起来比较简洁,只不过你需要熟悉文件结构才行。
在进行鸿蒙应用开发时,通常会将样式和代码进行分离,这一点是很常见了,我们在ets
目录下新建一个constants
文件夹,该目录下新建一个Constants.ets
文件,代码如下:
/**
* 字体大小
*/
export enum FontSize {
SMALL = 14,
MIDDLE = 16,
LARGE = 20,
};
/**
* 字体粗细
*/
export enum FontWeight {
BOLD = '400',
BOLDER = '500',
};
/**
* 权重是组件大小的全局默认值。
*/
export const WEIGHT = '100%';
/**
* Toast 出现的时间
*/
export const TIME = 1000;
/**
* App退出的间隔时间
*/
export const APP_EXIT_INTERVAL: number = 4500;
/**
* 页面TAG
*/
export const TAG: string = 'Index';
/**
* 标题内容
*/
export const TITLE: Resource = $r('app.string.title');
class style {
RANK_PADDING: number = 15; // 排名填充
CONTENT_WIDTH: string = '90%'; // 内容的宽度
BORDER_RADIUS: number = 20; // 边界半径
STROKE_WIDTH: number = 1; // 描边宽度
HEADER_MARGIN_TOP: number = 20; // 距离上边距
HEADER_MARGIN_BOTTOM: number = 15;// 距离下边距
LIST_HEIGHT: string = '65%'; // List高度
}
/**
* 页面样式
*/
export const Style: style = {
RANK_PADDING: 15,
CONTENT_WIDTH: '90%',
BORDER_RADIUS: 20,
STROKE_WIDTH: 1,
HEADER_MARGIN_TOP: 20,
HEADER_MARGIN_BOTTOM: 15,
LIST_HEIGHT: '65%'
};
class listHeaderStyle {
FONT_WEIGHT: number = 400; // 字体粗细
LAYOUT_WEIGHT_LEFT: string = '30%'; // 左边的布局权重
LAYOUT_WEIGHT_CENTER: string = '50%'; // 中间的布局权重
LAYOUT_WEIGHT_RIGHT: string = '20%'; // 右边的布局权重
}
/**
* 列表标题样式
*/
export const ListHeaderStyle: listHeaderStyle = {
FONT_WEIGHT: 400,
LAYOUT_WEIGHT_LEFT: '30%',
LAYOUT_WEIGHT_CENTER: '50%',
LAYOUT_WEIGHT_RIGHT: '20%',
};
class itemStyle {
TEXT_LAYOUT_SIZE: number = 24; // 文本的行高
CIRCLE_TEXT_BORDER_RADIUS: number = 24; // 圆形文本的边框半径
CIRCLE_TEXT_SIZE: number = 24; // 圆圈文本的大小
CIRCLE_TEXT_COLOR_STOP_1: number = 0.5; // 渐变色比例1
CIRCLE_TEXT_COLOR_STOP_2: number = 1.0; // 渐变色比例2
BAR_HEIGHT: number = 48; // item高度
LAYOUT_WEIGHT_LEFT: string = '30%'; // 左边的布局权重
LAYOUT_WEIGHT_CENTER: string = '50%'; // 中间的布局权重
LAYOUT_WEIGHT_RIGHT: string = '20%'; // 右边的布局权重
BORDER_WIDTH: number = 1; // 边框宽度
COLOR_BLUE: Resource = $r('app.color.item_color'); // 文字蓝色
COLOR_BLACK: Resource = $r('app.color.item_color_black'); // 文字黑色
}
/**
* 列表Item样式
*/
export const ItemStyle: itemStyle = {
TEXT_LAYOUT_SIZE: 24,
CIRCLE_TEXT_BORDER_RADIUS: 24,
CIRCLE_TEXT_SIZE: 24,
CIRCLE_TEXT_COLOR_STOP_1: 0.5,
CIRCLE_TEXT_COLOR_STOP_2: 1.0,
BAR_HEIGHT: 48,
LAYOUT_WEIGHT_LEFT: '30%',
LAYOUT_WEIGHT_CENTER: '50%',
LAYOUT_WEIGHT_RIGHT: '20%',
BORDER_WIDTH: 1,
COLOR_BLUE: $r('app.color.item_color'),
COLOR_BLACK: $r('app.color.item_color_black')
};
class titleBarStyle {
IMAGE_BACK_SIZE: number = 21; // 后退按钮的图像大小
IMAGE_BACK_MARGIN_RIGHT: number = 18; // 后退按钮的右边距
IMAGE_LOADING_SIZE: number = 22; // 刷新按钮的图像大小
BAR_HEIGHT: number = 47; // 标题栏的高度
BAR_MARGIN_HORIZONTAL: number = 26; // 标题组件的水平边距
BAR_MARGIN_TOP: number = 10; // 标题组件的上边距
WEIGHT: string = '50%'; // 行布局的权重
}
/**
* 标题栏样式
*/
export const TitleBarStyle: titleBarStyle = {
IMAGE_BACK_SIZE: 21,
IMAGE_BACK_MARGIN_RIGHT: 18,
IMAGE_LOADING_SIZE: 22,
BAR_HEIGHT: 47,
BAR_MARGIN_HORIZONTAL: 26,
BAR_MARGIN_TOP: 10,
WEIGHT: '50%',
};
这里的代码乍一看很多,不好理解,其实我们分析一下就知道是写什么属性,首先我们定义了页面字体大小和粗细的枚举类型,用于设置标题文字和其他文字,然后就是页面的权重、退出App的提示时间等、接着就是定义页面样式、标题栏样式、列表头样式、列表Item样式,通过注释你可以你知道每一个样式是什么意思,熟能生巧,你现在觉得不适应是因为不熟悉的缘故。
在ArkTS中组件是一个比较重要的知识点,组件也分为三个类型,基础组件、容器组件和自定义组件。
下面我们来自定义一个组件,做一个标题栏组件,效果如下图所示:
首先我们在ets
目录下新建一个view
文件夹,该目录下新建一个TitleComponent.ets
文件,代码如下:
/**
* 自定义页面标题组件
*/
import AppContext from '@ohos.app.ability.common'
import { FontSize, TitleBarStyle, WEIGHT } from '../constants/Constants'
@Component
export struct TitleComponent {
@Link isRefreshData: boolean //是否刷新数据
@State title: Resource = $r('app.string.title_default')
build() {
Row() {
Row() {
//返回图标
Image($r('app.media.ic_public_back'))
.height(TitleBarStyle.IMAGE_BACK_SIZE)
.width(TitleBarStyle.IMAGE_BACK_SIZE)
.margin({ right: TitleBarStyle.IMAGE_BACK_MARGIN_RIGHT })
.onClick(() => {
let handler = getContext(this) as AppContext.UIAbilityContext
handler.terminateSelf() //杀死程序
})
//标题文字
Text(this.title)
.fontSize(FontSize.LARGE)
}
.width(TitleBarStyle.WEIGHT)
.height(WEIGHT)
.justifyContent(FlexAlign.Start) //内容左对齐
Row() {
//刷新图标
Image($r('app.media.loading'))
.height(TitleBarStyle.IMAGE_LOADING_SIZE)
.width(TitleBarStyle.IMAGE_LOADING_SIZE)
.onClick(() => {
this.isRefreshData = !this.isRefreshData //修改刷新状态
})
}
.width(TitleBarStyle.WEIGHT)
.height(WEIGHT)
.justifyContent(FlexAlign.End) //内容右对齐
}
.width(WEIGHT)
.padding({ left: TitleBarStyle.BAR_MARGIN_HORIZONTAL,
right: TitleBarStyle.BAR_MARGIN_HORIZONTAL })
.margin({ top: TitleBarStyle.BAR_MARGIN_TOP })
.height(TitleBarStyle.BAR_HEIGHT)
.justifyContent(FlexAlign.SpaceAround) // 占满剩余空间
}
}
下面我们来分析一下这些代码,首先我们导入一些需要用到的样式和App上下文,因为点击返回键需要退出App,然后就是通过@Component
装饰的struct表示TitleComponent
结构体具有组件化能力,能够成为一个独立的组件。
然后我们使用到了@Link
修饰isRefreshData
,作为刷新数据的标识,但是在标题组件中并没有对此变量进行初始化,需要父组件在创建标题组件时对isRefreshData
进行赋值,在DevEco Studio中如果你对一个修饰符或者一个API不了解,你可以将鼠标放在上面,例如将鼠标放在@Link上面,会出现一个弹窗。
我们点击Show in API Reference
,编辑器右侧就会出现API的说明。
这个功能还是很Nice的,好了,我们接着来看,isRefreshData
变量在点击刷新图标时会进行更改,通过@Link
装饰的变量可以和父组件的@State
变量建立双向数据绑定,就会将对应该的值传递到父组件,父组件会更新UI,更新UI的时候根据状态切换渲染的数据源。同时定义了一个title,其实我们可以简单的来看,你就把isRefreshData,title
当成标题组件的两个参数,父组件要使用子组件,则必须要传两个值进来。自定义组件必须定义build()
方法,在其中进行UI描述。
接下来就是一个Row表示横向布局,Row里面放了两个Row,第一个左对齐,装载返回图标和标题,第二个Row放刷新图标,标题组件就介绍完了,下面我们可以将它装载的父组件中使用了,修改Index.ets
中的代码,如下所示:
import { TITLE, WEIGHT } from '../constants/Constants';
import { TitleComponent } from '../view/TitleComponent';
@Entry
@Component
struct Index {
// 是否切换RankList的数据
@State isSwitchDataSource: boolean = true
build() {
Column() {
TitleComponent({ isRefreshData: $isSwitchDataSource, title: TITLE })
}
.backgroundColor($r('app.color.background'))
.height(WEIGHT)
.width(WEIGHT)
}
}
这里我们就是在Index父组件中进行使用标题组件,通过 $
操作符来创建引用,使子组件中isRefreshData
和父组件中的isSwitchDataSource
建立双向数据绑定,当isRefreshData
值变化时,父组件Index
中的isSwitchDataSource
值也会随着改变,修改代码之后保存一下,然后可以看到预览页面发生了变化
下面我们来写列表头组件,在view
包下新建一个ListHeaderComponent.ets
文件,里面的代码如下所示:
/**
* 列表头自定义组件
*/
import { FontSize, ListHeaderStyle } from '../constants/Constants'
@Component
export struct ListHeaderComponent {
paddingValue: Padding | Length = 0
widthValue: Length = 0
build() {
Row() {
Text($r('app.string.page_number'))
.fontSize(FontSize.SMALL)
.width(ListHeaderStyle.LAYOUT_WEIGHT_LEFT)
.fontWeight(ListHeaderStyle.FONT_WEIGHT)
.fontColor($r('app.color.font_description'))
Text($r('app.string.page_type'))
.fontSize(FontSize.SMALL)
.width(ListHeaderStyle.LAYOUT_WEIGHT_CENTER)
.fontWeight(ListHeaderStyle.FONT_WEIGHT)
.fontColor($r('app.color.font_description'))
Text($r('app.string.page_vote'))
.fontSize(FontSize.SMALL)
.width(ListHeaderStyle.LAYOUT_WEIGHT_RIGHT)
.fontWeight(ListHeaderStyle.FONT_WEIGHT)
.fontColor($r('app.color.font_description'))
}
.width(this.widthValue)
.padding(this.paddingValue)
}
}
这里的代码就相对来说简单很多了,就是三个文字描述,就没有什么好说的,下面我们直接在Index.ets
中使用,
import { Style, TITLE, WEIGHT } from '../constants/Constants';
import { ListHeaderComponent } from '../view/ListHeaderComponent';
import { TitleComponent } from '../view/TitleComponent';
@Entry
@Component
struct Index {
// 是否切换RankList的数据
@State isSwitchDataSource: boolean = true
build() {
Column() {
//标题栏
TitleComponent({ isRefreshData: $isSwitchDataSource, title: TITLE })
//列表头
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
})
}
.backgroundColor($r('app.color.background'))
.height(WEIGHT)
.width(WEIGHT)
}
}
然后保存一下再看预览效果:
最后我们来看列表item组件,在view
包下新建一个ListItemComponent.ets
文件,代码如下所示:
import { FontSize, FontWeight, ItemStyle, WEIGHT } from '../constants/Constants';
/**
* 列表Item组件
*/
@Component
export struct ListItemComponent {
index: number;
name: Resource;
vote: string;
// 是否切换数据源
isSwitchDataSource: boolean = false;
// 是否改变文字选中文字颜色
@State isChange: boolean = false;
build() {
Row() {
//排名
Column() {
if (this.isRenderCircleText()) {
//渲染
if (this.index !== undefined) {
this.CircleText(this.index);
}
} else {
//不渲染
Text(this.index?.toString())
.lineHeight(ItemStyle.TEXT_LAYOUT_SIZE)
.textAlign(TextAlign.Center)
.width(ItemStyle.TEXT_LAYOUT_SIZE)
.fontWeight(FontWeight.BOLD)
.fontSize(FontSize.SMALL)
}
}
.width(ItemStyle.LAYOUT_WEIGHT_LEFT)
.alignItems(HorizontalAlign.Start)
//种类
Text(this.name)
.width(ItemStyle.LAYOUT_WEIGHT_CENTER)
.fontWeight(FontWeight.BOLDER)
.fontSize(FontSize.MIDDLE)
.fontColor(this.isChange ? ItemStyle.COLOR_BLUE : ItemStyle.COLOR_BLACK) //根据选中状态修改文字颜色
//得票数
Text(this.vote)
.width(ItemStyle.LAYOUT_WEIGHT_RIGHT)
.fontWeight(FontWeight.BOLD)
.fontSize(FontSize.SMALL)
.fontColor(this.isChange ? ItemStyle.COLOR_BLUE : ItemStyle.COLOR_BLACK) //根据选中状态修改文字颜色
}
.height(ItemStyle.BAR_HEIGHT)
.width(WEIGHT)
.onClick(() => { //item 点击事件
this.isChange = !this.isChange;
})
}
/**
* 圆形背景文字
* @param index
*/
@Builder CircleText(index: number) {
Row() {
Text(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'))
}
/**
* 是否渲染圆圈文本
* @returns
*/
isRenderCircleText(): boolean {
// 列表中第三个元素的渲染圆圈文本
return this.index === 1 || this.index === 2 || this.index === 3;
}
}
这个列表Item组件里面的代码比较多,我们来分析一下,首先导入的样式就没有什么好说的,然后我们看ListItemComponent
组件里面定义的5个参数,前三个是Item显示的内容,而isChange
是用来控制item中种类和得票数点击效果的,然后看到build()
方法里面,首先是横向布局,然后处理第一个数据,排名,因为我们希望前3个数据标注一下,所以在ListItemComponent
组件中写了一个isRenderCircleText()
函数,用于判断是否需要进行样式渲染,这里你会看到这里index判断的是1、2和3,但是下标是从0开始的,因此在传index进来的时候,index就是+1的,你不会看到那个排行榜从0开始,然后就是写了一个CircleText()
函数,通过这个函数传递index进去创建一个圆形背景,白色文字的样式UI。再往下走就是种类、得票数的渲染,在设置fontColor(this.isChange ? ItemStyle.COLOR_BLUE : ItemStyle.COLOR_BLACK)
中对isChange
进行判断从而设置不同的文字颜色,最后就是当前item的点击事件,在点击事件中,更改isChange
的值,因为是@State
装饰的,所以会触发UI更新,从而修改文字颜色,那么相信列表Item组件你都了解了,下面我们回到Index父组件。
在父组件使用子组件之前我们再来了解一些关于组建的知识点,通过@Entry
装饰的自定义组件用作页面的默认入口组件,加载页面是,将首先创建并呈现@Entry
装饰的自定义组件,比如当前的Index
,一个页面有且仅能有一个@Entry
,这一点很重要,只有被@Entry
修饰的组件或者其子组件才会在页面上显示,为什么要说这么多呢?
这是因为@Entry
和@Component
所修饰的组件的生命周期有所不同。
通过@Component
所修饰组件,生命周期如下图所示:
这是自定义组件创建到销毁的过程,在这个过程中系统提供了生命周期回调函数:aboutToAppear()
和aboutToDisappear()
,用于通知开发者该自定义组件所处的阶段,aboutToAppear()
在创建自定义组件实例后到执行起build()
函数之前执行,你可以在aboutToAppear()
函数中对UI需要展示的数据进行初始化或者申请定时器资源等操作,这样在后续build()
函数中可以使用这些数据和资源来进行UI展示。可以在aboutToDisappear()
函数中释放不再使用的资源,避免资源泄露。
还需要注意一点,由于这些回调函数是私有的,系统会在特定的时间下自动调用,是无法手动调用这些回调函数的。
通过@Entry
所修饰的页面入口组件,生命周期如下图所示:
可以看到相对于自定义组件,页面入口组件多了onPageShow()
、onBackPress()
和onPageHide()
三个生命周期函数,当用户从手机桌面打开应用,应用进入前台时页面显示,触发onPageShow()
函数,当用户点击home键回到桌面时,应用进入后台时,页面消失,触发onPageHide()
函数,而当通过系统方式执行返回操作时,触发onBackPress()
函数。这里提到了生命周期,是因为下面我们需要用到生命周期。
我们回到Index.ets,然后修改一些代码,修改后如下所示:
import promptAction from '@ohos.promptAction';
import { APP_EXIT_INTERVAL, Style, TIME, TITLE, WEIGHT } from '../constants/Constants';
import { RankData } from '../model/RankData';
import { RankViewModel } from '../model/RankViewModel';
import { ListHeaderComponent } from '../view/ListHeaderComponent';
import { ListItemComponent } from '../view/ListItemComponent';
import { TitleComponent } from '../view/TitleComponent';
let rankModel: RankViewModel = new RankViewModel()
@Entry
@Component
struct Index {
@State dataSource1: RankData[] = []
@State dataSource2: RankData[] = []
// 是否切换RankList的数据
@State isSwitchDataSource: boolean = true
// 记录点击系统导航返回按钮的时间
private clickBackTimeRecord: number = 0;
/**
* 是否显示Toast
* @returns
*/
isShowToast(): boolean {
return new Date().getTime() - this.clickBackTimeRecord > APP_EXIT_INTERVAL
}
/**
* 页面显示回调 - 生命周期
*/
aboutToAppear() {
this.dataSource1 = rankModel.loadRankDataSource1()
this.dataSource2 = rankModel.loadRankDataSource2()
}
/**
* 页面返回回调
* @returns
*/
onBackPress() {
if (this.isShowToast()) {
promptAction.showToast({
message: $r('app.string.prompt_text'), duration: TIME
})
this.clickBackTimeRecord = new Date().getTime();
return false
}
return false
}
build() {
Column() {
//标题栏
TitleComponent({ isRefreshData: $isSwitchDataSource, title: TITLE })
//列表头
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
})
//列表
this.RankList(Style.CONTENT_WIDTH)
}
.backgroundColor($r('app.color.background'))
.height(WEIGHT)
.width(WEIGHT)
}
/**
* 配置列表
* @param widthValue
*/
@Builder RankList(widthValue: Length) {
Column() {
List() {
ForEach(this.isSwitchDataSource ? this.dataSource1 : this.dataSource2,
(item: RankData, index?: number) => {
ListItem() {
// 加载Item
ListItemComponent({ index: (Number(index) + 1), name: item.name, vote: item.vote })
}
}, (item: RankData) => JSON.stringify(item))
}
.width(WEIGHT)
.height(Style.LIST_HEIGHT)
.divider({ strokeWidth: Style.STROKE_WIDTH})
}
.padding({
left: Style.RANK_PADDING,
right: Style.RANK_PADDING
})
.borderRadius(Style.BORDER_RADIUS)
.width(widthValue)
.alignItems(HorizontalAlign.Center)
.backgroundColor(Color.White)
}
}
下面我们进行解析,首先是初始化一个rankModel,这里我们前面写好的一个类,用于提供数据源,然后在Index中,创建两个数组,在回调函数aboutToAppear()
中进行初始化,然后在onBackPress()
回调函数中,处理是否需要显示退出应用时的Toast,return false
表示系统处理返回事件,return true
表示用户自己处理。接下来最重要的就是我们在Index中增加了RankList()
函数,函数中就是通过List()
组件装载ListItem()
,ForEach遍历当前的数据源,再通过调用ListItemComponent()
组件,构建每一个列表Item,注意这里index + 1
,所以0,1,2就变成了1,2,3,列表就写好了。最后在build()
函数中调用RankList()
函数,即可完成整个页面功能。下面我们运行一下,看看效果。
在上面的处理中我们是通过改变Item的状态来达到选中之后的文字颜色改变,当选了其他的Item之后,之前的Item并没有什么变化,那么如果我想做单选的效果呢?
从UI上来看,单选我们首先要记录一个选中位置,然后在点击Item的时候更新选中位置,修改文字颜色,同时要更新整个列表,更新列表的时候自然也会更新Item,那么这里就需要使用到@Link
来装饰选中位置,下面我们修改一下列表Item组件中的代码:
首先增加一个属性,然后根据值匹配当前Item的Index来设置文字颜色,并在点击Item的时候对选中位置重新赋值。
然后回到Index,这里我们增加一个selectedIndex,
这样就实现了单选功能,我就不贴动图了,因为没有真机,这个动图制作起来太麻烦了,你保存一下,在预览效果中也可以测试出来。
如果对你有所帮助的话,不妨 Star 或 Fork,山高水长,后会有期~
源码地址:MyApplication