导读:在本篇文章里,您将掌握监听器、滚动、侧滑等相关内容,助力你开发出更具交互的案例。
我们本次继续完成这个年度计划案例,并依然通过需求驱动的方式学习新知识点,整体效果如下

回顾:上一篇文章我们已经完成了TodoMain的显示,并且完成了从TodoItem里修改完成状态后,也能同步到TodoMain,目前案例还差TodoHeader与TodoInput部分未完成
TodoItem里的数据打勾变化后(完成状态变化),TodoMain已经能成功收到改动了。那么它的父组件,最早持有数组的Index有收到改动吗?并说出理由Index里的数组也跟着变了。因为TodoMain接收Index传递过来的数组时用的是@Link装饰器。而@Link具有父子同步的效果,因此子里TodoMain对数组改动,也能同步到父的Index此时头部的TodoHeader里,需要显示总目标数以及已完成数。也就意味着依赖了存放目标列表的数组
步骤如下
来到TodoHeader里声明一个成员变量接收数组,因为这个组件里仅仅只需要展示,无需改动数组。所以用@Prop修饰即可,并再声明个变量用来记录已完成数量,并把数组长度与已完成数量渲染到进度条与Text里,代码如下
import { TodoModel } from '../viewmodel/TodoModel'
export struct TodoHeader {
// 父传递数组进来,数组长度即为目标总数
todoList: TodoModel[] = []
// 声明一个变量记录已完成数量,将来用
finishedCount: number = 0
build() {
Row() {
.........
// 以下是界面改动部分
Stack() {
Progress({ value: this.finishedCount, total: this.todoList.length, type: ProgressType.Ring })
.width(80)
.height(80)
Text(`${this.finishedCount} / ${this.todoList.length}`)
}
}
...........
}
}来到Index做传递
.........
struct Index {
..........
build() {
Column({ space: 20 }) {
// 主要是这里传参
TodoHeader({ todoList: this.totalFlags })
........
}
}
}经过以上两步此时已能显示出总目标量,并且不管将来是添加了新目标还是删除了旧目标,这里的总量都会跟着变
到目前为止,统计已完成数,也即我们在TodoHeader里声明的成员变量finishedCount依然保持着初始化值:0
此时我们需要想办法统计出已完成数,并且随着数组的改变,已完成数也要重新统计
所以,我们需要的是一种监听数据是否有变化,一旦变化就能执行我们预设的逻辑的机制
鸿蒙开发里提供了这种机制,就叫数据监听器
语法
其他装饰器 ('方法名') 变量名: 变量类型
// 例
('getFinished') todoList: TodoModel[] = []释意:
todoList是否有更改,一旦有更改就调用getFinished这个方法进行处理注意:
export struct TodoHeader {
('getFinished') todoList: TodoModel[] = []
getFinished() {
console.log('数组改变了')
}
........
}此时来到界面我们随便找几个目标打勾,会发现在控制台成功输出数组改变了

此时,已成功监听到数组变动,所以只需在这个方法里,统计出数组里一共有多少个已完成的数量,然后赋值给finishedCount即可
代码如下(注意:这里为了照顾所有编程语言使用者,我用最简单的for循环统计。如懂JS可以使用reduce)
getFinished() {
let total = 0
for (let i = 0; i < this.todoList.length; i++) {
if (this.todoList[i].finished) {
total++
}
}
this.finishedCount = total
}恭喜,至此头部的统计部分我们全部完成

此时完成TodoInput里的添加新目标功能:
整体思路为
TodoInput,然后给输入框加输入完成事件,在事件里把输入的内容加到数组里即可(用@Link装饰,子里变了也能同步到父)步骤:
来到TodoInput,声明一个状态变量接收父的数组
import { TodoModel } from '../viewmodel/TodoModel'
.........
export struct TodoInput {
todoList: TodoModel[]
........
}来到Index做传递
build() {
Column({ space: 20 }) {
......
TodoInput({ todoList: this.totalFlags })
......
}
.....
}这个时候,只需要输入框加输入完成事件,事件里做非空判断,不为空把输入内容加入到数组里,再清空输入框即可
onSubmit......
import { promptAction } from '@kit.ArkUI'
export struct TodoInput {
.........
build() {
Row() {
TextInput({ placeholder: '请输入新目标', text: $$this.newFlag })
.width('90%')
.onSubmit(() => {
if (this.newFlag) {
this.todoList.push({
text: this.newFlag,
finished: false // 新增加的新年目标,默认情况就该是未完成状态
})
this.newFlag = '' // 情况输入框
} else {
promptAction.showToast({ message: '输入内容不能为空' })
}
})
}
........
}
}提示:这里想做的更严谨的同学还可以做去重处理
此时本案例存在一个问题:当数据过多,一页无法展示时,居然没有出现滚动条

原因:
在鸿蒙应用开发中,不是所有组件都具备内容滚动功能。
本例中,包住每一项目标的是Column(如下代码),而Column不具备滚动功能
Column({ space: 10 }) {
ForEach(this.todoList, (item: TodoModel, index: number) => {
// 这里用了ES6简写,完整写法是 TodoItem({ item: item })代表把ForEach里的item传递给TodoItem需要的item
TodoItem({
item, onChange: () => {
this.changeStatus(item, index)
}
})
})所以可以给Column外面包一个Scroll组件,如
Scroll() {
Column({ space: 10 }) {
.......
})
.width('100%')
}
.height(300)这样即具备了滚动功能。
这里为什么还给Scroll设置了高度呢
Scroll设置高度,它的高度就是根据内容自动计算得来,内容一共有多高,它就有多少高度。这样就导致内容永远没超出Scroll,就不具备滚动功能思考:高度写死300合理吗?
肯定不合理,就拿本案例来说,TodoMain是这个页面最后一个组件,它如果高度设小了,就导致后面留了很多空,比较丑,如下图

但是数字写高了又不好,有可能在其他小屏幕会超出,在大屏幕也不够
所以我们希望Scroll这个容器的高度能占用剩余高度
这时候可以用LayoutWeight属性,设置这个容器在父容器里主轴方向剩余空间的占比
故代码改动如下
Scroll() {
Column({ space: 10 }) {
.......
})
.width('100%')
}
.layoutWeight(1)解释;
Scroll在父容器剩余空间里占比1。因为只有设置了它占比,所以就相当于它自己独占剩余部分(如果只有一个容器设置占比,写1或者写99效果都一样)这里再稍微说明一下Scroll组件其他特点
上面虽然用Scroll能起到滚动效果,但我们这里不用它。因为,我们还需要具备侧滑功能,Scroll并不方便
Scroll开发中也相对用的少如果既要能滚动,又要具备侧滑效果,应该用List组件
List组件称之为列表组件,专门用来展示一堆相同宽度的列表项(例如TodoItem)。适合连续、多行呈现同类数据(例如我们本案例里的数组)
特点:当列表项达到一定数量,内容超过屏幕大小时,可以自动提供滚动功能
使用语法
List() {
ListItem() {
内容
}
}说明:List里仅能放ListItem(每一项)或ListGroup(分组),组件关系如下图

例如:

虽然List里组件只能放ListItem与ListItemGroup,但它内部可以使用if-else和ForEach语法做条件渲染、循环渲染
List像Row、Column这些容器一样,也可以设置space参数来控制每一个列表项之间的间距
并且通过List里的ListItem能设置侧滑
具体的关于List还有一些特点,将在下面把它用在本案例里再具体讲解,这里仅仅解释基本作用
注意:在开始之前,如果你按照上面的学习在案例TodoMain组件里使用过
Scroll来学习它,那记得把Scroll删掉
很简单,其实就是把原来的Column改成List,再把ForEach里的TodoItem用ListItem包起来即可,代码如下
.......
export struct TodoMain {
........
build() {
// 把之前的根容器从Column换成了List,List也能用Space属性
List({ space: 10 }) {
ForEach(this.todoList, (item: TodoModel, index: number) => {
// 把TodoItem用ListItem包起来,因为List组件里只能放`ListItem`
ListItem() {
TodoItem({
item, onChange: () => {
this.changeStatus(item, index)
}
})
}
})
}
.width('100%')
.layoutWeight(1)
}
}注意:记得给List高度,否则也一样无法滚动。我们这里依然是让它占用剩余高度
这个时候,我们的新年目标列表即具备滚动功能了,大家可以自行添加新目标任务直到超出显示范围,再试试看是否具备滚动到底的效果
接下来要讲的侧滑用的上它,所以先对它进行介绍
@Builder是用来装饰函数的,被它装饰的函数称之为自定义构造函数,
作用:有些时候,一段UI元素可能需要被复用。即可使用@Builder装饰的函数进行封装
语法
// 注意:写到build函数外面(也就是写成员变量的位置)
// @Builder也可以跟函数名同一行,但是鸿蒙语法规范建议写到上面
函数名 (参数) {
// 封装的UI元素
}例:
myUI () {
Row() {
Text('猫林')
.fontSize(20)
.fontWeight(700)
Button('点赞')
}
.width('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.Red)
}使用时直接像调用函数一样用即可,例如
build () {
Column({ space: 20 }) {
this.myUI()
this.myUI()
}
}效果如下

跟组件的区别:
首先,我们需要给每一项加侧滑功能
上面讲解List时,已经说过ListItem可以方便添加侧滑功能
实现方式也很简单,就是给ListItem添加swipeAction属性即可
用法:
ListItem() {
.....
}
.swipeAction( { start: 自定义构造函数, end: 自定义构造函数 } )start:设置左侧侧滑
end:设置右侧侧滑
可以同时写,代表左侧、右侧都具备侧滑效果(即可以左滑,也可以右滑)
也可以根据业务需求,决定单独要哪边的侧滑,像本案例仅需要右侧的侧滑,因此写end即可
自定义构造函数是用来传入侧滑出来的小界面,例如我们本案例右侧出来的部分即是一个小界面,如下图

那如何把这个小界面传递给ListItem呢?就是通过自定义构造函数传入,也就是用@Builder封装的函数
完成功能:
先来定义侧滑出来的小界面(来到TodoMain)
endSwipe() {
Image($r('app.media.ic_public_delete_filled'))
.width(35)
.height(35)
.fillColor(Color.Red) // 如果图片是svg格式的,用这个属性可以改变图片颜色
}ic_public_delete_filled这张图片格式为svg,并且原本是黑色,但我们界面需求是红色,因此可以通过fillColor属性进行改变颜色。(只对svg格式图片有效)然后给ListItem添加SwipeAction属性,并给end属性(因为需要右侧出现),然后传入上面的自定义构造函数
List({ space: 10 }) {
ForEach(this.todoList, (item: TodoModel, index: number) => {
ListItem() {
.......
}
.swipeAction({ end: this.endSwipe() })
})
}经过以上两步后,我们拖拽每一项往左滑动即可出现红色的删除图标,但此时点击图片没有任何反应
所以,我们还需要给@Builder里的删除图标加点击事件,但此时问题来了。点击事件里我们需要:点哪行的删除,就把这行删掉,也即把数组里这一行的数据删除。所以,我们还需要给@Builder的函数传入当前这行的下标当参数,如下
ForEach(this.todoList, (item: TodoModel, index: number) => {
ListItem() {
......
}
.swipeAction({ end: this.endSwipe(index) }) // 传入下标
})最后回到@Builder,添加形参接收,并完成点击事件即可
endSwipe(index: number) { // 添加形参
Image($r('app.media.ic_public_delete_filled'))
.width(35)
.height(35)
.fillColor(Color.Red)
.onClick(() => { // 添加点击事件
this.todoList.splice(index, 1)
})
}因为之前解释过,todoList是用@Link装饰,所以TodoMain里改变了,也会影响到 Index,既而影响到其他用到同数据的地方
所以到此为止,本案例算完整结束