鴻蒙UI學(xué)習(xí)(一)對(duì)Java布局模板News_Ability的解析(上)
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
前言
學(xué)習(xí)鴻蒙已經(jīng)一個(gè)月了,這一個(gè)月學(xué)到了不少東西:服務(wù)卡片的制作,分布式數(shù)據(jù)庫,分布式任務(wù)調(diào)度等等。。。但是這一個(gè)月來都是一些碎片化的學(xué)習(xí)為多,需要什么才去學(xué)習(xí)什么。我想來一次更為系統(tǒng)的學(xué)習(xí),于是選擇從UI的制作開始一步步地學(xué)習(xí)。對(duì)于UI學(xué)習(xí),以為相比于文檔上面枯燥的說教(當(dāng)然文檔還是得好好看,畢竟基礎(chǔ)很重要),解析IDE里面所自帶的UI模板肯定是更加有趣的方式。事實(shí)上,解析模板比我想象中學(xué)習(xí)的東西要多,很多我以為是這樣做的,卻發(fā)現(xiàn)別人是那樣做的。相信對(duì)于IDE模板的解析相信能夠幫助我更好的運(yùn)用這些組件。我第一個(gè)解析的模板選擇了News_Ability模板。下面將分享我對(duì)這個(gè)模板的解析思路以及學(xué)習(xí)到的東西。如果想要查看更多詳細(xì)的學(xué)習(xí)筆記,
News_Ability模板中的布局與組件
布局:Directional布局
組件:Image,Text,TextField,ListContainer,ScrollView
布局分析

第一個(gè)頁面的分析:由圖片可見,第一個(gè)頁面主要就是兩個(gè)部分,一是新聞種類的選擇欄,二是新聞概覽的欄。一開始我天真的認(rèn)為新聞種類選擇欄是通過TabList實(shí)現(xiàn)的,但通過查看代碼發(fā)現(xiàn)這兩個(gè)板塊都是通過ListContainer實(shí)現(xiàn)的。

第二個(gè)頁面分析:頁面的頂部是文章的題目,閱讀量和點(diǎn)贊數(shù),顯然都是用Text組件實(shí)現(xiàn),并且猜測是用一個(gè)水平方向得Directionnal組件裝起來得。接下來是一個(gè)ScrollView組件,使用這個(gè)組件的理由也很容易理解,文章的內(nèi)容無法保障在當(dāng)前屏幕就全部展示完畢,需要有一個(gè)把內(nèi)容下拉的組件。最底部就是評(píng)論輸入欄,以及點(diǎn)贊,轉(zhuǎn)發(fā),收藏(瘋狂暗示)等操作,查看代碼得知:分別是使用TextField,Image組件來實(shí)現(xiàn)的。
組件ListContainer的使用
組件ListContainer的使用相對(duì)比較復(fù)雜,我覺得還是有必要把文檔里面的東西搬運(yùn)一下。先呈上官方文檔官網(wǎng)文檔鏈接。
概括地來說,LisiContainer的使用有如下幾步
1.在layout目錄下,AbilitySlice對(duì)應(yīng)的布局文件page_listcontainer.xml文件中創(chuàng)建ListContainer。
- <ListContainer
- ohos:id="$+id:list_container"
- ohos:height="200vp"
- ohos:width="300vp"
- ohos:layout_alignment="horizontal_center"/>
2.在layout目錄下新建xml文件(例:item_sample.xml),作為ListContainer的子布局。
- <?xml version="1.0" encoding="utf-8"?>
- <DirectionalLayout
- xmlns:ohos="http://schemas.huawei.com/res/ohos"
- ohos:height="match_content"
- ohos:width="match_parent"
- ohos:left_margin="16vp"
- ohos:right_margin="16vp"
- ohos:orientation="vertical">
- <Text
- ohos:id="$+id:item_index"
- ohos:height="match_content"
- ohos:width="match_content"
- ohos:padding="4vp"
- ohos:text="Item0"
- ohos:text_size="20fp"
- ohos:layout_alignment="center"/>
- </DirectionalLayout>
3.創(chuàng)建SampleItem.java,作為ListContainer的數(shù)據(jù)包裝類
- public class SampleItem {
- private String name;
- public SampleItem(String name) {
- this.name = name;
- }
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- }
4.ListContainer每一行可以為不同的數(shù)據(jù),因此需要適配不同的數(shù)據(jù)結(jié)構(gòu),使其都能添加到ListContainer上。創(chuàng)建SampleItemProvider.java,繼承自BaseItemProvider。必須重寫的方法如下:

示例代碼如下:
- // 請(qǐng)根據(jù)實(shí)際工程/包名引入
- import com.example.myapplication.ResourceTable;
- import ohos.aafwk.ability.AbilitySlice;
- import ohos.agp.components.*;
- import java.util.List;
- public class SampleItemProvider extends BaseItemProvider {
- private List<SampleItem> list;
- private AbilitySlice slice;
- public SampleItemProvider(List<SampleItem> list, AbilitySlice slice) {
- this.list = list;
- this.slice = slice;
- }
- @Override
- public int getCount() {
- return list == null ? 0 : list.size();
- }
- @Override
- public Object getItem(int position) {
- if (list != null && position >= 0 && position < list.size()){
- return list.get(position);
- }
- return null;
- }
- @Override
- public long getItemId(int position) {
- return position;
- }
- @Override
- public Component getComponent(int position, Component convertComponent, ComponentContainer componentContainer) {
- final Component cpt;
- if (convertComponent == null) {
- cpt = LayoutScatter.getInstance(slice).parse(ResourceTable.Layout_item_sample, null, false);
- } else {
- cpt = convertComponent;
- }
- SampleItem sampleItem = list.get(position);
- Text text = (Text) cpt.findComponentById(ResourceTable.Id_item_index);
- text.setText(sampleItem.getName());
- return cpt;
- }
- }
5.在Java代碼中添加ListContainer的數(shù)據(jù),并適配其數(shù)據(jù)結(jié)構(gòu)。
- @Override
- public void onStart(Intent intent) {
- super.onStart(intent);
- super.setUIContent(ResourceTable.Layout_page_listcontainer);
- initListContainer();
- }
- private void initListContainer() {
- ListContainer listContainer = (ListContainer) findComponentById(ResourceTable.Id_list_container);
- List<SampleItem> list = getData();
- SampleItemProvider sampleItemProvider = new SampleItemProvider(list, this);
- listContainer.setItemProvider(sampleItemProvider);
- }
- private ArrayList<SampleItem> getData() {
- ArrayList<SampleItem> list = new ArrayList<>();
- for (int i = 0; i <= 8; i++) {
- list.add(new SampleItem("Item" + i));
- }
- return list;
- }
6.listContainer在sampleItemProvider 初始化后修改數(shù)據(jù)。
- private void initListContainer() {
- ListContainer listContainer = (ListContainer) findComponentById(ResourceTable.Id_list_container);
- List<SampleItem> list = getData();
- SampleItemProvider sampleItemProvider = new SampleItemProvider(list, this);
- listContainer.setItemProvider(sampleItemProvider);
- list.add(new SampleItem("Item" + sampleItemProvider.getCount()));
- listContainer.setBindStateChangedListener(new Component.BindStateChangedListener() {
- @Override
- public void onComponentBoundToWindow(Component component) {
- // ListContainer初始化時(shí)數(shù)據(jù)統(tǒng)一在provider中創(chuàng)建,不直接調(diào)用這個(gè)接口;
- // 建議在onComponentBoundToWindow監(jiān)聽或者其他事件監(jiān)聽中調(diào)用。
- sampleItemProvider.notifyDataChanged();
- }
- @Override
- public void onComponentUnboundFromWindow(Component component) {}
- });
- }
新聞列表界面的解析
xml文件解析
新聞列表界面的UI效果如下圖:

打開文件一看,里面有很多布局文件,但是想要知道呈現(xiàn)在我們眼前的這一畫面究竟是哪一個(gè)文件其實(shí)很簡單。因?yàn)槌绦蚴紫冗\(yùn)行的是MainAbility類,所以打開MainAbility類,可以看見下圖:

上面這幅圖中setMainRoute方法指向了MainAbilityListSlice類,即路由到MainAbilityListSlice類上了,所以顯示在我們面前的畫面是MainAbilitySlice所加載的布局。于是,我們就繼續(xù)到MainAbilityListSlice類的代碼中去找設(shè)置布局的語句。

哎,這不就是找到了這個(gè)布局到底是誰了嗎?接下來我們就可以到resources包里的layout文件夾中解析xml布局代碼了!我們點(diǎn)開這個(gè)xml布局文件:

這是一個(gè)DirectionalLayout,方向是垂直的(vertical)。里面有兩個(gè)列表,一個(gè)是水平方向的,應(yīng)該就是新聞種類的選擇欄了,另一個(gè)在這里并沒有在這里顯式設(shè)定方向,不過顯然可以推斷這個(gè)就是新聞內(nèi)容概覽的列表。中間的Component我們暫時(shí)還不知道它的作用,不過,我們會(huì)知道的。既然已經(jīng)有了兩個(gè)ListContainer,根據(jù)上文我們對(duì)這個(gè)組件的認(rèn)識(shí),可以推斷肯定有兩個(gè)xml文件分別用來編輯這兩個(gè)ListContainer里面內(nèi)容的樣式的,查看layout文件夾里面的xml文件,根據(jù)文件名我們就可以推測哪兩個(gè)文件是用來編輯ListContainer里的內(nèi)容的樣式的。

當(dāng)然,文件名只是起到幫助我們尋找的作用,想要更加嚴(yán)謹(jǐn)?shù)脑挘€是得到j(luò)ava代碼中去尋找語句,打開NewsTypeProvider類,可以看見如下語句,這便驗(yàn)證了我們的猜測了。(同理,另一個(gè)ListContainer也是這樣操作)

接下來就打開這些編輯樣式,打開之前,出于學(xué)習(xí)的目的,我想我們要先有一個(gè)自己大概的猜想,把打開文件作為一種驗(yàn)證的方法。

新聞種類選擇欄很簡單,我猜測只有一個(gè)Text組件,事實(shí)也是這樣的。新聞概覽的樣式如上圖,由圖中我們猜測,這是由一個(gè)水平方向的Directional布局組織起來的額組件,里面放著一個(gè)用來顯示文章標(biāo)題的Text組件和一個(gè)用來顯示文章圖片的Image組件。接下來,讓我們來看看是不是這樣的吧!這里由于截圖大小的問題,就選擇把代碼復(fù)制下來。
- <DirectionalLayout
- xmlns:ohos="http://schemas.huawei.com/res/ohos"
- ohos:height="110vp"
- ohos:width="match_parent"
- ohos:orientation="vertical">
- <!--水平的方向布局,是概覽的每一條新聞的樣式-->
- <DirectionalLayout
- ohos:height="109.5vp"
- ohos:width="match_parent"
- ohos:orientation="horizontal"
- ohos:padding="10vp">
- <!--文章標(biāo)題-->
- <Text
- ohos:id="$+id:item_news_title"
- ohos:height="match_content"
- ohos:width="0vp"
- ohos:max_text_lines="3"
- ohos:multiple_lines="true"
- ohos:right_padding="20vp"
- ohos:text_size="18vp"
- ohos:weight="3"/>
- <!--文章圖片-->
- <Image
- ohos:id="$+id:item_news_image"
- ohos:height="match_parent"
- ohos:width="0vp"
- ohos:scale_mode="stretch"
- ohos:weight="2"/>
- </DirectionalLayout>
- <Component
- ohos:height="0.5vp"
- ohos:width="match_parent"
- ohos:background_element="#FF162AAB"
- />
- </DirectionalLayout>
We are almost right.但是,細(xì)心點(diǎn)就會(huì)發(fā)現(xiàn)這里有兩個(gè)問題:1.父布局是一個(gè)垂直方向的Directional布局,里面才包含一個(gè)水平方向的Directional子布局。2.除了我們的猜測之外,Component組件又出現(xiàn)了,并且它的底色是白色。也許到了有必要搞清楚Component組件是什么東西的時(shí)候了。在我學(xué)習(xí)布局的時(shí)候會(huì)經(jīng)常用到一個(gè)方法:改顏色。默認(rèn)顏色都是白色導(dǎo)致我們有時(shí)候很難分辨我們每個(gè)組件的大小,位置等,把顏色改成便于區(qū)分,花里胡哨的,會(huì)有利于我們這些初學(xué)者學(xué)習(xí)UI布局。
這里把Component的顏色改一下,就會(huì)發(fā)現(xiàn)它的作用類似于一個(gè)分界線,并且它是一條很細(xì)的線,這是因?yàn)樗膆eight只有0.5vp。那我們就改一改它的參數(shù),驗(yàn)證一下我們的猜想。讓我們整活起來:把底色改成喜慶的大紅,再把高度給他調(diào)成20vp。

這里有一點(diǎn)需要注意的,因?yàn)楦季值母叨纫约捌渲幸粋€(gè)子Directional布局(并且這個(gè)布局在Component的上面)的高度已經(jīng)確定了,所以只更改一個(gè)參數(shù)是不夠的,再作如下更改:

順便也把前面的Component組件的參數(shù)改一下,運(yùn)行效果如下:

好家伙,有點(diǎn)過年那味兒了,當(dāng)然我的心情也跟這顏色一樣喜慶,因?yàn)檫@不單驗(yàn)證了我的猜想,我還學(xué)會(huì)了新的布局方法:加一個(gè)Component組件來使各組件有一個(gè)分隔的效果!至此,xml文件我們已經(jīng)搞得清清楚楚了!接下來就去看看java代碼里面有什么可以學(xué)習(xí)的地方吧!
Java代碼解析
根據(jù)上面我們對(duì)ListContainer組件的初步認(rèn)識(shí),在完成了對(duì)xml文件的編輯之后的一步是創(chuàng)建數(shù)據(jù)包裝類。這個(gè)數(shù)據(jù)包裝類放在beans包里面,里面有NewsType和NewsInfo兩個(gè)類,前者很簡單,就不拆開看了,我們粗略地看一下后者。

數(shù)據(jù)封裝類中的屬性分別為:標(biāo)題,種類,圖片路徑,閱讀量,點(diǎn)贊量,內(nèi)容。剩下的是一些設(shè)置或者獲得這些屬性的方法。
下一步呢?是的,我們?nèi)タ纯碢rovider類吧,打開provider文件夾,挑選更為復(fù)雜一點(diǎn)的NewsListProvider類打開看看吧,這個(gè)類繼承自BaseItemProvider(BaseItemProvider文檔鏈接),且必須重寫四個(gè)方法。這個(gè)類有兩個(gè)屬性,一個(gè)是存放新聞信息的list,一個(gè)是上下文。類里面還藏有一個(gè)靜態(tài)內(nèi)部類ViewHolder。代碼如下
- public class NewsListProvider extends BaseItemProvider {
- private List<NewsInfo> newsInfoList;
- private Context context;
- /**
- * constructor function
- *
- * @param listBasicInfo list info
- * @param context context
- */
- public NewsListProvider(List<NewsInfo> listBasicInfo, Context context) {
- this.newsInfoList = listBasicInfo;
- this.context = context;
- }
- //必須重寫的方法1:返回填充的表項(xiàng)個(gè)數(shù)
- @Override
- public int getCount() {
- return newsInfoList == null ? 0 : newsInfoList.size();
- }
- //必須重寫的方法2:根據(jù)id(表項(xiàng)的位置)返回表項(xiàng)
- @Override
- public Object getItem(int position) {
- return newsInfoList.get(position);
- }
- //必須重寫的方法3:返回表項(xiàng)的id
- @Override
- public long getItemId(int position) {
- return position;
- }
- //必須重寫的方法4:根據(jù)id返回組件
- @Override
- public Component getComponent(int position, Component component, ComponentContainer componentContainer) {
- ViewHolder viewHolder;
- Component temp = component;
- //如果還沒有component,那么就進(jìn)行如下操作:
- if (temp == null) {
- //1.獲得組件的布局
- temp = LayoutScatter.getInstance(context).parse(ResourceTable.Layout_item_news_layout, null, false);
- //2.創(chuàng)建一個(gè)viewHolder,并設(shè)置它的屬性
- viewHolder = new ViewHolder();
- //設(shè)置它的標(biāo)題:從temp中的布局找到里面的Text組件,并將其設(shè)置成這一個(gè)viewHolder的標(biāo)題
- viewHolder.title = (Text) temp.findComponentById(ResourceTable.Id_item_news_title);
- //設(shè)置它的圖片:從temp中的布局找到里面的Image組件,并將其設(shè)置成這一個(gè)viewHolder的圖片
- viewHolder.image = (Image) temp.findComponentById(ResourceTable.Id_item_news_image);
- //設(shè)置這個(gè)組件的標(biāo)簽,viewHolder就是這個(gè)組件的標(biāo)簽
- temp.setTag(viewHolder);
- }
- //如果已經(jīng)有component了,通過組件的標(biāo)簽來獲得viewHolder里面的內(nèi)容
- else {
- viewHolder = (ViewHolder) temp.getTag();
- }
- viewHolder.title.setText(newsInfoList.get(position).getTitle());
- viewHolder.image.setPixelMap(CommonUtils.getPixelMapFromPath(context, newsInfoList.get(position).getImgUrl()));
- //返回這個(gè)子組件
- return temp;
- }
- private static class ViewHolder
- {
- Text title;
- Image image;
- }
每一語句的內(nèi)容都注釋到上面的代碼里面了,因此不再贅述。準(zhǔn)備工作都做好了,下面回到MainAbilityListSlice類中繼續(xù)看代碼。接下來一步,根據(jù)上面的ListContainer組件的使用方法,我們要在Java代碼中添加ListContainer的數(shù)據(jù),并適配其數(shù)據(jù)結(jié)構(gòu)。詳細(xì)的解析在代碼中,如下:
- private void initData()
- {
- //得到新聞?lì)愋偷臄?shù)據(jù),并創(chuàng)建一個(gè)類型為NewsType的列表來存放這些數(shù)據(jù),由于這些數(shù)據(jù)都放在路徑為"entry/resources/rawfile/news_type_datas.json"的json文件中,所以用如下語句提取
- List<NewsType> newsTypeList =
- ZSONArray.stringToClassList(
- CommonUtils.getStringFromJsonPath(this, "entry/resources/rawfile/news_type_datas.json"),
- NewsType.class);
- //得到所有新聞概覽的數(shù)據(jù),并創(chuàng)建一個(gè)類型為NewsInfo的列表來存放這些數(shù)據(jù),由于這些數(shù)據(jù)都放在路徑為"entry/resources/rawfile/news_datas.json"的json文件中,所以用如下語句提取
- totalNewsDataList =
- ZSONArray.stringToClassList(
- CommonUtils.getStringFromJsonPath(this, "entry/resources/rawfile/news_datas.json"),
- NewsInfo.class);
- //創(chuàng)建一個(gè)用來存儲(chǔ)新聞數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu)ArrayList
- newsDataList = new ArrayList<>();
- newsDataList.addAll(totalNewsDataList);
- //實(shí)例化下面兩個(gè)類
- newsTypeProvider = new NewsTypeProvider(newsTypeList, this);
- newsListProvider = new NewsListProvider(newsDataList, this);
- }
然后就是設(shè)置響應(yīng)點(diǎn)擊的監(jiān)聽器,在這個(gè)模板中,點(diǎn)擊新聞種類欄的項(xiàng)可以篩選新聞,并且新聞種類項(xiàng)的樣式會(huì)發(fā)生變化,點(diǎn)擊新聞概覽的項(xiàng)可以跳轉(zhuǎn)到詳細(xì)頁面。由已經(jīng)有的代碼可以猜測前者是通過改變Listcontainer的項(xiàng)目的內(nèi)容來實(shí)現(xiàn),而后者則是通過帶參數(shù)的頁面跳轉(zhuǎn)來實(shí)現(xiàn)的。代碼及詳細(xì)注釋如下:
- private void initListener() {
- //新聞種類選擇欄的項(xiàng)被點(diǎn)擊時(shí)的回調(diào)事件
- selectorListContainer.setItemClickedListener(
- //
- (listContainer, component, position, id) -> {
- //當(dāng)被點(diǎn)擊到,不著急變樣式,先獲得這個(gè)Text
- setCategorizationFocus(false);
- selectText = (Text) component.findComponentById(ResourceTable.Id_news_type_text);
- //獲得完了,再進(jìn)行樣式變換
- setCategorizationFocus(true);//樣式變換
- //清除現(xiàn)有的newsDataList,為的是為下面更新newsDataList作準(zhǔn)備,這樣就可以做到跟新聞概覽框的內(nèi)容與新聞種類相對(duì)應(yīng)
- newsDataList.clear();
- for (NewsInfo mTotalNewsData : totalNewsDataList) {
- //把屬于當(dāng)前被點(diǎn)擊的新聞種類的新聞數(shù)據(jù)加進(jìn)newsDataList中,做到同步的效果
- if (selectText.getText().equals(mTotalNewsData.getType()) || position == 0) {
- newsDataList.add(mTotalNewsData);
- }
- }
- //更新列表內(nèi)容
- updateListView();
- });
- //新聞概覽表中的元素被點(diǎn)擊的時(shí)候,進(jìn)行跳轉(zhuǎn)操作
- newsListContainer.setItemClickedListener(
- (listContainer, component, position, id) -> {
- Intent intent = new Intent();
- Operation operation =
- new Intent.OperationBuilder()
- .withBundleName(getBundleName())
- .withAbilityName(MainAbility.class.getName())
- .withAction("action.detail")
- .build();
- intent.setOperation(operation);
- //設(shè)置跳轉(zhuǎn)攜帶的參數(shù)
- intent.setParam(MainAbilityDetailSlice.INTENT_TITLE, newsDataList.get(position).getTitle());
- intent.setParam(MainAbilityDetailSlice.INTENT_READ, newsDataList.get(position).getReads());
- intent.setParam(MainAbilityDetailSlice.INTENT_LIKE, newsDataList.get(position).getLikes());
- intent.setParam(MainAbilityDetailSlice.INTENT_CONTENT, newsDataList.get(position).getContent());
- intent.setParam(MainAbilityDetailSlice.INTENT_IMAGE, newsDataList.get(position).getImgUrl());
- startAbility(intent);
- });
- }
到此,第一個(gè)頁面的實(shí)現(xiàn)思路已經(jīng)被我們剖析得很清楚了。不知不覺已經(jīng)碼了8000字,如果再把第二個(gè)頁面在本貼中剖析,恐淪為“又長又臭”之作,因此決定把這個(gè)模板的解析分為兩篇文章,這樣大家讀起來也清晰一點(diǎn),我寫起來也輕松一點(diǎn)。那么就先對(duì)本文來個(gè)小結(jié)吧!
小結(jié)
1.在解析這個(gè)模板之前,我一直以為新聞種類欄使用的是Tablist組件來實(shí)現(xiàn)的。使用Tablist會(huì)出現(xiàn)的問題就是如果標(biāo)簽過多,多到溢出屏幕,無法做到滑動(dòng)就能夠查看的效果(如果可以做到,各位大佬能否指點(diǎn)一二?)。
2.解析過程中我學(xué)習(xí)到了ListContainer的使用方法,并且對(duì)其有了較為深刻的認(rèn)識(shí)。
3.解析思路,從MainAbility出發(fā),順藤摸瓜,遇到陌生的組件就去查看文檔,然后配合模板的實(shí)戰(zhàn)例子使用,效果杠杠的。點(diǎn)開詳細(xì)代碼前,須有自己的想法或者是大概的思路。
4.不了解的功能的組件可以通過設(shè)置一個(gè)辣眼睛的背景顏色來凸顯它,這個(gè)方法在自己寫布局的時(shí)候也很好用。
5.Component可作為分界的手段。
更多資料請(qǐng)關(guān)注我們的項(xiàng)目 :Awesome-HarmonyOS_木棉花
本項(xiàng)目會(huì)長期更新 ,希望隨著鴻蒙一同成長變強(qiáng)的既有我們,也有正在看著這個(gè)項(xiàng)目的你。明年3月,深大校園內(nèi)的木棉花會(huì)盛開,那時(shí),鴻蒙也會(huì)變的更好,愿這花開,有你我的一份。
文章相關(guān)附件可以點(diǎn)擊下面的原文鏈接前往下載
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)

























