加载中...
返回

【HarmonyOS learning】「案例:ArkTS基础知识」分析

项目源码

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.jsonsrc/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 包含刷新图标;

  • 第一个 ImageonClick ,实现返回动作。

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的理解难度,并且从场景上看, indexname 也并不是可选的;

  • @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,分析下来,自觉学到不少,笔记也写出不少,算是很有些内容了。

项目源码

参考资料

有朋自远方来,不亦说乎?
Built with Hugo
Theme Stack designed by Jimmy