輕松玩轉(zhuǎn)HarmonyOS ArkUI 3.0 開發(fā)實(shí)戰(zhàn)—合成1024
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
前言
HarmonyOS ArkUI 3.0正式到來,今天就給大家分享一下我的HarmonyOS ArkUI 3.0 框架試玩初體驗(yàn),以合成1024的開發(fā)實(shí)戰(zhàn),帶大家感受一下HarmonyOS ArkUI 3.0的極簡(jiǎn)開發(fā)。
效果圖

代碼文件結(jié)構(gòu)

正文
一、創(chuàng)建一個(gè)空白的工程
1. 安裝和配置DevEco Studio
DevEco Studio下載、DevEco Studio安裝
2. 創(chuàng)建一個(gè)Empty Ability應(yīng)用
DevEco Studio下載安裝成功后,打開DevEco Studio,點(diǎn)擊左上角的File,點(diǎn)擊New,再選擇New Project,選擇Empty Ability選項(xiàng),點(diǎn)擊Next按鈕

將文件命名為MyETSApplication(文件名不能出現(xiàn)中文或者特殊字符,否則將無法成功創(chuàng)建項(xiàng)目文件),Project Type勾選Application,選擇保存路徑,Language勾選eTS,選擇API7,設(shè)備勾選Phone,最后點(diǎn)擊Finish按鈕。

3. 準(zhǔn)備工作
在entry>src>main>config.json文件中最下方"launchType": "standard"的后面添加以下代碼,這樣就可以實(shí)現(xiàn)去掉應(yīng)用上方的標(biāo)簽欄了。
config.json最下方部分代碼:
- "metaData": {
- "customizeData": [
- {
- "name": "hwc-theme",
- "value": "androidhwext:style/Theme.Emui.Light.NoTitleBar",
- "extra": ""
- }
- ]
- }
二、實(shí)現(xiàn)界面布局
1. 保存圖片
將logo圖片保存到entry>src>main>resources>base>media文件中。
2. 新一代的聲明式UI開發(fā)范式
具體而言,ArkUI 3.0中的新一代聲明式UI開發(fā)范式,主要特征如下:
(1)基于TypeScript擴(kuò)展的聲明式UI描述語法,提供了類自然語言的UI描述和組合。
(2)開箱即用的多態(tài)組件。多態(tài)是指UI描述是統(tǒng)一的,UI呈現(xiàn)在不同類型設(shè)備上會(huì)有所不同。比如Button組件在手機(jī)和手表會(huì)有不同的樣式和交互方式。
(3)多維度的狀態(tài)管理機(jī)制,支持靈活的數(shù)據(jù)驅(qū)動(dòng)的UI變更。
裝飾器:用來裝飾類、結(jié)構(gòu)體、方法以及變量,賦予其特殊的含義,如上述示例中@Entry、@Component、@State都是裝飾器。@Component表示這是個(gè)自定義組件;@Entry則表示這是個(gè)入口組件;@State表示組件中的狀態(tài)變量,這個(gè)狀態(tài)變化會(huì)引起UI變更。
自定義組件:可復(fù)用的UI單元,可組合其它組件,如上述被@Component裝飾的struct Hello。
UI描述:聲明式的方式來描述UI的結(jié)構(gòu),如上述build()方法內(nèi)部的代碼塊。
內(nèi)置組件:框架中默認(rèn)內(nèi)置的基礎(chǔ)和布局組件,可直接被開發(fā)者調(diào)用,比如示例中的Column、Text、Divider、Button。
事件方法:用于添加組件對(duì)事件的響應(yīng)邏輯,統(tǒng)一通過事件方法進(jìn)行設(shè)置,如跟隨在Button后面的onClick()。
屬性方法:用于組件屬性的配置,統(tǒng)一通過屬性方法進(jìn)行設(shè)置,如fontSize()、width()、height()、color()等,可通過鏈?zhǔn)秸{(diào)用的方式設(shè)置多項(xiàng)屬性。
3. 實(shí)現(xiàn)界面
這一次程序用到的裝飾器分別有 @Entry 、 @Component、@State和 @Link 。
裝飾器 @Entry 裝飾的自定義組件用作頁面的默認(rèn)入口組件,加載頁面時(shí),將首先創(chuàng)建并呈現(xiàn)@Entry裝飾的自定義組件。要注意的是:在單個(gè)源文件中,最多可以使用@Entry裝飾一個(gè)自定義組件。
裝飾器 @Component 裝飾的struct表示該結(jié)構(gòu)體具有組件化能力,能夠成為一個(gè)獨(dú)立的組件,這種類型的組件也稱為自定義組件。該組件可以組合其他組件,它通過實(shí)現(xiàn)build方法來描述UI結(jié)構(gòu)。
組件生命周期包括aboutToAppear、aboutToDisappear、onPageShow、onPageHide和onBackPress
aboutToAppear:函數(shù)在創(chuàng)建自定義組件的新實(shí)例后,在執(zhí)行其build函數(shù)之前執(zhí)行。允許在aboutToAppear函數(shù)中改變狀態(tài)變量,這些更改將在后續(xù)執(zhí)行build函數(shù)中生效。
aboutToDisappear:函數(shù)在自定義組件析構(gòu)消耗之前執(zhí)行。不允許在aboutToDisappear函數(shù)中改變狀態(tài)變量,特別是@Link變量的修改可能會(huì)導(dǎo)致應(yīng)用程序行為不穩(wěn)定。
onPageShow:當(dāng)此頁面顯示時(shí)觸發(fā)一次。包括路由過程、應(yīng)用進(jìn)入前后臺(tái)等場(chǎng)景,僅@Entry修飾的自定義組件生效。
onPageHide:當(dāng)此頁面消失時(shí)觸發(fā)一次。包括路由過程、應(yīng)用進(jìn)入前后臺(tái)等場(chǎng)景,僅@Entry修飾的自定義組件生效。
onBackPress:當(dāng)用戶點(diǎn)擊返回按鈕時(shí)觸發(fā),,僅@Entry修飾的自定義組件生效。
裝飾器 @State 裝飾的變量是組件內(nèi)部的狀態(tài)數(shù)據(jù),當(dāng)這些狀態(tài)數(shù)據(jù)被修改時(shí),將會(huì)調(diào)用所在組件的build方法進(jìn)行UI刷新。要注意的是,標(biāo)記為@State的屬性不能直接在組件外部修改,必須為所有@State變量分配初始值。
裝飾器 @Link 裝飾的變量可以和父組件的@State變量建立雙向數(shù)據(jù)綁定。要注意的是,@Link變量不能在組件內(nèi)部進(jìn)行初始化,在創(chuàng)建組件的新實(shí)例時(shí),必須使用命名參數(shù)初始化所有@Link變量。@Link變量可以使用@State變量或@Link變量的引用進(jìn)行初始化。@State變量可以通過’$'操作符創(chuàng)建引用。
index.ets:
先定義一個(gè)方格的背景顏色字典colors,用以繪制不同數(shù)字的背景顏色,和一個(gè)全局變量number,用以ForEach的鍵值生成。
- var number = 1
- const colors={
- "0": "#CDC1B4",
- "2": "#EEE4DA",
- "4": "#ECE0C6",
- "8": "#F2B179",
- "16": "#F59563",
- "32": "#F67C5F",
- "64": "#F65E3B",
- "128": "#EDCF72",
- "256": "#EDCC61",
- "512": "#99CC00",
- "1024": "#83AF9B",
- "2048": "#0099CC",
- "4096": "#0099CC",
- "8192": "#0099CC"
- }
對(duì)于4 x 4的方格,如果要一個(gè)一個(gè)繪制,那么就需要重復(fù)繪制16個(gè)Text組件,而且這些Text組件除了文本之外,其他屬性值都是一樣的,這樣極其繁瑣且沒有必要,體現(xiàn)不了HarmonyOS ArkUI 3.0的極簡(jiǎn)開發(fā)。我們可以把4 x 4的方格以每一行定義成一個(gè)組件,每一行每一行地繪制,這樣能夠極大的減少代碼量。
對(duì)于每一行組件,傳統(tǒng)的方式是重復(fù)繪制4個(gè)Text組件,而且這些Text組件除了文本之外,其他屬性值都是一樣的,同樣極其繁瑣且沒有必要,我們可以采用ForEach循環(huán)渲染來繪制:
- 第一個(gè)參數(shù)必須是數(shù)組:允許空數(shù)組,空數(shù)組場(chǎng)景下不會(huì)創(chuàng)建子組件。同時(shí)允許設(shè)置返回值為數(shù)組類型的函數(shù),例如arr.slice(1, 3),設(shè)置的函數(shù)不得改變包括數(shù)組本身在內(nèi)的任何狀態(tài)變量,如Array.splice、Array.sort或Array.reverse這些原地修改數(shù)組的函數(shù);
- 第二個(gè)參數(shù)用于生成子組件的lambda函數(shù)。它為給定數(shù)組項(xiàng)生成一個(gè)或多個(gè)子組件。單個(gè)組件和子組件列表必須括在大括號(hào)“{…}”中;
- 可選的第三個(gè)參數(shù)是用于鍵值生成的匿名函數(shù)。它為給定數(shù)組項(xiàng)生成唯一且穩(wěn)定的鍵值。當(dāng)子項(xiàng)在數(shù)組中的位置更改時(shí),子項(xiàng)的鍵值不得更改,當(dāng)數(shù)組中的子項(xiàng)被新項(xiàng)替換時(shí),被替換項(xiàng)的鍵值和新項(xiàng)的鍵值必須不同。鍵值生成器的功能是可選的。但是,出于性能原因,強(qiáng)烈建議提供,這使開發(fā)框架能夠更好地識(shí)別數(shù)組更改。如單擊進(jìn)行數(shù)組反向時(shí),如果沒有提供鍵值生成器,則ForEach中的所有節(jié)點(diǎn)都將重建。
使用裝飾器@Component,自定義一個(gè)每一行的組件,用裝飾器@Link定義一個(gè)數(shù)組grids。在build()里面添加彈性布局Flex,使用循環(huán)渲染ForEach來繪制組件Text。對(duì)于每一個(gè)Text組件,文本判斷是否為0,如果值為0,則不顯示,背景顏色采用剛才定義好的背景顏色字典colors對(duì)應(yīng)的背景顏色,文本顏色判斷其值是否為2或4,如果為2或4,則采用顏色#645B52,否則采用背景顏色白色。
- @Component
- struct setText {
- @Link grids: number[]
- build() {
- Flex({ justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center, direction: FlexDirection.Row }) {
- ForEach(this.grids,
- (item: number) => Text(item == 0 ? '' : item.toString())
- .width(70)
- .height(70)
- .textAlign(TextAlign.Center)
- .fontSize(30)
- .margin({ left: 5, top: 5, right: 5, bottom: 5 })
- .backgroundColor(colors[item.toString()])
- .fontColor((item == 2 || item == 4) ? '#645B52' : '#FFFFFF'),
- (item: number) => (number++) + item.toString())
- }
- }
- }
同理,使用裝飾器@Component,自定義一個(gè)按鈕Button組件,用以繪制上下左右四個(gè)按鈕。
- @Component
- struct setButton {
- private dirtext: string
- private dir: string
- @Link Grids: number[][]
- @Link grid1: number[]
- @Link grid2: number[]
- @Link grid3: number[]
- @Link grid4: number[]
- build() {
- Button(this.dirtext)
- .width(60)
- .height(60)
- .fontSize(30)
- .fontWeight(FontWeight.Bold)
- .align(Alignment.Center)
- .backgroundColor('#974B31')
- .fontColor('#FFFFFF')
- .margin({ left: 5, top: 3, right: 5, bottom: 3 })
- }
- }
在裝飾器@Entry裝飾的結(jié)構(gòu)體的build()中,將原來的代碼全部刪掉。
使用裝飾器@State定義一個(gè)二維數(shù)組和四個(gè)一維數(shù)組,添加垂直布局Column,寬和高都為100%,背景顏色為白色,在其中添加Image組件,引用剛才保存好的logo圖片,再添加一個(gè)寬和高都是320,背景顏色為#BBADA0的垂直布局Column,在其添加四個(gè)剛才定義好的行組件setText。
在外圍的垂直布局Column中再添加四個(gè)剛才定義好的按鈕組件setButton,其中中間兩個(gè)按鈕組件位于彈性布局Flex中,最后添加一個(gè)Button組件,文本內(nèi)容為“重新開始”。
- @Entry
- @Component
- struct Index {
- @State grids: number[][] = [[0, 0, 0, 0],
- [0, 2, 0, 0],
- [0, 0, 2, 0],
- [0, 0, 0, 0]]
- @State grid1: number[] = [this.grids[0][0], this.grids[0][1], this.grids[0][2], this.grids[0][3]]
- @State grid2: number[] = [this.grids[1][0], this.grids[1][1], this.grids[1][2], this.grids[1][3]]
- @State grid3: number[] = [this.grids[2][0], this.grids[2][1], this.grids[2][2], this.grids[2][3]]
- @State grid4: number[] = [this.grids[3][0], this.grids[3][1], this.grids[3][2], this.grids[3][3]]
- build() {
- Column() {
- Image($r('app.media.logo1024')).width('100%').height(140).align(Alignment.Center)
- Column() {
- setText({ grids: $grid1 })
- setText({ grids: $grid2 })
- setText({ grids: $grid3 })
- setText({ grids: $grid4 })
- }
- .width(320)
- .height(320)
- .backgroundColor("#BBADA0")
- setButton({dirtext: '↑', dir: 'up', Grids: $grids, grid1: $grid1, grid2: $grid2, grid3: $grid3, grid4: $grid4})
- Flex({ justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center, direction: FlexDirection.Row }) {
- setButton({dirtext: '←', dir: 'left', Grids: $grids, grid1: $grid1, grid2: $grid2, grid3: $grid3, grid4: $grid4})
- setButton({dirtext: '→', dir: 'right', Grids: $grids, grid1: $grid1, grid2: $grid2, grid3: $grid3, grid4: $grid4})
- }
- setButton({dirtext: '↓', dir: 'down', Grids: $grids, grid1: $grid1, grid2: $grid2, grid3: $grid3, grid4: $grid4})
- Button('重新開始')
- .width(180)
- .height(50)
- .fontSize(30)
- .align(Alignment.Center)
- .backgroundColor('#974B31')
- .fontColor('#FFFFFF')
- .margin({ left: 5, top: 3, right: 5, bottom: 3 })
- }
- .width('100%')
- .height('100%')
- .backgroundColor("#FFFFFF")
- .alignItems(HorizontalAlign.Center)
- }
- }
三、編寫邏輯代碼
index.ets:
在結(jié)構(gòu)體setButton中添加四個(gè)函數(shù):
- addTwoOrFourToGrids():用以隨機(jī)生成一個(gè)新的方格數(shù)字,數(shù)字為2或4。
- swipeGrids(direction):用以實(shí)現(xiàn)方格的重新生成。
- changeGrids(direction):用以實(shí)現(xiàn)方格的上下左右移動(dòng)。
- changeString():用以將二維數(shù)組分成四個(gè)一維數(shù)組。
最后在Button組件的屬性里添加一個(gè)點(diǎn)擊事件,依次調(diào)用函數(shù)swipeGrids(direction)、addTwoOrFourToGrids()和changeString()。
- @Component
- struct setButton {
- private dirtext: string
- private dir: string
- @Link Grids: number[][]
- @Link grid1: number[]
- @Link grid2: number[]
- @Link grid3: number[]
- @Link grid4: number[]
- addTwoOrFourToGrids() {
- let array = [];
- for (let row = 0; row < 4; row++)
- for (let column = 0;column < 4; column++)
- if (this.Grids[row][column] == 0)
- array.push([row, column]);
- let randomIndes = Math.floor(Math.random() * array.length);
- let row = array[randomIndes][0];
- let column = array[randomIndes][1];
- if (Math.random() < 0.8) {
- this.Grids[row][column] = 2;
- } else {
- this.Grids[row][column] = 4;
- }
- }
- swipeGrids(direction) {
- let newGrids = this.changeGrids(direction);
- if (newGrids.toString() != this.Grids.toString()) {
- this.Grids = newGrids;
- }
- }
- changeGrids(direction) {
- let newGrids = [[0, 0, 0, 0],
- [0, 0, 0, 0],
- [0, 0, 0, 0],
- [0, 0, 0, 0]];
- if (direction == 'left' || direction == 'right') {
- let step = 1;
- if (direction == 'right') {
- step = -1;//step作為循環(huán)時(shí)數(shù)組下標(biāo)改變的方向
- }
- for (let row = 0; row < 4; row++) {//每一層
- let array = [];
- let column = 0;//如果為left則從0開始right從3開始,
- if (direction == 'right') {
- column = 3;
- }
- for (let i = 0; i < 4; i++) {
- if (this.Grids[row][column] != 0) {//把所有非零元依次放入數(shù)組中
- array.push(this.Grids[row][column]);
- }
- column += step;//當(dāng)direction為left時(shí)則從0向3遞增,為right時(shí)則從3向0遞減
- }
- for (let i = 0; i < array.length - 1; i++) {//訪問當(dāng)前元素及他的下一個(gè)元素,所有循環(huán)次數(shù)為length-1
- if (array[i] == array[i + 1]) {//判斷是否可合并,
- array[i] += array[i + 1];//合并,
- array[i + 1] = 0;//合并后參與合并的第二個(gè)元素消失
- i++;
- }
- }
- column = 0;
- if (direction == 'right') {
- column = 3;
- }
- for (const elem of array) {
- if (elem != 0) {//跳過array里的空元素
- newGrids[row][column] = elem;//把合并后的狀態(tài)賦給新數(shù)組grids,
- column += step;
- }
- }
- }
- } else if (direction == 'up' || direction == 'down') {//同理
- let step = 1;
- if (direction == 'down') {
- step = -1;
- }
- for (let column = 0; column < 4; column++) {
- let array = [];
- let row = 0;
- if (direction == 'down') {
- row = 3;
- }
- for (let i = 0; i < 4; i++) {
- if (this.Grids[row][column] != 0) {
- array.push(this.Grids[row][column]);
- }
- row += step;
- }
- for (let i = 0; i < array.length - 1; i++) {
- if (array[i] == array[i + 1]) {
- array[i] += array[i + 1];
- array[i + 1] = 0;
- i++;
- }
- }
- row = 0;
- if (direction == 'down') {
- row = 3;
- }
- for (const elem of array) {
- if (elem != 0) {
- newGrids[row][column] = elem;
- row += step;
- }
- }
- }
- }
- return newGrids;
- }
- changeString() {
- this.grid1 = [this.Grids[0][0], this.Grids[0][1], this.Grids[0][2], this.Grids[0][3]]
- this.grid2 = [this.Grids[1][0], this.Grids[1][1], this.Grids[1][2], this.Grids[1][3]]
- this.grid3 = [this.Grids[2][0], this.Grids[2][1], this.Grids[2][2], this.Grids[2][3]]
- this.grid4 = [this.Grids[3][0], this.Grids[3][1], this.Grids[3][2], this.Grids[3][3]]
- }
- build() {
- Button(this.dirtext)
- .width(60)
- .height(60)
- .fontSize(30)
- .fontWeight(FontWeight.Bold)
- .align(Alignment.Center)
- .backgroundColor('#974B31')
- .fontColor('#FFFFFF')
- .margin({ left: 5, top: 3, right: 5, bottom: 3 })
- .onClick((event: ClickEvent) => {
- this.swipeGrids(this.dir)
- this.addTwoOrFourToGrids()
- this.changeString()
- })
- }
- }
在結(jié)構(gòu)體index中文本內(nèi)容為“重新開始”的按鈕添加一個(gè)點(diǎn)擊事件,用以重新初始化數(shù)據(jù)。
- @Entry
- @Component
- struct Index {
- @State grids: number[][] = [[0, 0, 0, 0],
- [0, 2, 0, 0],
- [0, 0, 2, 0],
- [0, 0, 0, 0]]
- @State grid1: number[] = [this.grids[0][0], this.grids[0][1], this.grids[0][2], this.grids[0][3]]
- @State grid2: number[] = [this.grids[1][0], this.grids[1][1], this.grids[1][2], this.grids[1][3]]
- @State grid3: number[] = [this.grids[2][0], this.grids[2][1], this.grids[2][2], this.grids[2][3]]
- @State grid4: number[] = [this.grids[3][0], this.grids[3][1], this.grids[3][2], this.grids[3][3]]
- build() {
- Column() {
- Image($r('app.media.logo1024')).width('100%').height(140).align(Alignment.Center)
- Column() {
- setText({ grids: $grid1 })
- setText({ grids: $grid2 })
- setText({ grids: $grid3 })
- setText({ grids: $grid4 })
- }
- .width(320)
- .height(320)
- .backgroundColor("#BBADA0")
- setButton({dirtext: '↑', dir: 'up', Grids: $grids, grid1: $grid1, grid2: $grid2, grid3: $grid3, grid4: $grid4})
- Flex({ justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center, direction: FlexDirection.Row }) {
- setButton({dirtext: '←', dir: 'left', Grids: $grids, grid1: $grid1, grid2: $grid2, grid3: $grid3, grid4: $grid4})
- setButton({dirtext: '→', dir: 'right', Grids: $grids, grid1: $grid1, grid2: $grid2, grid3: $grid3, grid4: $grid4})
- }
- setButton({dirtext: '↓', dir: 'down', Grids: $grids, grid1: $grid1, grid2: $grid2, grid3: $grid3, grid4: $grid4})
- Button('重新開始')
- .width(180)
- .height(50)
- .fontSize(30)
- .align(Alignment.Center)
- .backgroundColor('#974B31')
- .fontColor('#FFFFFF')
- .margin({ left: 5, top: 3, right: 5, bottom: 3 })
- .onClick((event: ClickEvent)=>{
- this.grids = [[0, 0, 0, 0],
- [0, 2, 0, 0],
- [0, 0, 2, 0],
- [0, 0, 0, 0]]
- this.grid1 = [this.grids[0][0], this.grids[0][1], this.grids[0][2], this.grids[0][3]]
- this.grid2 = [this.grids[1][0], this.grids[1][1], this.grids[1][2], this.grids[1][3]]
- this.grid3 = [this.grids[2][0], this.grids[2][1], this.grids[2][2], this.grids[2][3]]
- this.grid4 = [this.grids[3][0], this.grids[3][1], this.grids[3][2], this.grids[3][3]]
- })
- }
- .width('100%')
- .height('100%')
- .backgroundColor("#FFFFFF")
- .alignItems(HorizontalAlign.Center)
- }
- }
寫在最后
HarmonyOS ArkUI 3.0 框架還有很多內(nèi)容在本次程序中沒有涉及到,例如頁面跳轉(zhuǎn)、數(shù)據(jù)管理、分布式數(shù)據(jù)庫、分布式流轉(zhuǎn)、分布式協(xié)同等等,我會(huì)在以后的文章中陸陸續(xù)續(xù)分享我的實(shí)戰(zhàn)操作,希望能與各位一起學(xué)習(xí)相互交流♪(∇*)
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)