鴻蒙應(yīng)用開(kāi)發(fā)從入門(mén)到入行
第六天 - 數(shù)據(jù)監(jiān)聽(tīng)器肥印、滾動(dòng)识椰、側(cè)滑功能
導(dǎo)讀:在本篇文章里,您將掌握監(jiān)聽(tīng)器竖独、滾動(dòng)裤唠、側(cè)滑等相關(guān)內(nèi)容挤牛,助力你開(kāi)發(fā)出更具交互的案例莹痢。
本次整體學(xué)習(xí)目標(biāo)介紹
-
我們本次繼續(xù)完成這個(gè)
年度計(jì)劃
案例,并依然通過(guò)需求驅(qū)動(dòng)的方式學(xué)習(xí)新知識(shí)點(diǎn)墓赴,整體效果如下
- 回顧:上一篇文章我們已經(jīng)完成了
TodoMain
的顯示竞膳,并且完成了從TodoItem
里修改完成狀態(tài)后,也能同步到TodoMain
诫硕,目前案例還差TodoHeader
與TodoInput
部分未完成
對(duì)上篇文最后留下的互動(dòng)問(wèn)題做解答
- 上篇問(wèn)到:在本案例中
TodoItem
里的數(shù)據(jù)打勾變化后(完成狀態(tài)變化)坦辟,TodoMain
已經(jīng)能成功收到改動(dòng)了。那么它的父組件章办,最早持有數(shù)組的Index
有收到改動(dòng)嗎锉走?并說(shuō)出理由 - 相信到這篇文章的時(shí)候,大家已經(jīng)有正確答案了藕届,這里貓林也做個(gè)解答:
-
Index
里的數(shù)組也跟著變了挪蹭。因?yàn)?code>TodoMain接收Index
傳遞過(guò)來(lái)的數(shù)組時(shí)用的是@Link
裝飾器。而@Link
具有父子同步的效果休偶,因此子里TodoMain
對(duì)數(shù)組改動(dòng)梁厉,也能同步到父的Index
-
年度目標(biāo)案例 - 統(tǒng)計(jì)總量
此時(shí)頭部的
TodoHeader
里,需要顯示總目標(biāo)數(shù)以及已完成數(shù)踏兜。也就意味著依賴(lài)了存放目標(biāo)列表的數(shù)組-
步驟如下
-
來(lái)到
TodoHeader
里聲明一個(gè)成員變量接收數(shù)組词顾,因?yàn)檫@個(gè)組件里僅僅只需要展示八秃,無(wú)需改動(dòng)數(shù)組。所以用@Prop
修飾即可肉盹,并再聲明個(gè)變量用來(lái)記錄已完成數(shù)量昔驱,并把數(shù)組長(zhǎng)度與已完成數(shù)量渲染到進(jìn)度條與Text里,代碼如下import { TodoModel } from '../viewmodel/TodoModel' @Component export struct TodoHeader { // 父?jìng)鬟f數(shù)組進(jìn)來(lái)上忍,數(shù)組長(zhǎng)度即為目標(biāo)總數(shù) @Prop todoList: TodoModel[] = [] // 聲明一個(gè)變量記錄已完成數(shù)量舍悯,將來(lái)用 @State finishedCount: number = 0 build() { Row() { ......... // 以下是界面改動(dòng)部分 Stack() { Progress({ value: this.finishedCount, total: this.todoList.length, type: ProgressType.Ring }) .width(80) .height(80) Text(`${this.finishedCount} / ${this.todoList.length}`) } } ........... } }
-
來(lái)到
Index
做傳遞......... @Entry @Component struct Index { .......... build() { Column({ space: 20 }) { // 主要是這里傳參 TodoHeader({ todoList: this.totalFlags }) ........ } } }
-
經(jīng)過(guò)以上兩步此時(shí)已能顯示出總目標(biāo)量,并且不管將來(lái)是添加了新目標(biāo)還是刪除了舊目標(biāo)睡雇,這里的總量都會(huì)跟著變
- 因?yàn)锧Prop修飾后萌衬,父的數(shù)組有改變會(huì)自動(dòng)同步到子,所以子里也會(huì)有最新數(shù)組數(shù)據(jù)
-
年度目標(biāo)案例 - 統(tǒng)計(jì)已完成數(shù) - 監(jiān)聽(tīng)器的使用
到目前為止它抱,統(tǒng)計(jì)已完成數(shù)秕豫,也即我們?cè)?code>TodoHeader里聲明的成員變量
finishedCount
依然保持著初始化值:0-
此時(shí)我們需要想辦法統(tǒng)計(jì)出已完成數(shù),并且隨著數(shù)組的改變观蓄,已完成數(shù)也要重新統(tǒng)計(jì)
- 例如:數(shù)組里打勾或者取消打勾混移、刪除打勾項(xiàng)等都要實(shí)時(shí)統(tǒng)計(jì)
所以,我們需要的是一種監(jiān)聽(tīng)數(shù)據(jù)是否有變化侮穿,一旦變化就能執(zhí)行我們預(yù)設(shè)的邏輯的機(jī)制
鴻蒙開(kāi)發(fā)里提供了這種機(jī)制歌径,就叫數(shù)據(jù)監(jiān)聽(tīng)器
-
語(yǔ)法
其他裝飾器 @Watch('方法名') 變量名: 變量類(lèi)型 // 例 @Prop @Watch('getFinished') todoList: TodoModel[] = []
-
釋意:
- 代表監(jiān)聽(tīng)
todoList
是否有更改,一旦有更改就調(diào)用getFinished
這個(gè)方法進(jìn)行處理
- 代表監(jiān)聽(tīng)
-
注意:
- 使用監(jiān)聽(tīng)器時(shí)亲茅,請(qǐng)記得必須要聲明它綁定的方法回铛,像上面代碼,我們需要聲明成員方法克锣,如下
export struct TodoHeader { @Prop @Watch('getFinished') todoList: TodoModel[] = [] getFinished() { console.log('數(shù)組改變了') } ........ }
-
此時(shí)來(lái)到界面我們隨便找?guī)讉€(gè)目標(biāo)打勾茵肃,會(huì)發(fā)現(xiàn)在控制臺(tái)成功輸出
數(shù)組改變了
此時(shí),已成功監(jiān)聽(tīng)到數(shù)組變動(dòng)袭祟,所以只需在這個(gè)方法里验残,統(tǒng)計(jì)出數(shù)組里一共有多少個(gè)已完成的數(shù)量,然后賦值給finishedCount即可
-
代碼如下(注意:這里為了照顧所有編程語(yǔ)言使用者巾乳,我用最簡(jiǎn)單的for循環(huán)統(tǒng)計(jì)您没。如懂JS可以使用reduce)
getFinished() { let total = 0 for (let i = 0; i < this.todoList.length; i++) { if (this.todoList[i].finished) { total++ } } this.finishedCount = total }
恭喜,至此頭部的統(tǒng)計(jì)部分我們?nèi)客瓿?/p>
年度目標(biāo)案例 - 添加新目標(biāo)
- 此時(shí)完成
TodoInput
里的添加新目標(biāo)功能: - 整體思路為
- 把數(shù)組傳遞給
TodoInput
胆绊,然后給輸入框加輸入完成事件氨鹏,在事件里把輸入的內(nèi)容加到數(shù)組里即可(用@Link
裝飾,子里變了也能同步到父)
- 把數(shù)組傳遞給
-
步驟:
-
來(lái)到
TodoInput
辑舷,聲明一個(gè)狀態(tài)變量接收父的數(shù)組import { TodoModel } from '../viewmodel/TodoModel' ......... export struct TodoInput { @Link todoList: TodoModel[] ........ }
-
來(lái)到
Index
做傳遞build() { Column({ space: 20 }) { ...... TodoInput({ todoList: this.totalFlags }) ...... } ..... }
-
這個(gè)時(shí)候喻犁,只需要輸入框加輸入完成事件,事件里做非空判斷,不為空把輸入內(nèi)容加入到數(shù)組里肢础,再清空輸入框即可
- 輸入完成即按下輸入法的回車(chē)鍵还栓。事件為:
onSubmit
...... import { promptAction } from '@kit.ArkUI' @Component export struct TodoInput { ......... build() { Row() { TextInput({ placeholder: '請(qǐng)輸入新目標(biāo)', text: $$this.newFlag }) .width('90%') .onSubmit(() => { if (this.newFlag) { this.todoList.push({ text: this.newFlag, finished: false // 新增加的新年目標(biāo),默認(rèn)情況就該是未完成狀態(tài) }) this.newFlag = '' // 情況輸入框 } else { promptAction.showToast({ message: '輸入內(nèi)容不能為空' }) } }) } ........ } }
- 輸入完成即按下輸入法的回車(chē)鍵还栓。事件為:
提示:這里想做的更嚴(yán)謹(jǐn)?shù)耐瑢W(xué)還可以做去重處理
-
年度目標(biāo)案例 - 目標(biāo)列表滾動(dòng)
-
此時(shí)本案例存在一個(gè)問(wèn)題:當(dāng)數(shù)據(jù)過(guò)多传轰,一頁(yè)無(wú)法展示時(shí)剩盒,居然沒(méi)有出現(xiàn)滾動(dòng)條
- 大家可以通過(guò)輸入一些新目標(biāo)來(lái)測(cè)試,你會(huì)發(fā)現(xiàn)鋪滿頁(yè)面后發(fā)現(xiàn)無(wú)法滾動(dòng)
- 如下圖
-
原因:
在鴻蒙應(yīng)用開(kāi)發(fā)中慨蛙,不是所有組件都具備內(nèi)容滾動(dòng)功能辽聊。
-
本例中,包住每一項(xiàng)目標(biāo)的是
Column
(如下代碼)期贫,而Column
不具備滾動(dòng)功能Column({ space: 10 }) { ForEach(this.todoList, (item: TodoModel, index: number) => { // 這里用了ES6簡(jiǎn)寫(xiě)跟匆,完整寫(xiě)法是 TodoItem({ item: item })代表把ForEach里的item傳遞給TodoItem需要的item TodoItem({ item, onChange: () => { this.changeStatus(item, index) } }) })
-
所以可以給
Column
外面包一個(gè)Scroll
組件,如Scroll() { Column({ space: 10 }) { ....... }) .width('100%') } .height(300)
這樣即具備了滾動(dòng)功能通砍。
-
這里為什么還給
Scroll
設(shè)置了高度呢- 如果不設(shè)置高度玛臂,將無(wú)法滾動(dòng)
- 原因:
- 當(dāng)內(nèi)容超出容器大小時(shí),我們才需要滾動(dòng)以及才能擁有滾動(dòng)封孙。所以迹冤,如果內(nèi)容并沒(méi)有超出容器,是不具備滾動(dòng)功能的虎忌。
- 而如果你不給
Scroll
設(shè)置高度泡徙,它的高度就是根據(jù)內(nèi)容自動(dòng)計(jì)算得來(lái),內(nèi)容一共有多高膜蠢,它就有多少高度堪藐。這樣就導(dǎo)致內(nèi)容永遠(yuǎn)沒(méi)超出Scroll,就不具備滾動(dòng)功能
-
思考:高度寫(xiě)死300合理嗎狡蝶?
- 肯定不合理庶橱,就拿本案例來(lái)說(shuō)贮勃,
TodoMain
是這個(gè)頁(yè)面最后一個(gè)組件贪惹,它如果高度設(shè)小了,就導(dǎo)致后面留了很多空寂嘉,比較丑奏瞬,如下圖
- 肯定不合理庶橱,就拿本案例來(lái)說(shuō)贮勃,
- 但是數(shù)字寫(xiě)高了又不好,有可能在其他小屏幕會(huì)超出泉孩,在大屏幕也不夠
- 所以我們**希望Scroll這個(gè)容器的高度能占用剩余高度**
- 這時(shí)候可以用`LayoutWeight`屬性硼端,設(shè)置這個(gè)容器在父容器里主軸方向剩余空間的占比
- 故代碼改動(dòng)如下
```dart
Scroll() {
Column({ space: 10 }) {
.......
})
.width('100%')
}
.layoutWeight(1)
```
- 解釋?zhuān)?
- 這代表讓`Scroll`在父容器剩余空間里占比1。因?yàn)橹挥性O(shè)置了它占比寓搬,所以就相當(dāng)于它自己獨(dú)占剩余部分(如果只有一個(gè)容器設(shè)置占比珍昨,寫(xiě)1或者寫(xiě)99效果都一樣)
-
這里再稍微說(shuō)明一下
Scroll
組件其他特點(diǎn)- 能讓自己的子組件具備滾動(dòng)功能,且它里面只能放一個(gè)子組件。
- 它自己一般不直接存放內(nèi)容镣典,而是讓子組件存放內(nèi)容兔毙。
-
上面雖然用
Scroll
能起到滾動(dòng)效果,但我們這里不用它兄春。因?yàn)榕彀覀冞€需要具備側(cè)滑功能,Scroll并不方便- 事實(shí)上
Scroll
開(kāi)發(fā)中也相對(duì)用的少
- 事實(shí)上
如果既要能滾動(dòng)赶舆,又要具備側(cè)滑效果哑姚,應(yīng)該用
List
組件
知識(shí)點(diǎn) - List組件
List組件稱(chēng)之為列表組件,專(zhuān)門(mén)用來(lái)展示一堆相同寬度的列表項(xiàng)(例如TodoItem)芜茵。適合連續(xù)叙量、多行呈現(xiàn)同類(lèi)數(shù)據(jù)(例如我們本案例里的數(shù)組)
特點(diǎn):當(dāng)列表項(xiàng)達(dá)到一定數(shù)量,內(nèi)容超過(guò)屏幕大小時(shí)九串,可以自動(dòng)提供滾動(dòng)功能
-
使用語(yǔ)法
List() { ListItem() { 內(nèi)容 } }
說(shuō)明:List里僅能放ListItem(每一項(xiàng))或ListGroup(分組)宛乃,組件關(guān)系如下圖
- 例如:
雖然
List
里組件只能放ListItem
與ListItemGroup
,但它內(nèi)部可以使用if-else
和ForEach
語(yǔ)法做條件渲染蒸辆、循環(huán)渲染List
像Row
征炼、Column
這些容器一樣,也可以設(shè)置space
參數(shù)來(lái)控制每一個(gè)列表項(xiàng)之間的間距并且通過(guò)
List
里的ListItem
能設(shè)置側(cè)滑具體的關(guān)于List還有一些特點(diǎn)躬贡,將在下面把它用在本案例里再具體講解谆奥,這里僅僅解釋基本作用
年度目標(biāo)案例 - 使用List改寫(xiě)TodoMain
注意:在開(kāi)始之前,如果你按照上面的學(xué)習(xí)在案例TodoMain組件里使用過(guò)
Scroll
來(lái)學(xué)習(xí)它拂玻,那記得把Scroll刪掉
-
很簡(jiǎn)單酸些,其實(shí)就是把原來(lái)的
Column
改成List
,再把ForEach
里的TodoItem
用ListItem
包起來(lái)即可檐蚜,代碼如下....... @Component export struct TodoMain { ........ build() { // 把之前的根容器從Column換成了List魄懂,List也能用Space屬性 List({ space: 10 }) { ForEach(this.todoList, (item: TodoModel, index: number) => { // 把TodoItem用ListItem包起來(lái),因?yàn)長(zhǎng)ist組件里只能放`ListItem` ListItem() { TodoItem({ item, onChange: () => { this.changeStatus(item, index) } }) } }) } .width('100%') .layoutWeight(1) } }
注意:記得給
List
高度闯第,否則也一樣無(wú)法滾動(dòng)市栗。我們這里依然是讓它占用剩余高度這個(gè)時(shí)候,我們的新年目標(biāo)列表即具備滾動(dòng)功能了咳短,大家可以自行添加新目標(biāo)任務(wù)直到超出顯示范圍填帽,再試試看是否具備滾動(dòng)到底的效果
知識(shí)點(diǎn) - @Builder裝飾器
接下來(lái)要講的側(cè)滑用的上它,所以先對(duì)它進(jìn)行介紹
@Builder是用來(lái)裝飾函數(shù)的咙好,被它裝飾的函數(shù)稱(chēng)之為自定義構(gòu)造函數(shù)篡腌,
作用:有些時(shí)候,一段UI元素可能需要被復(fù)用勾效。即可使用
@Builder
裝飾的函數(shù)進(jìn)行封裝-
語(yǔ)法
// 注意:寫(xiě)到build函數(shù)外面(也就是寫(xiě)成員變量的位置) @Builder // @Builder也可以跟函數(shù)名同一行嘹悼,但是鴻蒙語(yǔ)法規(guī)范建議寫(xiě)到上面 函數(shù)名 (參數(shù)) { // 封裝的UI元素 }
-
例:
@Builder myUI () { Row() { Text('貓林') .fontSize(20) .fontWeight(700) Button('點(diǎn)贊') } .width('100%') .justifyContent(FlexAlign.Center) .backgroundColor(Color.Red) }
-
使用時(shí)直接像調(diào)用函數(shù)一樣用即可叛甫,例如
build () { Column({ space: 20 }) { this.myUI() this.myUI() } }
效果如下
-
跟組件的區(qū)別:
- @Builder更輕量,一般只封裝本頁(yè)面里的UI元素杨伙。而組件可能有自己的狀態(tài)數(shù)據(jù)且能復(fù)用在多個(gè)頁(yè)面
年度目標(biāo)案例 - 實(shí)現(xiàn)側(cè)滑刪除
首先合溺,我們需要給每一項(xiàng)加側(cè)滑功能
上面講解
List
時(shí),已經(jīng)說(shuō)過(guò)ListItem
可以方便添加側(cè)滑功能實(shí)現(xiàn)方式也很簡(jiǎn)單缀台,就是給
ListItem
添加swipeAction
屬性即可-
用法:
ListItem() { ..... } .swipeAction( { start: 自定義構(gòu)造函數(shù), end: 自定義構(gòu)造函數(shù) } )
start:設(shè)置左側(cè)側(cè)滑
end:設(shè)置右側(cè)側(cè)滑
可以同時(shí)寫(xiě)棠赛,代表左側(cè)、右側(cè)都具備側(cè)滑效果(即可以左滑膛腐,也可以右滑)
也可以根據(jù)業(yè)務(wù)需求睛约,決定單獨(dú)要哪邊的側(cè)滑,像本案例僅需要右側(cè)的側(cè)滑哲身,因此寫(xiě)end即可
自定義構(gòu)造函數(shù)是用來(lái)傳入側(cè)滑出來(lái)的小界面辩涝,例如我們本案例右側(cè)出來(lái)的部分即是一個(gè)小界面,如下圖
那如何把這個(gè)小界面?zhèn)鬟f給
ListItem
呢勘天?就是通過(guò)自定義構(gòu)造函數(shù)傳入怔揩,也就是用@Builder
封裝的函數(shù)-
完成功能:
-
先來(lái)定義側(cè)滑出來(lái)的小界面(來(lái)到
TodoMain
)@Builder endSwipe() { Image($r('app.media.ic_public_delete_filled')) .width(35) .height(35) .fillColor(Color.Red) // 如果圖片是svg格式的,用這個(gè)屬性可以改變圖片顏色 }
-
ic_public_delete_filled
這張圖片格式為svg脯丝,并且原本是黑色商膊,但我們界面需求是紅色,因此可以通過(guò)fillColor
屬性進(jìn)行改變顏色宠进。(只對(duì)svg格式圖片有效)
-
-
然后給
ListItem
添加SwipeAction
屬性晕拆,并給end屬性(因?yàn)樾枰覀?cè)出現(xiàn)),然后傳入上面的自定義構(gòu)造函數(shù)List({ space: 10 }) { ForEach(this.todoList, (item: TodoModel, index: number) => { ListItem() { ....... } .swipeAction({ end: this.endSwipe() }) }) }
經(jīng)過(guò)以上兩步后材蹬,我們拖拽每一項(xiàng)往左滑動(dòng)即可出現(xiàn)紅色的刪除圖標(biāo)实幕,但此時(shí)點(diǎn)擊圖片沒(méi)有任何反應(yīng)
-
所以,我們還需要給
@Builder
里的刪除圖標(biāo)加點(diǎn)擊事件堤器,但此時(shí)問(wèn)題來(lái)了昆庇。點(diǎn)擊事件里我們需要:點(diǎn)哪行的刪除,就把這行刪掉闸溃,也即把數(shù)組里這一行的數(shù)據(jù)刪除整吆。所以,我們還需要給@Builder
的函數(shù)傳入當(dāng)前這行的下標(biāo)當(dāng)參數(shù)圈暗,如下ForEach(this.todoList, (item: TodoModel, index: number) => { ListItem() { ...... } .swipeAction({ end: this.endSwipe(index) }) // 傳入下標(biāo) })
-
最后回到
@Builder
掂为,添加形參接收,并完成點(diǎn)擊事件即可@Builder endSwipe(index: number) { // 添加形參 Image($r('app.media.ic_public_delete_filled')) .width(35) .height(35) .fillColor(Color.Red) .onClick(() => { // 添加點(diǎn)擊事件 this.todoList.splice(index, 1) }) }
-
- 因?yàn)橹敖忉屵^(guò)员串,todoList是用
@Link
裝飾,所以TodoMain
里改變了昼扛,也會(huì)影響到Index
寸齐,既而影響到其他用到同數(shù)據(jù)的地方 - 所以到此為止欲诺,本案例算完整結(jié)束
總結(jié)
- 用文字講解案例著實(shí)麻煩。因?yàn)樾枰罅拷貓D渺鹦、以及代碼解釋扰法。以后如果要分享實(shí)戰(zhàn)項(xiàng)目,貓林老師還是盡量以視頻教學(xué)為主吧毅厚。
- 到本篇文章結(jié)束塞颁,如果各位都有每篇認(rèn)真閱讀,其實(shí)就已經(jīng)具備了入門(mén)能力吸耿。以后幾乎遇到任何需求的案例和交互都能實(shí)現(xiàn)祠锣。
- 所以后續(xù),貓林老師打算后續(xù)的系列文章咽安,都是以分享某一個(gè)實(shí)戰(zhàn)技巧伴网、技術(shù)點(diǎn)的形式來(lái)寫(xiě)。例如:如何發(fā)送請(qǐng)求妆棒、如何沉浸式布局澡腾、如何多端適配、如何性能分析與優(yōu)化等糕珊,敬請(qǐng)期待