本篇文章将结合一个小型DEMO,去讲解一下如何使用ArkTS 以及 ArkUI 进行开发. 认真看完这篇文章,你将会收获
- 文本组件,按钮组件,图片组件的基本使用
- Flex,Row 和 Column 布局容器
- 底部导航栏Tabs组件的使用
- if/else条件渲染
- ForEach循环渲染
- @State修饰符定义状态数据
- 等等
话不多说,我们一起开启鸿蒙之旅吧~
先看下效果.
1. 创建项目工程
打开 DevEco Studio,点击 File ==> New ==> Create Project,在 Choose Your Ability Template 下选择模板"Application",选中 Empty Ability 模板,点击 Next 进入下一步配置。
进入到配置工程的页面
- Project name 填写项目名称,默认是
MyApplication
- Bundle name 填写包名,比如
com.arkui.club
等, - "Compile SDK" 和 "Compatible SDK" 选择 10,
- Module name 保持默认值即可,Model 选择 Stage,其他参数保持默认设置即可
点击finish, 等待项目初始化.
2. 项目目录介绍
详细说明可查阅 官网手册
java
entry // HarmonyOS工程模块 src │ └───main │ └───ets │ ├───entryability // 应用入口 ├───entrybackupability // 备份恢复能力 └───pages // 页面文件 │ └───resources // 资源文件(图形、多媒体等) │ └───module.json5 // 模块配置文件 build-profile.json5 // 编译信息配置 hvigorfile.ts // 编译构建任务脚本 obfuscation-rules.txt // 混淆规则文件 oh-package.json5 // 包信息描述 oh_modules // 第三方库依赖
- app.json5: 全局配置
- entry: HAP包模块
- src/main/ets: ArkTS源码
- resources: 应用资源
- module.json5: HAP包配置
- build-profile.json5: 编译配置
- hvigorfile.ts: 构建脚本
- obfuscation-rules.txt: 代码混淆规则
- oh-package.json5: 包名和依赖描述
- oh_modules: 第三方依赖库
3. 编写页面
在编写代码的过程中, 会将一些方法,API 穿插在里面进行讲解, 方便大家更好地去结合DEMO 进行理解, 然后更好地去应用.
3.1 底部导航栏实现
底部导航栏主要使用到了ArkUI中的Tabs组件
ts
declare class TabsAttribute extends CommonMethod<TabsAttribute> { vertical(value: boolean): TabsAttribute; scrollable(value: boolean): TabsAttribute; barMode(value: BarMode): TabsAttribute; barWidth(value: Length): TabsAttribute; barHeight(value: Length): TabsAttribute; animationDuration(value: number): TabsAttribute; }
在这里我们定义三个自定义组件,
ts
// 添加按钮{icon + text} + method @Builder tabAdd() { Column() { Blank() Image(this.index == 0 ? $r('app.media.add') : $r('app.media.add')) .size({ width: 25, height: 25 }) Text('添加') .fontSize(16) .fontColor(this.index == 0 ? "#2a58d0" : "#6b6b6b") Blank() } .height('100%') .width("100%") .onClick(() => { this.index = 0; this.controller.changeIndex(this.index); }) } // 转盘按钮{icon + text} + method @Builder tabTurntable() { Column() { Blank() Image(this.index == 1 ? $r('app.media.zhuanpan') : $r('app.media.zhuanpan')) .size({ width: 25, height: 25 }) Text('小决定') .fontSize(16) .fontColor(this.index == 1 ? "#2a58d0" : "#6b6b6b") Blank() } .height('100%') .width("100%") .onClick(() => { this.index = 1; this.controller.changeIndex(this.index); }) } // 我的按钮{icon + text} + method @Builder tabMine() { // 我的 Column() { Blank() Image(this.index == 2 ? $r('app.media.mine') : $r('app.media.mine')) .size({ width: 25, height: 25 }) Text('我的') .fontSize(16) .fontColor(this.index == 2 ? "#2a58d0" : "#6b6b6b") Blank() } .height('100%') .width("100%") .onClick(() => { this.index = 2; this.controller.changeIndex(this.index); }) }
下面我就先讲解一下上面的代码:
@Builder
我们使用@Builder定义组件 当页面有多个相同的UI结构时,若每个都单独声明,同样会有大量重复的代码。为避免重复代码,可以将相同的UI结构提炼为一个自定义组件,完成UI结构的复用。
除此之外,ArkTS还提供了一种更
轻量的UI结构复用机制
@Builder方法,开发者可以将重复使用的UI元素
抽象成一个@Builder方法,该方法可在build()方法中调用多次,以完成UI结构的复用。
Column
Column 是Ark UI 中的线性布局容器,ArkUI开发框架通过
Row
和Colum
来实现线性布局。
Column
按照竖直方向布局子组件,主轴为竖直方向,纵轴为水平方向。其实很多属性 和我们使用
css中的Flex 属性
一样, 因为内容太多,我就不在这里一一赘述了,大家可以去看下这个网站,关于这个描述的很详细. (www.arkui.club/chapter5/5_…)
Blank
Blank
表示空白填充组件,它用在Row
和Column
组件内来填充组件在主轴方向上的剩余尺寸的能力。
在上面代码中, Blank空白填充组件主要填充了图标上面的剩余大小,以及文字下面的剩余大小.
$r() 加载图片 这种方式一般用于从本地加载图片资源.
- 将图片资源放在resource/main/base/media 目录下.
- 使用
$r('app.media.文件名')
进行获取即可
给每个按钮添加点击事件
ts
export declare class CommonMethod<T> { onClick(event: (event?: ClickEvent) => void): T; }
onClick:给组件添加点击事件的回调,设置该回调后,当点击组件时会触发该回调。回调参数 event
包含了点击信息,比如点击坐标等。
我们先定义一个状态变量, 为index
. 使用@State修饰符号
.
@State 修饰符概述 @State 修饰的变量是组件内部状态数据
,修改时会调用组件的 build() 方法刷新 UI 。(有点vue中的响应式数据了) 其具有以下特点:
- 支持多种数据类型,包括
class
、number
、boolean
、string
及其构成的数组,但不支持object
和any
。 - 是内部私有变量,只能在组件内访问。 - 组件不同实例的内部状态数据相互独立。
- 必须进行本地初始化,且初始值要有意义。
- 创建自定义组件时,可通过状态变量名设置初始值。
这里创建一个index变量, 是为了再点击不同的按钮的时候, 切换不同的值.
至于页面跳转,打大家接着往下看看.
ts
@State index: number = 1; // 选项卡下标,默认为第一个
ts
Column() { Blank() Image(this.index == 1 ? $r('app.media.zhuanpan') : $r('app.media.zhuanpan')) .size({ width: 25, height: 25 }) Text('小决定') .fontSize(16) .fontColor(this.index == 1 ? "#2a58d0" : "#6b6b6b") Blank() } .height('100%') .width("100%") .onClick(() => { this.index = 1; })
3.2 实现点击底部导航栏按钮进行页面跳转
在3.1 中, 我们已经实现了三个自定义的按钮组件. 其中每个组件都是由上方Logo + 下方文字进行构成
. 并且给每个按钮绑定了方法, 即点击的时候修改index状态的值
. 同时对于一些UI组件有了一定的了解.(其实和css大差不差,就是写的形式发生了变化.)
这一小节中,我们去学习一下Tabs组件
,实现页面"跳转".
首先我们需要先认识一下Tabs组件(Tabs、TabContent)
Tabs 组件就像是一个可以切换页面的容器,它里面有几个选项卡,每个选项卡对应一个页面。
ts
Tabs({ // 相关属性设置 }) { // 每个选项卡对应的内容 }
比如说,你可以把 Tabs 组件想象成一个笔记本,笔记本的每一页都可以写不同的内容。 而 Tabs 组件的作用就是让你可以方便地在这些页面之间切换。
ts
Tabs({ // 例如设置切换时的动画时长等属性 animationDuration: 500 // 动画时长为 500 毫秒 }) { // 不同的页面内容 TabContent() { // 页面 1 的内容 } TabContent() { // 页面 2 的内容 } }
在这个组件中,你可以设置选项卡的位置,比如是放在上面还是下面。
ts
Tabs({ barPosition: BarPosition.Top // 设置选项卡位置在上面 }) { // 选项卡对应的页面内容 }
还可以绑定一个控制器,这个控制器就像是一个小管家,来管理选项卡的一些行为。
ts
Tabs({ controller: myController // 绑定名为 myController 的控制器 }) { // 选项卡页面内容 }
此外,你还可以设置选项卡的一些属性,比如高度是多少,是可以滑动切换页面还是固定的,以及切换页面时的动画时长等。
ts
Tabs({ barHeight: 40, // 设置选项卡高度为 40 scrollable: true, // 可滑动切换 animationDuration: 300 // 切换动画时长 300 毫秒 }) { // 选项卡页面内容 }
当前其中的属性远不止这些, 我只是将本次Demo使用的一些属性拿出来和大家说一下, 如果后续想去了解更多关于Tabs组件的内容的话, 可以在这个网站进行查阅 Tabs组件
敲黑板了~ 下面看本次案例是如何使用的.
我们可以看到每个TabContent 的后面都有一个toBar(),这个主要用来指定在上一节中我们自定义的菜单组件的.
点击进行页签切换.
我们new 了一个TabsController对象, 然后再Tabs组件配置项中进行指定. 并且在末尾添加了它自身向外暴露的onChange事件.
然后再去我们定义好了三个组件的onclick下面 加入这句话, 点击每个按钮时,会将 this.index
设置为对应标签页的索引值,并通过 this.controller.changeIndex(this.index)
来通知控制器进行页面切换
ts
this.controller.changeIndex(this.index);
ts
private controller: TabsController = new TabsController(); build() { Tabs({ barPosition: BarPosition.End, // TabBar排列在下方 controller: this.controller // 绑定控制器
3.3 小决定页面编写.
先看一下
分析一下布局结构
- 整体是纵向布局
- 中间那个显示区域,给了固定宽高, 采用flex布局, warp等等
- 包裹按钮的大盒子是flex 横向布局
这里我主要说一下转盘, 哦 不对, 是各个小方格的实现吧.
我们发现这些样式都一样,结构一样,只是数据不同, 那我们就可以考虑使用forEach 循环渲染来实现的
ArkUI开发框架提供循环渲染(ForEach组件)来迭代数组,并为每个数组项创建相应的组件。
ForEach
定义如下:
ts
interface ForEach {( arr: Array<any>, itemGenerator: (item: any, index?: number) => void, keyGenerator?: (item: any, index?: number) => string ): ForEach; }
- arr:必须是数组,允许空数组,空数组场景下不会创建子组件。
- itemGenerator:子组件生成函数,为给定数组项生成一个或多个子组件。
- keyGenerator:匿名参数,用于给定数组项生成唯一且稳定的键值。
我们先定义一个food类,表示每个食物.
ts
// 定义 Foods 类 class Foods { id: string = ""; foodName: string = ""; isActive: boolean = false; constructor(id: string, foodName: string, isActive: boolean) { this.id = id; this.foodName = foodName; this.isActive = isActive; } }
然后去实例化它
ts
@State foodsGroups: Array<Foods> = [ new Foods("1", "红烧肉", false), new Foods("2", "宫保鸡丁", false), new Foods("3", "麻婆豆腐", false), new Foods("4", "清蒸鲈鱼", false), new Foods("5", "糖醋排骨", false), new Foods("6", "扬州炒饭", false), new Foods("7", "水煮牛肉", false), new Foods("8", "番茄炒蛋", false), new Foods("9", "回锅肉", false), new Foods("10", "鱼香肉丝", false), new Foods("11", "油淋鸡", false), new Foods("12", "佛跳墙", false), ];
接着去循环渲染出来结构和数据
ts
Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center, alignContent: FlexAlign.Center }) { ForEach(this.foodsGroups, (item: Foods, index: number) => { Text(item.foodName) .fontSize(16) .padding(10) .margin(10) .width(90) .height(50) .textAlign(TextAlign.Center) .fontColor(item.isActive ? '#ffffff':'#000000') .backgroundColor(item.isActive ? '#f95959' : '#6eb6ff') .borderRadius(10) }, (item: Foods) => {return item.id}) }.width('100%').height(300).backgroundColor('')
除了进行循环渲染, 我们还做了一个根据每个对象的isActive
字段的值, 来动态设置样式属性.这个后面我们会用到的. 因为后面会去动态修改这个字段的值.
这里稍微带一下css的属性
- 整体采用flex布局,采用了横向布局,因为设置了宽,并超出换行, 所以子盒子在一行放不开的时候就会自动换到下一行当中. 然后子盒子在水平和垂直方向保持一个居中.
4. 编写onClick事件
目前我们的静态页面已经完成了,下面我的想法是这样的
- 点击开始, 从当前已有的菜 方格子中随机 筛选出一个
- 点击重置, 回到初始的状态.
4.1 点击之后筛选出一个数组的某一项
ts
// 定义一个函数来随机选择一个食物并设置 isActive private randomizeFoodSelection() { for (let i = 0; i < this.foodsGroups.length; i++) { this.foodsGroups[i].isActive =false; } const randomIndex = Math.floor(Math.random() * this.foodsGroups.length); // 保留randomIndex索引的对象 const itemToKeep = this.foodsGroups[randomIndex]; // 修改对象的isActive属性为true itemToKeep.isActive = true; // 清空数组 this.foodsGroups.length = 0; // 添加保留的对象到数组 this.foodsGroups.push(itemToKeep); }
- 循环遍历 将foodsGroups的每个对象的isActive的值设置为False
- 使用
Math.random
生成一个随机数,并通过计算得到一个在foodsGroups
数组长度范围内的随机索引randomIndex
。- 获取该随机索引对应的元素并将其存储在
itemToKeep
变量中。- 将
itemToKeep
的isActive
属性设置为true
,表示选中。- 将
foodsGroups
数组清空。- 把之前选中的元素
itemToKeep
重新添加到清空后的foodsGroups
数组中。
4.2 恢复数组到初始的状态
将foodsGrops 的值直接服用原来我们复制好的. 使用... 展开运算符 即可实现浅拷贝. 然后将所有的isActive的值设置为False, 因为考虑到上次点击之后将foodsGrops存在一个对象也就是筛选出来的, 他的isActive为
ts
Button('重置', { type: ButtonType.Normal }) .height(40) .width(90) .borderRadius(8) .backgroundColor('#ff6464') // 设置背景色 .onClick(() => { this.foodsGroups = [...this.originalFoodsGroups]; for (let i = 0; i < this.foodsGroups.length; i++) { this.foodsGroups[i].isActive =false; } })
5. 效果展示
6. 该文件的代码(CV可用)
注意图片资源更改一下, 大家下去可以自己去练习一下.
ts
// 定义 Foods 类 class Foods { id: string = ""; foodName: string = ""; isActive: boolean = false; constructor(id: string, foodName: string, isActive: boolean) { this.id = id; this.foodName = foodName; this.isActive = isActive; } } @Entry @Component struct TabsTest { @State index: number = 1; // 选项卡下标,默认为第一个 @State foodsGroups: Array<Foods> = [ new Foods("1", "红烧肉", false), new Foods("2", "宫保鸡丁", false), new Foods("3", "麻婆豆腐", false), new Foods("4", "清蒸鲈鱼", false), new Foods("5", "糖醋排骨", false), new Foods("6", "扬州炒饭", false), new Foods("7", "水煮牛肉", false), new Foods("8", "番茄炒蛋", false), new Foods("9", "回锅肉", false), new Foods("10", "鱼香肉丝", false), new Foods("11", "油淋鸡", false), new Foods("12", "佛跳墙", false), ]; @State originalFoodsGroups:Array<Foods>= [...this.foodsGroups]; // 定义一个函数来随机选择一个食物并设置 isActive private randomizeFoodSelection() { for (let i = 0; i < this.foodsGroups.length; i++) { this.foodsGroups[i].isActive =false; } const randomIndex = Math.floor(Math.random() * this.foodsGroups.length); // 保留randomIndex索引的对象 const itemToKeep = this.foodsGroups[randomIndex]; // 修改对象的isActive属性为true itemToKeep.isActive = true; // 清空数组 this.foodsGroups.length = 0; // 添加保留的对象到数组 this.foodsGroups.push(itemToKeep); } // 添加按钮{icon + text} + method @Builder tabAdd() { Column() { Blank() Image(this.index == 0 ? $r('app.media.add') : $r('app.media.add')) .size({ width: 25, height: 25 }) Text('添加') .fontSize(16) .fontColor(this.index == 0 ? "#2a58d0" : "#6b6b6b") Blank() } .height('100%') .width("100%") .onClick(() => { this.index = 0; this.controller.changeIndex(this.index); }) } // 转盘按钮{icon + text} + method @Builder tabTurntable() { Column() { Blank() Image(this.index == 1 ? $r('app.media.zhuanpan') : $r('app.media.zhuanpan')) .size({ width: 25, height: 25 }) Text('小决定') .fontSize(16) .fontColor(this.index == 1 ? "#2a58d0" : "#6b6b6b") Blank() } .height('100%') .width("100%") .onClick(() => { this.index = 1; this.controller.changeIndex(this.index); }) } // 我的按钮{icon + text} + method @Builder tabMine() { // 我的 Column() { Blank() Image(this.index == 2 ? $r('app.media.mine') : $r('app.media.mine')) .size({ width: 25, height: 25 }) Text('我的') .fontSize(16) .fontColor(this.index == 2 ? "#2a58d0" : "#6b6b6b") Blank() } .height('100%') .width("100%") .onClick(() => { this.index = 2; this.controller.changeIndex(this.index); }) } private controller: TabsController = new TabsController(); build() { Tabs({ barPosition: BarPosition.End, // TabBar排列在下方 controller: this.controller // 绑定控制器 }) { // 第一个页面 TabContent() { Column() { Text('添加') } .width('100%') .height('100%') .alignItems(HorizontalAlign.Center) .justifyContent(FlexAlign.Center) .backgroundColor("#aabbcc") } .tabBar(this.tabAdd) // 第二个页面 TabContent() { Column() { // 1. 文本标语 Text('解决选择困难,就来转一转把') .fontSize(16) .fontWeight(500) // 2. 结果展示 Button() { Text('今晚吃点什么?').fontColor('white') }.width(150).height(50).backgroundColor('SkyBlue').border(null) // 3. 转盘 Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center, alignContent: FlexAlign.Center }) { ForEach(this.foodsGroups, (item: Foods, index: number) => { Text(item.foodName) .fontSize(16) .padding(10) .margin(10) .width(90) .height(50) .textAlign(TextAlign.Center) .fontColor(item.isActive ? '#ffffff':'#000000') .backgroundColor(item.isActive ? '#f95959' : '#6eb6ff') .borderRadius(10) }, (item: Foods) => {return item.id}) }.width('100%').height(300).backgroundColor('') // 4. 开始和重置按钮 Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceAround, alignContent: FlexAlign.Center}) { Button('开始', { type: ButtonType.Normal }) .height(40) .width(90) .borderRadius(8) .backgroundColor('#8d4bbb') // 设置背景色 .onClick(() => { this.randomizeFoodSelection(); }) Button('重置', { type: ButtonType.Normal }) .height(40) .width(90) .borderRadius(8) .backgroundColor('#ff6464') // 设置背景色 .onClick(() => { // 清空数组 this.foodsGroups.length = 0; this.foodsGroups = [...this.originalFoodsGroups]; }) }.width('100%').padding(32) } .width('100%') .height('100%') .backgroundColor("#e9e7ef") .alignItems(HorizontalAlign.Center) .justifyContent(FlexAlign.SpaceAround) .padding(10) } .tabBar(this.tabTurntable) // 第三个页面 TabContent() { Column() { Text('我的') .fontSize(30) } .width('100%') .height('100%') .alignItems(HorizontalAlign.Center) .justifyContent(FlexAlign.Center) .backgroundColor("#ccaabb") } .tabBar(this.tabMine) // 使用自定义TabBar } .width('100%') .height('100%') .barHeight(60) .barMode(BarMode.Fixed) // TabBar均分 .onChange((index: number) => { // 页面切换回调 this.index = index; }) } }