【張榮超老師】鴻蒙卡片開發超細致總結
很多朋友都把自己的手機升級為了鴻蒙系統。如果你手頭有兩部或兩部以上鴻蒙系統的手機,就可以盡情地體驗鴻蒙的分布式能力了。如果你手頭只有一部鴻蒙系統的手機,不知道你有沒有感知到:與升級前相比,在用戶體驗上有哪些變化呢?細心體驗就會發現,最大的變化非”卡片”莫屬了!卡片的功能非常強大,用戶無需打開應用,就可以從卡片中獲取應用相關的動態信息,而且還可以與卡片進行交互。最重要的是,在未來,卡片很可能會成為一個巨大的流量入口,從而成為第三方應用廝殺的陣地!
一、什么是卡片
手機升級為鴻蒙系統之后,在某些應用的圖標下方顯示了一條橫線,如下圖所示:

凡是圖標下方顯示一條橫線的應用,都可以在桌面上添加對應的卡片。
以“新浪新聞”這個應用為例,用手指按下圖標的同時往上滑,就會彈出該應用的默認卡片,如下圖所示:

點擊卡片右上角的圖釘,就將卡片固定在了桌面上,如下圖所示:

卡片中的新聞會動態刷新。這樣,用戶無需打開應用,就可以從卡片中獲取應用相關的動態信息。點擊卡片中的某一條新聞,就跳轉到了應用的相關頁面,如下圖所示:
這個卡片設計得不是很好,最好是點擊卡片中的某一條新聞,能跳轉到應用內該條新聞對應的詳情頁面。
再以“音樂”這個應用為例,用手指按下圖標的同時往上滑,就會彈出該應用的默認卡片,點擊卡片右上角的圖釘,就將卡片固定在了桌面上,如下圖所示:
點擊卡片中的按鈕,可以開始播放音樂和暫停播放音樂。這樣,通過與卡片進行交互,用戶無需打開應用,就可以實現應用內的部分操作。
通過這兩個例子,我們看到:卡片是應用內頁面的展現形式,將頁面的重要信息或者操作前置到卡片上,以達到服務直達、減少體驗層級的目的。
二、卡片的數量和尺寸
我們已經知道了:卡片是應用內頁面的展現形式,也就是應用內Page Ability的展現形式。一個應用內包含1~N個Page Ability,我們可以在config.json中為每個Page Ability配置0~16個卡片,而配置的每個卡片可以有1~4個尺寸,因此,每個Page Ability對應的卡片數是0~64。
如何查看一個應用的所有卡片呢?以“日歷”這個應用為例,在桌面上長按其圖標,在彈出的菜單中點擊”服務卡片”,就顯示出了日歷這個應用的所有卡片,如下圖所示:

可以通過上下滑動在卡片之間進行切換。
在所有卡片中,只有一個卡片下方的按鈕顯示為”已設為上滑卡片”,其它卡片下方的按鈕都顯示為”設為上滑卡片”。當某個卡片被設為上滑卡片之后,在桌面上用手指按下應用圖標的同時往上滑,彈出的默認卡片就是該上滑卡片。比如將最后一個月視圖的卡片設為上滑卡片,如下圖所示:
點擊下方的按鈕”設為上滑卡片”,該卡片就會等待用戶將其釘在桌面上,先點擊桌面的空白處將其取消,然后用手指按下應用圖標的同時往上滑,彈出的默認卡片就是月視圖的卡片了。
再次查看“日歷”這個應用的所有卡片。對于任意一個卡片,都可以點擊下方的按鈕”添加到桌面”,將其添加到桌面上。對于同一個卡片,用戶可以在桌面上重復添加多個實例,如下圖所示:
長按桌面上的某個卡片實例,在彈出的菜單中可以移除該卡片,也可以查看其對應應用的所有卡片。此外,用手指按下應用圖標的同時往上滑,然后長按彈出的默認卡片,也可以查看其對應應用的所有卡片。
無論一個應用有多少個卡片,卡片只有4種尺寸,分別是:1×2的微尺寸、 2×2的小尺寸、2×4的中尺寸、4×4的大尺寸。以“相機”這個應用為例,如下圖所示:

對于1×2的微尺寸,會占據1行2列;對于2×2的小尺寸,會占據2行2列;對于2×4的中尺寸,會占據2行4列。同理,對于4×4的大尺寸,會占據4行4列。任何一個卡片的尺寸都屬于這4種尺寸中的其中一種。
三、卡片與原子化服務
與傳統的需要安裝的應用相比,原子化服務是應用的另外一種形態,他是可以提供特定功能的、免安裝的、有獨立入口的應用形態。這里有一個非常重要的關鍵詞:免安裝。原子化服務是鴻蒙系統提供的一種面向未來的服務提供方式,他非常非常的重要,希望大家引起足夠的重視。
給大家舉個例子就明白什么是原子化服務了,如下圖所示:
對于某個傳統方式的、需要安裝的”購物應用T”, 在按照原子化服務理念調整設計后,可以將”商品瀏覽”獨立拆分為一個原子化服務A,將”購物車”獨立拆分為一個原子化服務B,將”支付”獨立拆分為一個原子化服務C,每個原子化服務都提供了特定的功能,而且是免安裝的。用戶在用到某個原子化服務時,再按需進行安裝,系統程序框架會在后臺自動地從原子化服務平臺進行下載和安裝,而無需用戶顯式地手動安裝。
1個原子化服務完成1個特定的便捷服務。原子化服務由1個或多個HAP包組成,1個HAP包對應1個FA或1個PA。每個FA或PA均可獨立運行,完成1個特定功能。原子化服務的大小不能超過10MB。
原子化服務在桌面上是沒有圖標的,用戶可以通過”服務中心”對原子化服務進行統一地查看、搜索和管理。從屏幕左下角或右下角向斜上方滑動,即可進入”服務中心”,如下圖所示:


在”我的服務”板塊,展示了常用的服務;在”發現”板塊,提供了全量的服務供用戶進行管理和使用。
原子化服務在”服務中心”的顯示形式為卡片,可以將其添加到桌面。這就是卡片與原子化服務的關系。
打開DevEco Studio,創建一個HarmonyOS的工程,然后選擇模板Empty Ability(JS)或Empty Ability(Java),點擊按鈕Next,進入到工程配置界面,如下圖所示:

其中,工程類型有兩種:一種是”Service”,也就是原子化服務;另一種是”Application”,也就是傳統的應用。此外,還可以選擇”是否在服務中心進行展示”。我們將工程類型指定為”原子化服務”,并且選擇”在服務中心進行展示”。按照上圖進行配置之后,點擊按鈕Finish以創建一個工程。
接下來,把鴻蒙手機連接到電腦上,對工程的主模塊entry自動簽名,如下圖所示:

簽名之后,將工程運行到鴻蒙手機上,顯示出了主界面。由于該工程的類型是”原子化服務”,所以在桌面上并沒有相應的圖標。由于在創建工程時選擇了”在服務中心進行展示”,因此,打開服務中心,就看到了相應的入口卡片,如下圖所示:

長按卡片,可以進入相應的服務,如下圖所示:

點擊卡片,可以將其”添加到我的服務”或”添加到桌面”,如下圖所示:

好,這樣,就給大家講清楚了卡片、原子化服務和服務中心的關系。
四、卡片的整體框架
華為官方給出了一張卡片的整體框架圖,如下圖所示:

可能很多朋友看到這張圖就直接暈菜了。我們將其簡化一下,如下圖所示:

圖的最左邊是卡片提供方,要么是傳統應用,要么是原子化服務。之所以將兩者稱之為卡片提供方,是因為傳統應用或原子化服務中的Page Ability為卡片提供了表現素材,卡片是Page Ability的表現形式。在傳統應用或原子化服務中定義了卡片的生命周期回調方法。圖的最右邊是卡片使用方,要么是桌面,要么是服務中心。之所以將兩者稱之為卡片使用方,是因為用戶通過桌面或服務中心來使用卡片。圖的中間是卡片管理服務,他是卡片的大管家,是卡片提供方和卡片使用方的中介和橋梁。
以卡片的定時或定點刷新為例,如果一個卡片在config.json中配置了定時或定點刷新,具體的流程如下圖所示:

❶ timer事件會通知卡片管理服務;
❷ 卡片管理服務會去卡片提供方的對象管理模塊中找到對應的卡片提供方;
❸ 卡片提供方回調卡片的生命周期刷新方法;
❹ 卡片提供方將刷新數據返回給卡片管理服務;
❺ 卡片管理服務根據卡片名稱查找卡片使用方;
❻ 卡片管理服務刷新卡片使用方的卡片。
好,了解了卡片的整體框架之后,接下來,我們就正式進入到實操部分。我會首先給大家介紹如何使用JS開發卡片,然后再來介紹如何使用Java開發卡片。
五、 使用JS開發卡片
5.1 使用模板創建卡片
打開DevEco Studio,創建一個HarmonyOS的工程,然后選擇模板Empty Ability(JS),點擊按鈕Next,進入到工程配置界面,如下圖所示:

其中,將工程類型指定為傳統應用,并且不選擇”在服務中心進行展示”。按照上圖進行配置之后,點擊按鈕Finish以創建一個工程。
如何在一個傳統應用的工程中創建卡片呢?在目錄entry上點擊右鍵,在彈出的菜單中選擇New,然后在彈出的子菜單中點擊Service Widget,如下圖所示:

這里的Service Widget指的就是卡片。
在模板選擇界面,選擇基本的模板Grid Pattern,點擊按鈕Next,進入到卡片配置界面,如下圖所示:

首先配置卡片的名稱和描述;然后配置卡片關聯的Page Ability;然后配置卡片的編程語言類型是JS;接下來配置卡片的JS組件名稱;最后配置卡片支持的規格,其中,2*2的小尺寸是必須要支持的,我們再勾選一個1*2的微尺寸。點擊按鈕Finish以創建一個卡片。
重復剛才的操作,在工程中再創建一個卡片,卡片配置界面如下圖所示:

其中,卡片支持的規格,除了默認的2*2小尺寸之外,再勾選一下2*4的中尺寸和4*4的大尺寸。
這樣,DevEco Studio就自動幫我們生成了一些目錄和文件。先打開js子目錄,如下圖所示:

其中,main_widget1和main_widget2是創建卡片時配置的JS組件名稱;index.hml中定義了卡片中包含哪些UI組件;index.css中定義了卡片中的UI組件都長什么樣;index.json中定義了卡片中動態綁定的數據,此外,還可以定義click觸發的事件,稍候會給大家詳細介紹。
再打開java子目錄,如下圖所示:

其中,MainWidget1和MainWidget2是創建卡片時配置的卡片名稱;在MainAbility中添加了卡片的生命周期回調方法,如:onCreateForm()、onUpdateForm()、onTriggerFormEvent()、等。此外,還添加了FormControllerManager、FormController、以及兩個以Impl結尾的實現類,這4個文件到底有什么用呢?稍候給大家介紹。
最后,打開config.json看一下,里面自動添加了很多配置,如下圖所示:

MainAbility添加了標簽”forms”,這里的form就是卡片的意思,和Service Widget是一回事兒。”forms”是一個數組,包含兩個元素,分別表示我們創建的兩個卡片。順便提一下,在前面我們有講到:“可以在config.json中為每個Page Ability配置0~16個卡片”,也就是說,數組”forms”中最多可以包含16個元素。上圖的最下方還添加了一個標簽”js”,”js”也是一個數組,包含三個元素,其中后兩個元素就是兩個卡片對應的js組件,”name”分別是”main_widget1”和”main_widget2”,這兩個值就對應著上面的標簽”forms”中”jsComponentName”的兩個值。也就是說,上面的標簽”forms”中卡片的js組件,是在下面的標簽”js”中定義的。
我們再來看一下上面的標簽”forms”中卡片的配置:
“isDefault”表示該卡片是否為默認的上滑卡片,也就是用手指按下應用圖標的同時往上滑時彈出的卡片。
“scheduledUpdateTime”表示卡片定點刷新的時刻,采用24小時制,精確到分鐘。
“defaultDimension“表示卡片的默認尺寸規格,取值必須在下面的“supportDimensions“所配置的列表中。
“colorMode“表示卡片的主題樣式,默認值是”auto”,表示自適應,還可以取值為”dark”或”light”,分別表示深色主題和淺色主題。
“supportDimensions“表示卡片支持的尺寸規格,也就是我們在創建卡片時配置的尺寸規格。
“updateEnabled”表示卡片是否支持定時刷新或定點刷新,優先選擇定時刷新。
“updateDuration“表示卡片定時刷新的周期。當取值為0時,表示該參數不生效;當取值為正整數N時,表示刷新周期為30*N分鐘。
接下來,我們先對工程的主模塊entry自動簽名,然后看一下運行效果。運行之后,在桌面上應用圖標的下方顯示了一條橫線,表示該應用是支持卡片的,如下圖所示:

在桌面上長按應用的圖標,在彈出的菜單中點擊”服務卡片”,就顯示出了應用的所有卡片,如下圖所示:

上下滑動所有卡片,總共有5個,其中,名為MainWidget1的卡片有兩個,尺寸分別是1*2和2*2,名為MainWidget2的卡片有三個,尺寸分別是2*2、2*4和4*4。此外,名為MainWidget1的2*2的卡片被設為了上滑卡片,這是因為:在config.json中,將”isDefault”設為了”true”,并且將”defaultDimension”設為了” 2*2”,如下圖所示:

5.2 卡片的初始化
當我們在桌面上長按應用的圖標然后顯示所有卡片的時候,MainAbility中卡片的生命周期方法onCreateForm()會被自動回調,方法onCreateForm()的實現如下圖所示:

因為總共有5個卡片,所以方法onCreateForm()會被回調5次,如下圖所示:

在方法onCreateForm()中,分別調用intent.getLongParam()、intent.getStringParam()和intent.getIntParam()獲得了卡片的id、名稱和尺寸。此外,在方法onCreateForm()中可以進行一些卡片的初始化操作。大家想想看,現在應用內只有5個卡片,假如應用內有幾十個卡片,難道要把這幾十個卡片的所有初始化操作都寫在方法onCreateForm()中嗎?顯然是不好的做法!為此,通過模板自動生成的代碼中,為我們提供了FormControllerManager、FormController、XxxImpl這幾個類。其中,FormControllerManager是卡片管理器的大管家;FormController是卡片的管理器,他是一個抽象類;XxxImpl實現了FormController,MainWidget1Impl是名為MainWidget1的卡片對應的卡片控制器,MainWidget2Impl是名為MainWidget2的卡片對應的卡片控制器。這樣,就可以根據卡片的名稱對卡片進行獨立控制了。在方法onCreateForm()中,首先得到FormControllerManager的實例,然后根據卡片ID得到對應的卡片控制器,也就是對應的XxxImpl的實例,最后調用對應的XxxImpl中的方法bindFormData()。
打開MainWidget1Impl,方法bindFormData()中的代碼如下所示:

根據卡片的尺寸,分別設置了兩個變量”mini”和”dim2X4”的值,這兩個變量是子目錄main_widget1中的index.json中的兩個變量。這里順便說一下,在index.hml中,很多變量都使用兩個花括號括了起來,這些變量的值是在程序的運行過程中動態確定的,這種技術稱之為動態綁定。這些變量的初始值在index.json中的標簽”data”中進行了定義。所以,在方法bindFormData()中,根據卡片的尺寸修改了兩個動態綁定的變量的值。我們對自動生成的代碼再修改一下,如下圖所示:

同時,對MainWidget2Impl中的方法bindFormData()也修改一下,如下圖所示:

運行工程,顯示所有卡片,如下圖所示:

所有卡片的標題都被修改了(尺寸最小的卡片除外,因為他本來就不顯示標題)。
5.3 卡片的定點/定時刷新
接下來,我們試一下卡片的定點刷新。打開config.json,先將MainWidget2對應的標簽”updateDuration”修改為0,以關閉定時刷新。對于標簽“scheduledUpdateTime”設定的時刻,當到達之后,MainAbility中卡片的回調方法onUpdateForm()就會被自動調用,如下圖所示:

在方法體的最后,調用了卡片控制器的方法updateFormData()。打開MainWidget2Impl,在方法updateFormData()中,添加如下代碼:

首先,將要刷新的數據存放在一個ZSONObject實例中,然后,將其封裝在一個FormBindingData的實例bindingData中,最后,調用MainAbility的方法updateForm(),并將bindingData作為第二個實參。
打開config.json,將標簽“scheduledUpdateTime”的值修改為當前時刻的兩分鐘之后。
運行工程,將ManWidget2對應的三個卡片都添加到桌面上,當到達設定的定點時刻之后,三個卡片的標題都刷新了,如下圖所示:


5.4 卡片的跳轉事件
對于桌面上名為MainWidget2的三個卡片,接下來我們要實現的功能是:點擊任意一個卡片中左上方的圖片,都跳轉到SecondAbility對應的頁面。
新建一個名為SecondAbility的Page Ability,其所在的包是com.zrc.demos。
打開子目錄main_widget2中的index.json,添加如下配置:

其中,標簽”actions”用于定義所有的事件,目前只定義了一個名為“startSecondAbility”的事件。將標簽“action”的值設置為“router”,表示該事件是一個跳轉事件。標簽“abilityName”的值指定了跳轉的目標Ability。標簽“params”的值指定了跳轉時攜帶的數據。
打開子目錄main_widget2中的index.hml,在標簽image中添加一個屬性onclick,并將值設置為剛剛在index.json中定義的action的名稱“startSecondAbility”,如下圖所示:

打開SecondAbilitySlice,在回調方法onStart()中獲取跳轉時攜帶的數據,如下圖所示:

首先,根據key的值“params”獲得一個字符串格式的JSON數據;然后,調用ZSONObject.stringToZSON()將其轉換為一個ZSONObject的實例data;最后,從data中分別獲得”param1”和”param2”這兩個key對應的value。
運行工程,將名為MainWidget2的任意一個卡片添加到桌面上,點擊卡片中左上方的圖片,跳轉到了SecondAbility對應的頁面,打印出的log如下所示:

5.5 卡片的消息事件
除了支持跳轉事件,卡片還支持消息事件。當觸發消息事件時,卡片所對應Page Ability的生命周期方法onTriggerFormEvent()會被自動回調。接下來我們要實現的功能是:對于名為MainWidget2的任意一個卡片,點擊卡片的空白處,都會回調方法onTriggerFormEvent()。
打開子目錄main_widget2中的index.json,添加如下配置:

在標簽”actions”中再定義了一個名為“sendMessageEvent”的事件。將標簽“action”的值設置為“message”,表示該事件是一個消息事件。標簽“params”中定義了相關的數據。
打開子目錄main_widget2中的index.hml,在第6行的標簽div中添加一個屬性onclick,并將值設置為剛剛在index.json中定義的action的名稱“sendMessageEvent”,如下圖所示:

通過模板創建卡片時自動生成的方法onTriggerFormEvent(),如下圖所示:

在方法體的最后,調用了卡片控制器的方法onTriggerFormEvent()。因此,打開MainWidget2Impl,在方法onTriggerFormEvent()中,添加如下代碼:

首先,message是一個字符串格式的JSON數據;然后,調用ZSONObject.stringToZSON()將message轉換為一個ZSONObject的實例data;最后,從data中分別獲得”p1”和”p2”這兩個key對應的value。
運行工程,將名為MainWidget2的任意一個卡片添加到桌面上,點擊卡片的空白處,卡片所對應Page Ability的生命周期方法onTriggerFormEvent()被自動回調,打印出的log如下所示:

六、使用Java開發卡片
請不要跳過前面的“使用JS開發卡片”,因為前后是有聯系的,即便你對JS不熟悉,相信你也可以看懂的。此外,大家在學習接下來的內容時,請注意與前面的“使用JS開發卡片”進行對比。
6.1 使用模板創建卡片
首先,我們使用模板創建一個Java類型的卡片。在目錄entry上點擊右鍵,在彈出的菜單中選擇New,然后在彈出的子菜單中點擊Service Widget,在模板選擇界面,選擇基本的模板Grid Pattern,點擊按鈕Next,進入到卡片配置界面,如下圖所示:

其中,配置卡片的編程語言類型為JAVA。為了與名為MainWidget2的三個卡片進行對比,我們也配置三個尺寸:2*2、2*4、4*4。
這樣,DevEco Studio就自動幫我們生成了一些目錄和文件。
打開java子目錄,自動創建了一個子目錄mainwidget3,并且自動創建了一個文件MainWidget3Impl以作為卡片MainWidget3的控制器,如下圖所示:

打開js子目錄,沒有自動創建任何目錄和文件,而是在子目錄layout中自動創建了3個布局文件,如下圖所示:

這是3個不同尺寸卡片的布局文件,通過文件名就能看出每個布局文件所對應的卡片尺寸。布局文件里定義了卡片中包含哪些UI組件以及UI組件都長什么樣,因此,布局文件就相當于子目錄js中的hml文件和css文件。
最后,打開config.json看一下,里面自動添加了一些配置,如下圖所示:

與js不同的是,java卡片要分別配置”landscapeLayouts”和“portraitLayouts”,分別表示卡片對應的橫向布局文件和豎向布局文件。值得注意的是:配置的數組中的元素要與“supportDemensions”中的元素一一對應。
運行工程,在桌面上長按應用的圖標,在彈出的菜單中點擊”服務卡片”,就顯示出了應用的所有卡片,現在總共有8個,其中,名為MainWidget3的卡片有3個。
6.2 卡片的初始化
當我們在桌面上長按應用的圖標然后顯示所有卡片的時候,MainAbility中卡片的生命周期方法onCreateForm()會被自動回調,因為總共有8個卡片,所以方法onCreateForm()會被回調8次。在方法體的最后,調用了對應的XxxImpl中的方法bindFormData()。
打開MainWidget3Impl,方法bindFormData()中的代碼如下所示:

三個卡片的布局文件都放在了一個map中,根據卡片的尺寸得到對應的布局文件,然后創建一個提供者卡片信息ProviderFormInfo的實例并將其返回了。
接下來,我們實現與MainWidget2相同的功能:修改三個卡片的標題。首先,在卡片對應的三個布局文件中,都為卡片標題對應的組件Text添加一個id,如下圖所示:

然后,在MainWidget3Impl中添加一個靜態常量,如下圖所示:

在返回ProviderFormInfo的實例之前,先根據卡片的尺寸修改對應的標題,如下圖所示:

首先,構造一個ComponentProvider的實例,用于表示一個Java卡片實例;然后,根據卡片的尺寸修改布局文件中標題的值,其中,標題是通過布局文件中組件Text對應的id進行指定的;最后,調用providerFormInfo的方法mergeActions()并且將componentProvider作為實參傳過去。
運行工程,名為MainWidget3的三個卡片的標題都被修改了,如下圖所示:

6.3 卡片的定點/定時更新
接下來,我們試一下卡片的定點刷新。打開config.json,先將MainWidget3對應的標簽”updateDuration”修改為0,以關閉定時刷新。對于標簽“scheduledUpdateTime”設定的時刻,當到達之后,MainAbility中卡片的回調方法onUpdateForm()就會被自動調用,在方法體的最后,調用了卡片控制器的方法updateFormData()。打開MainWidget3Impl,在方法updateFormData()中,添加如下代碼:

首先,構造一個ComponentProvider的實例,用于表示一個Java卡片實例,傳入的第一個實參是根據卡片尺寸得到的布局文件。然后,調用方法setText()修改卡片的標題;最后,調用MainAbility的方法updateForm(),并將componentProvider作為第二個實參。
打開config.json,將標簽“scheduledUpdateTime”的值修改為當前時刻的兩分鐘之后。
運行工程,將ManWidget3對應的三個卡片都添加到桌面上,當到達設定的定點時刻之后,三個卡片的標題都刷新了。
6.4 卡片的事件
與JS不同的是,Java卡片是通過IntentAgent來設置事件的。以跳轉事件為例,接下來我們實現的功能是:對于名為MainWidget3的、尺寸為2*4的卡片,當點擊標題時跳轉到SecondAbility對應的頁面。
打開MainWidget3Impl,在方法bindFormData()中,當卡片尺寸為2*4時,調用componentProvider的方法setIntentAgent(),傳入的第一個實參是標題在布局文件中的id,第二個實參是用于頁面跳轉的IntentAgent實例。代碼如下所示:

接下來,定義方法getStartAbilityIntentAgent()的具體實現,代碼如下所示:

運行工程,將名為MainWidget3的、尺寸為2*4的卡片添加到桌面,點擊卡片中的標題,跳轉到了SecondAbility對應的頁面。
七、開發卡片到底該使用JS還是使用Java
官方文檔中給出了JS卡片和Java卡片的場景能力差異,如下表所示:

通過該表可以看出:
JS卡片比JAVA卡片支持的控件和能力都更豐富。
Java卡片適合作為一個直達入口,沒有復雜的頁面和事件。
JS卡片適合有復雜界面的卡片。
我個人更推薦使用JS卡片,因為使用起來更靈活、更簡單、功能更強大!
文章相關附件可以點擊下面的原文鏈接前往下載
原文鏈接:https://harmonyos.51cto.com/posts/4776