鴻蒙開源第三方組件—自定義流式布局組件FlowLayout_ohos
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
前言
基于安卓平臺的自定義流式布局組件FlowLayout,實現(xiàn)了鴻蒙的功能化遷移和重構(gòu)。代碼已經(jīng)開源到(https://gitee.com/isrc_ohos/flow-layout_ohos),歡迎各位開發(fā)者下載使用并提出寶貴意見!
背景
流式布局也叫百分比布局,它具有指定的對齊方式、水平間隙和垂直間隙,特別適用于多標(biāo)簽的展示,可以實現(xiàn)組件中的標(biāo)簽橫向?qū)R,也可以在多個標(biāo)簽的總寬度超過組件寬度時自動換行,是移動端開發(fā)中經(jīng)常使用的布局方式之一。我們可以在很多應(yīng)用場景下看到流式布局的使用,比如商品分類展示,搜索記錄展示等。
組件效果展示
該組件應(yīng)用只包含一個顯示頁面。為了呈現(xiàn)出流式布局的效果,我們在頁面布局中添加了多個標(biāo)簽,如“java”、“kotlin”、“ohos”、“Deveco-studio”、“app”等作為布局中的子組件。具體顯示效果如圖1所示。

圖1 組件效果展示
Sample解析
FlowLayout_ohos在Library中已經(jīng)封裝了組件的主要功能,往FlowLayout_ohos組件中放入標(biāo)簽會自動橫向?qū)R并且在多個標(biāo)簽的總寬度超過組件寬度時自動換行,因此在Sample中我們只需要添加標(biāo)簽內(nèi)容并使用流式布局將標(biāo)簽內(nèi)容進(jìn)行顯示即可。
在標(biāo)簽顯示的過程中,我們可以調(diào)用一些Library暴露的接口來對子組件的顯示特征進(jìn)行設(shè)置,比如組件最多顯示的行數(shù)等。下面將具體講解FlowLayout_ohos組件的使用方法,共分為5個步驟:
步驟1. 導(dǎo)入相關(guān)類
步驟2. 初始化流式布局和數(shù)據(jù)容器
步驟3. 添加標(biāo)簽內(nèi)容到數(shù)據(jù)容器
步驟4. 將標(biāo)簽內(nèi)容添加進(jìn)布局
步驟5. 相關(guān)特征設(shè)置
接下來我們來看一下每一個步驟涉及的詳細(xì)操作。
(1)導(dǎo)入相關(guān)類
在MainAbilitySlice文件中,通過import關(guān)鍵字導(dǎo)入FlowAdapter類和FlowLayout類。FlowLayout類用于組件的顯示,F(xiàn)lowAdapter類用于向組件設(shè)置標(biāo)簽。
- import com.huawei.mylibrary.FlowAdapter;
- import com.huawei.mylibrary.FlowLayout;
(2)初始化流式布局和數(shù)據(jù)容器
實例化FlowLayout類的對象mFlowLayout ,然后創(chuàng)建元素為String類型的列表mContentList作為添加標(biāo)簽的容器,以下我們稱之為數(shù)據(jù)容器。
- private FlowLayout mFlowLayout;
- private List<String> mContentList = new ArrayList<>();
- @Override
- public void onStart(Intent intent) {
- ......
- mFlowLayout = new FlowLayout(this);
- }
(3)添加標(biāo)簽內(nèi)容到數(shù)據(jù)容器
通過add()方法向數(shù)據(jù)容器mContentList中添加想要展示的標(biāo)簽,5個不同的標(biāo)簽通過for循環(huán)循環(huán)四次逐個放入容器,共形成20個需要在頁面展示的標(biāo)簽。
- for (int i = 0; i < 4; i++) {
- mContentList.add("java");
- mContentList.add("kotlin");
- mContentList.add("ohos");
- mContentList.add("Deveco-studio");
- mContentList.add("app");
- }
(4)將標(biāo)簽內(nèi)容添加進(jìn)布局
實例化FlowAdapter類的對象adapter,并將數(shù)據(jù)容器mContentList作為FlowAdapter類構(gòu)造方法的參數(shù)。后通過setAdapter()方法將標(biāo)簽內(nèi)容添加到組件中。
- // 設(shè)置 Adapter
- FlowAdapter adapter = new FlowAdapter(this, mContentList);
- // 將標(biāo)簽內(nèi)容添加到組件中
- mFlowLayout.setAdapter(adapter);
(5) 將標(biāo)簽內(nèi)容添加到組件中
- mFlowLayout.setAdapter(adapter);
(6)相關(guān)特征設(shè)置
mFlowLayout可以調(diào)用一些Library暴露的接口實現(xiàn)流式布局的特征設(shè)置,這里我們設(shè)置了組件布局內(nèi)最多顯示的行數(shù)。
- // 設(shè)置最多顯示的行數(shù)
- mFlowLayout.setMaxLines(9);
Library解析
流式布局應(yīng)用非常廣泛,但鴻蒙官方卻并未給出相應(yīng)的布局方式,因此流式布局只能自定義實現(xiàn),本節(jié)主要介紹自定義布局的步驟。
想要實現(xiàn)自定義布局,需要完成以下三個步驟:1)流式布局的FlowLayout類需要繼承ComponentContainer類,并添加構(gòu)造方法。2) 實現(xiàn)ComponentContainer.EstimateSizeListener接口,重寫onEstimateSize()方法,用于確定FlowLayout_ohos組件寬高。3)實現(xiàn)Component.LayoutRefreshedListener接口,重寫onRefreshed()方法用來排列子組件并確定子組件位置。1)步驟的操作較為簡單,此處不再贅述,本節(jié)主要描述2)、3)步驟的原理。
(1)重寫onEstimateSize方法
根據(jù)onEstimateSize(int widthMeasureSpec, int heightMeasureSpec)方法傳入的參數(shù),選擇測量組件寬度和高度的方式,并得到組件寬度和高度的具體值,通過setEstimatedSize()方法設(shè)置給組件。下面介紹具體的步驟:
1、得到組件的測量模式和父組件的寬度、高度
- 調(diào)用EstimateSpec.getMode(widthMeasureSpec)方法,傳入widthMeasureSpec參數(shù),得到組件寬度的測量模式。
- 調(diào)用EstimateSpec.getMode(heightMeasureSpec)方法,傳入heightMeasureSpec參數(shù),得到組件高度的測量模式。
- 調(diào)用EstimateSpec.getSize(widthMeasureSpec)方法,傳入widthMeasureSpec參數(shù),得到父組件的寬度。
- 調(diào)用EstimateSpec.getSize(heightMeasureSpec)方法,傳入heightMeasureSpec參數(shù),得到父組件的高度。
- int widthSize = EstimateSpec.getSize(widthMeasureSpec);//父組件的寬度
- int widthMode = EstimateSpec.getMode(widthMeasureSpec); //組件寬度的測量模式
- int heightSize =EstimateSpec.getSize(heightMeasureSpec);//父組件的高度
- int heightMode = EstimateSpec.getMode(heightMeasureSpec);//組件高度的測量模式
2、確定組件寬度和高度的具體值
widthMode /heightMode 可能存在兩種不同的模式,在不同的模式下組件的寬度和高度的值也會有不同的計算方式。
- PRECISE 模式:在這種模式下,組件設(shè)置其寬、高為MATCH_PARENT。
- NOT_EXCEED 模式:在這種模式下,組件設(shè)置其寬、高為MATCH_CONTENT 。
在PRECISE 模式下,組件的寬度和高度與父組件一致,這種計算方式較為簡單。但是在NOT_EXCEED 模式下,組件的寬度和高度是根據(jù)子組件的寬度和高度來決定的,此時需要遍歷各子組件,對每個子組件進(jìn)行測量,并在寬度和高度上求和,才能計算出最終的組件的寬高。子組件的遍歷過程是通過helper()方法來實現(xiàn)的。
- int[] a = helper(widthSize);
- int measuredHeight = 0; //組件的高度值
- if (heightMode == EstimateSpec.PRECISE) { // PRECISE 模式
- measuredHeight = heightSize;
- }
- else if (heightMode == EstimateSpec.NOT_EXCEED) { // NOT_EXCEED 模式
- measuredHeight = a[0]; //遍歷各子組件后得到的組件高度
- }
- int measuredWidth = 0; //組件的寬度值
- if (widthMode == EstimateSpec.PRECISE) { // PRECISE 模式
- measuredWidth = widthSize;
- }else if (widthMode == EstimateSpec.NOT_EXCEED) { // NOT_EXCEED 模式
- measuredWidth = a[1]; //遍歷各子組件后得到的組件寬度
- }
3、將測量得到的高度和寬度值設(shè)置給組件。
通過setEstimatedSize()方法,將步驟2中得到的組件寬度和高度值設(shè)置給組件。
- setEstimatedSize(measuredWidth, measuredHeight);
(2)重寫onRefreshed方法
onRefreshed()方法主要用來確定子組件的擺放位置。該位置在helper()方法中已經(jīng)得到,并保存在mChildrenPositionList中。mChildrenPositionList是一個元素類型為Rect的列表,每一個元素代表一個子組件的位置信息。因此,在確定子組件的擺放位置時,只需要調(diào)用mChildrenPositionList中的元素信息,并將其賦給各子組件即可。
- @Override
- public void onRefreshed(Component component) {
- int n = Math.min(getChildCount(), mChildrenPositionList.size());
- for (int i = 0; i < n; i++) {
- Component child = getComponentAt(i); //獲取各組件
- Rect rect = mChildrenPositionList.get(i); //組件信息
- child.setLeft(rect.left); //組件位置設(shè)置
- child.setRight(rect.right);
- child.setBottom(rect.bottom);
- child.setTop( rect.top);
- }
- mVisibleItemCount = n;
- }
(3)helper()方法
helper()方法是一個“工具”方法,在onEstimateSize()和onRefreshed()的重寫中都提供了“幫助”。helper()方法對外提供的功能,主要為以下三個方面:
1)在組件的布局方式為MATCH_CONTENT情況下,遍歷各子組件,對每個子組件的寬度和高度進(jìn)行測量,并在寬度和高度上求和,計算出最終組件的寬度和高度。
2)判斷換行條件,實現(xiàn)流動布局的效果。
3)保存子組件的位置信息。
下面我們將圍繞上述內(nèi)容展開講解。
1)計算組件寬度和高度
- 組件的寬度
組件的寬度取決于子組件的排布是否存在換行的情況,若是子組件排布存在換行的情況,組件寬度等于父組件的寬度。若是子組件排布不存在換行的情況,組件寬度等于當(dāng)前行的寬度。代碼中isOneRow表示是否存在換行的情況,width 表示當(dāng)前行的寬度,widthSize表示父組件的寬度,各變量的示意如圖2所示。

圖2 代碼變量示意圖
- int childWidth = child.getMarginLeft() + child.getEstimatedWidth() + child.getMarginRight(); //每個子組件的寬度
- width += childWidth; //每行的寬度
- ...
- res[1] = isOneRow? width + getPaddingRight() : widthSize; //組件的寬度
- 組件的高度
組件的高度是每一行子組件高度的總和,而每一行的高度則是取該行中所有子組件中最高的值。
- int childHeight =child.getMarginTop() + child.getEstimatedHeight() + child.getMarginBottom();
- maxHeight = Math.max(maxHeight, childHeight); //取最大值
- ...
- res[0] = height + maxHeight + getPaddingBottom(); //組件的高度
2)判斷換行條件
從效果圖中可以看到,F(xiàn)lowLayout_ohos組件的布局是一行行的,如果當(dāng)前行的剩余寬度已經(jīng)放不了下一個子組件,那就把這個子組件移到下一行顯示。
所以我們需要計算當(dāng)前行已經(jīng)占據(jù)的寬度加上下一個子組件的寬度是否超過組件的最大寬度,以判斷下一個子組件是否需要換行顯示。
- if (width + childWidth + getPaddingRight() > widthSize) { //需要換行
- height += maxHeight; // 增加一行的高度
- width = getPaddingLeft(); // 獲取新一行已經(jīng)占據(jù)的寬度
- maxHeight = childHeight;
- isOneRow = false;
- currLine++; //行數(shù)+1
- if (currLine > mMaxLines) { //超過設(shè)定的最大顯示行數(shù),退出
- break;
- }
- }
3)保存子組件的位置信息
根據(jù)當(dāng)前已有的寬高,確定子組件的位置,并將位置信息作為參數(shù)傳入Rect 類實例化對象的過程中,用Rect 類對象標(biāo)識子組件的位置信息,并將這些信息逐個放入List中,在onRefreshed()方法中被使用到。
- Rect rect = new Rect(width +child.getMarginLeft(),
- height + child.getMarginTop(),
- width + childWidth - child.getMarginRight(),
- height + childHeight - child.getMarginBottom());
- mChildrenPositionList.add(rect);
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)