Harmony ArkTS语言(下)

简介: Harmony ArkTS语言(下)

Harmony ArkTS语言(上)https://developer.aliyun.com/article/1407878


⑤ 标题组件

  下面我们来自定义一个组件,做一个标题栏组件,效果如下图所示:

edfeb8f0fc0e4edf9ae54e7fd8efa0f1.png

首先我们在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上面,会出现一个弹窗。

5886aaf89ee64fd2ab5fe41f8429030b.png

我们点击Show in API Reference,编辑器右侧就会出现API的说明。

977f424ee28f42deae453bd0d861d38f.png

  这个功能还是很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值也会随着改变,修改代码之后保存一下,然后可以看到预览页面发生了变化

77ef2e0859a44b57ad3cff697a40ccc1.png

⑥ 列表头组件

下面我们来写列表头组件,在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)
  }
}


然后保存一下再看预览效果:

f69a7f347e5c42fcb75537148250d9a2.png

⑦ 列表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所修饰组件,生命周期如下图所示:

de3bbec8f96b43c7a1cbee53baa30ac9.png

  这是自定义组件创建到销毁的过程,在这个过程中系统提供了生命周期回调函数:aboutToAppear()aboutToDisappear(),用于通知开发者该自定义组件所处的阶段,aboutToAppear()在创建自定义组件实例后到执行起build()函数之前执行,你可以在aboutToAppear()函数中对UI需要展示的数据进行初始化或者申请定时器资源等操作,这样在后续build()函数中可以使用这些数据和资源来进行UI展示。可以在aboutToDisappear()函数中释放不再使用的资源,避免资源泄露。


  还需要注意一点,由于这些回调函数是私有的,系统会在特定的时间下自动调用,是无法手动调用这些回调函数的。


 通过@Entry所修饰的页面入口组件,生命周期如下图所示:

850d6e01e0fd41a3a079b43aa57429d5.png

  可以看到相对于自定义组件,页面入口组件多了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()函数,即可完成整个页面功能。下面我们运行一下,看看效果。

image.gif

⑩ 单选

  在上面的处理中我们是通过改变Item的状态来达到选中之后的文字颜色改变,当选了其他的Item之后,之前的Item并没有什么变化,那么如果我想做单选的效果呢?

  从UI上来看,单选我们首先要记录一个选中位置,然后在点击Item的时候更新选中位置,修改文字颜色,同时要更新整个列表,更新列表的时候自然也会更新Item,那么这里就需要使用到@Link装饰选中位置,下面我们修改一下列表Item组件中的代码:

4976acb967ef4120bab343e187ea42bf.png

  首先增加一个属性,然后根据值匹配当前Item的Index来设置文字颜色,并在点击Item的时候对选中位置重新赋值。

01c986bbe2f542bebfddf439f6bcfbb9.png

然后回到Index,这里我们增加一个selectedIndex,

9d158fef684b4498a441545f5db1aa75.png

再构建Item时,将这个值传进去

d14ad218c6e54e8d899271aaeeeefbf3.png

  这样就实现了单选功能,我就不贴动图了,因为没有真机,这个动图制作起来太麻烦了,你保存一下,在预览效果中也可以测试出来。


三、源码


如果对你有所帮助的话,不妨 StarFork,山高水长,后会有期~

源码地址:MyApplication

相关文章
|
6月前
|
JavaScript 前端开发 开发者
深入理解ArkTS:Harmony OS 应用开发语言 TypeScript 的基础语法和关键特性
深入理解ArkTS:Harmony OS 应用开发语言 TypeScript 的基础语法和关键特性
607 0
|
开发框架 API 开发者
HarmonyOS学习路之方舟开发框架—学习ArkTS语言(基本语法 二)
在ArkUI中,UI显示的内容均为组件,由框架直接提供的称为系统组件,由开发者定义的称为自定义组件。在进行 UI 界面开发时,通常不是简单的将系统组件进行组合使用,而是需要考虑代码可复用性、业务逻辑与UI分离,后续版本演进等因素。因此,将UI和部分业务逻辑封装成自定义组件是不可或缺的能力。
|
25天前
|
JavaScript 前端开发 安全
TypeScript无缝衔接ArkTS:快速入门鸿蒙ArkTS基本语法
【10月更文挑战第12天】TypeScript无缝衔接ArkTS:快速入门鸿蒙ArkTS基本语法
75 0
TypeScript无缝衔接ArkTS:快速入门鸿蒙ArkTS基本语法
|
4月前
|
存储 Dart 开发工具
flutter实现语言的国际化
【7月更文挑战第16天】
84 1
|
6月前
|
JavaScript 前端开发 索引
【HarmonyOS 4.0 应用开发实战】TypeScript入门之接口详讲
【HarmonyOS 4.0 应用开发实战】TypeScript入门之接口详讲
92 0
|
6月前
|
JavaScript 前端开发 Android开发
Harmony ArkTS语言(上)
Harmony ArkTS语言(上)
223 0
|
开发框架 前端开发 JavaScript
HarmonyOS学习路之方舟开发框架—学习ArkTS语言(基本语法 三)
页面和自定义组件生命周期 在开始之前,先明确自定义组件和页面的关系
HarmonyOS学习路之方舟开发框架—学习ArkTS语言(基本语法 三)
|
开发框架 JavaScript 开发者
HarmonyOS学习路之方舟开发框架—学习ArkTS语言(基本语法 一)
ArkTS是HarmonyOS优选的主力应用开发语言。ArkTS围绕应用开发在TypeScript(简称TS)生态基础上做了进一步扩展,继承了TS的所有特性,是TS的超集。因此,在学习ArkTS语言之前,建议开发者具备TS语言开发能力。
|
开发框架 前端开发 开发者
HarmonyOS学习路之方舟开发框架—学习ArkTS语言(基本语法 五)
如果每个组件的样式都需要单独设置,在开发过程中会出现大量代码在进行重复样式设置,虽然可以复制粘贴,但为了代码简洁性和后续方便维护,我们推出了可以提炼公共样式进行复用的装饰器@Styles。
|
开发框架 开发者
HarmonyOS学习路之方舟开发框架—学习ArkTS语言(基本语法 四)
当创建了自定义组件,并想对该组件添加特定功能时,例如在自定义组件中添加一个点击跳转操作。若直接在组件内嵌入事件方法,将会导致所有引入该自定义组件的地方均增加了该功能。为解决此问题,ArkUI引入了@BuilderParam装饰器,@BuilderParam用来装饰指向@Builder方法的变量,开发者可在初始化自定义组件时对此属性进行赋值,为自定义组件增加特定的功能。该装饰器用于声明任意UI描述的一个元素,类似slot占位符。