Harmony ArkTS语言(上)https://developer.aliyun.com/article/1407878
⑤ 标题组件
下面我们来自定义一个组件,做一个标题栏组件,效果如下图所示:
首先我们在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组件
最后我们来看列表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,
再构建Item时,将这个值传进去
这样就实现了单选功能,我就不贴动图了,因为没有真机,这个动图制作起来太麻烦了,你保存一下,在预览效果中也可以测试出来。
三、源码
如果对你有所帮助的话,不妨 Star 或 Fork,山高水长,后会有期~
源码地址:MyApplication