自定義控件篇 — 標簽流式布局TagFlowLayout

本篇主要內(nèi)容:從0到1寫一個流式布局TagFlowLayout

1 通過本篇可以了解什么

  • 繼承至ViewGroup的組件如何編寫onMeasureonLayout方法闪湾;
  • Viewmargin值是如何在onMeasureonLayout中使用的妻导;
  • 流式布局的基本原理。

2 繼承ViewGroup的組件到底意味著什么

首先,ViewGroup是一個組件容器纳猫,它自身沒有進行任何測量和布局恕出,但是它提供了一系列測量子View的方法蔼卡,方便我們調(diào)用屯伞。

再者腿箩,我們需要在繼承ViewGroup組件中的測量方法中進行子View控件的測量。

onMeasure中確定各個View的大小以及onLayout中需要的擺放參數(shù)劣摇,onLayout中進行擺放珠移。

paddingmargin值需要在測量和擺放時加入計算中,onMeasure中大部分考慮的是margin末融,onLayout中考慮paddingmargin值钧惧。

3 實現(xiàn)過程

廢話不多說,先看效果圖:

圖1

3.1 創(chuàng)建控件

這一步相對簡單勾习,就不做過多說明浓瞪,代碼如下:

public class TagFlowLayout extends ViewGroup {
    public TagFlowLayout(Context context) {
        this(context, null);
    }

    public TagFlowLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TagFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {

    }
}

3.2 onMeasure方法實現(xiàn)過程

先用圖說明一下measure邏輯流程,其實也很簡單巧婶。

圖2

由上圖可知乾颁,我們有兩個目標:

  • 其一,遍歷并計算每個子View艺栈;
  • 其二英岭,找出超出屏幕位置的View,并進行換行湿右。
3.2.1 遍歷并計算每個View
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        LogUtils.d("onMeasure: " + onMeasureCount++);

        int childCount = getChildCount();

        for (int i = 0; i < childCount; i++) {

        }

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    }

接下來開始對每個子View進行測量诅妹,ViewGroup給我們提供了測量子View的方法measureChildWithMargins()于是就有了如下代碼:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    LogUtils.d("onMeasure: " + onMeasureCount++);

    int childCount = getChildCount();

    for (int i = 0; i < childCount; i++) {
        View child = getChildAt(i);

        measureChildWithMargins();
    }

    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

}

但是發(fā)現(xiàn)measureChildWithMargins有五個參數(shù),如下:

protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed)
  • 第一個參數(shù)child诅需,就是我們需要測量的View漾唉;
  • 第二個參數(shù)parentWidthMeasureSpec荧库,這是我們傳遞給子view對于寬度的建議堰塌;
  • 第三個參數(shù)是指父控件在水平方向上已使用的寬度,有可能是其他子 view使用的空間分衫;
  • 第四個和第五個參數(shù)與第二個场刑、三個參數(shù)雷同,只是是豎直方向蚪战。

知道這幾個參數(shù)的意義后牵现,于是我們就可以在計算子view時傳遞相應的參數(shù)值。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    LogUtils.d("onMeasure: " + onMeasureCount++);

    int childCount = getChildCount();

    for (int i = 0; i < childCount; i++) {
        View childView = getChildAt(i);
        measureChildWithMargins(childView, widthMeasureSpec, 0, heightMeasureSpec, 0);
    }
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

}

到這里我們應該會有個疑問:widthUsed和heightUsed這兩個參數(shù)為什么是0邀桑?

原因如下:

  • 因為如果把已用的空間傳入這個參數(shù)瞎疼,那么有可能會導致影響子view的測量結(jié)果;
    至于為什么會影響子 view的測量結(jié)果壁畸,我們看看源碼:
protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

    // widthUsed會作為getChildMeasureSpec方法中padding參數(shù)值的一部分進入到view的MeasureSpec參數(shù)的計算中去贼急,
    // 所以父view的建議有可能會影響子view最終大小
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
  • 而我們所想要的效果是子View按照自己的測量方式測量出自己大小茅茂,空間不足,另起一行太抓,不能約束子View空闲。

好了,上面查看了部分源碼進行分析走敌,我們繼續(xù)回歸主題碴倾。

3.2.2 找出超出屏幕的View,并且進行換行掉丽。

基本思路如下:

  • 定義相應數(shù)據(jù)結(jié)構(gòu)跌榔;
  • 同一行寬度累加;
  • 與控件TagFlowLayout本身寬度進行比較查看是否超出當前行捶障。

由圖2可知矫户,其實就是計算出每一行都有哪些View,最容易想到的數(shù)據(jù)結(jié)構(gòu)就是:List<List<View>>残邀。外層List表示有多少行皆辽,里層List表示每一行多少個子View

另外芥挣,需要一個臨時變量記錄住當前子View已經(jīng)使用的空間驱闷,定義為currentLineTotalWidth

還需知道控件本身的寬度widthSize空免,為了和當前所有View已占用的空間進行寬度對比空另,代碼如下:

// 所有的view
private List<List<View>> mAllViews;
// 每一行的View
private List<View> mRowViewList;

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    LogUtils.d("onMeasure: " + onMeasureCount++);
    mAllViews.clear();
    mRowViewList.clear();

    // TagFlowLayout的寬度
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    // 當前行遍歷過程中子View的寬度累加
    // 也可以當成當前行已使用的空間
    int currentLineTotalWidth = 0;

    int childCount = getChildCount();

    for (int i = 0; i < childCount; i++) {
        View childView = getChildAt(i);

        measureChildWithMargins(childView, widthMeasureSpec, 0, heightMeasureSpec, 0);
        // 獲取當前子View的寬度
        int childWidth = childView.getMeasuredWidth();
        // 一行已經(jīng)超出,另起一行
        if (currentLineTotalWidth + childWidth > widthSize) {
            // 重置當前currentLineTotalWidth 
            currentLineTotalWidth = 0;
            // 添加當前行的所有子View
            mAllViews.add(mRowViewList);
            // 另起一行蹋砚,需要新開辟一個集合
            mRowViewList = new ArrayList<>();
            mRowViewList.add(childView);
            // 最后一個控件單獨一行
            if (i == (childCount - 1)) {
                mAllViews.add(mRowViewList);
            }        
        } else {
            // 沒換行扼菠,繼續(xù)累加
            currentLineTotalWidth += childView.getMeasuredWidth();
            mRowViewList.add(childView);
            // 最后一個view并且沒有超出寬度
            if (i == (childCount - 1)) {
                mAllViews.add(mRowViewList);
            }
        }
    }

    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

}

接下來,我們的目標是算出控件的寬高坝咐,并且設(shè)置進setMeasuredDimension方法中循榆。

而最終的寬度就是所有行中最大的那個寬,所以每次新增加一個控件就可以比較兩個值中的最大值墨坚。而高度是累加秧饮,在每次換行時都加上上一行的高度。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    // 省略無關(guān)代碼 ...

    // 測量最終的寬度
    int selfMeasureWidth = 0;
    // 測量最終的高度
    int selfMeasureHeight = 0;
    // 當前行的最大高度
    int currentLineMaxHeight = 0;
    
    if (currentLineTotalWidth + childWidth > widthSize) {
        selfMeasureWidth = Math.max(selfMeasureWidth, currentLineTotalWidth);
        selfMeasureHeight += currentLineMaxHeight;
        currentLineMaxHeight = childHeight + marginLayout.topMargin + marginLayout.bottomMargin;
        if (i == (childCount - 1)) {
            selfMeasureHeight += currentLineMaxHeight;
        }
    } else {
        currentLineMaxHeight = Math.max(currentLineMaxHeight, (childHeight + marginLayout.topMargin + marginLayout.bottomMargin));
        currentLineTotalWidth += childView.getMeasuredWidth();
        selfMeasureWidth = Math.max(selfMeasureWidth, currentLineTotalWidth);
        if (i == (childCount - 1)) {
            selfMeasureHeight += currentLineMaxHeight;
        }
    }

    setMeasuredDimension(selfMeasureWidth, selfMeasureHeight);
}

到此為止泽篮,控件的onMeasure方法就已基本完成盗尸。

3.3 onLayout方法實現(xiàn)過程

接下來就是擺放位置,分別從總的控件集合中取出相應的控件帽撑,然后進行擺放泼各,坐標位置就是控件的上下左右四個點。

在橫向上亏拉,如果一行有多個控件扣蜻,則進行寬度的累加來確定其他子View的位置寸癌。

在縱向上,主要就是確定每一行的起始高度位置弱贼。

而針對這個起始高度位置蒸苇,我們在測量onMeasure過程中,會保存一個高度集合mHeightList吮旅,記錄每一行最大高度溪烤,將在onLayout中使用。

主要代碼如下:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        LogUtils.d("onLayout: " + onLayoutCount++);

        int alreadyUsedWidth;
        // 擺放的開始高度位置
        int beginHeight = 0;
        int left, top, right, bottom;
        // mHeightList存放了每一行的最大高度
        if (mHeightList.size() != mAllViews.size()) {
            LogUtils.e("mHeightList's size is not equal to mAllViews's size");
            return;
        }

        for (int i = 0; i < mAllViews.size(); i++) {
            List<View> rowList = mAllViews.get(i);
            if (rowList == null) continue;
            // 每一行開始的擺放的位置alreadyUsedWidth庇勃,累加后檬嘀,成為每一行已經(jīng)使用的空間
            alreadyUsedWidth = getPaddingLeft();
            beginHeight += mHeightList.get(i);

            if (i == 0) {
                beginHeight += getPaddingTop();
            }

            for (int j = 0; j < rowList.size(); j++) {
                View childView = rowList.get(j);
                MarginLayoutParams params = (MarginLayoutParams) childView.getLayoutParams();

                left = alreadyUsedWidth + params.leftMargin;
                right = left + childView.getMeasuredWidth();
                top = beginHeight + params.topMargin;
                bottom = top + childView.getMeasuredHeight();

                childView.layout(left, top, right, bottom);

                alreadyUsedWidth = right;
            }
        }
    }

3.4 關(guān)于實現(xiàn)子 View MarginLayout的布局

默認情況下,ViewGroup的LayoutParams為ViewGroup.LayoutParams责嚷,如需要使用margin_left這類屬性操作鸳兽,需要重寫generateLayoutParams方法。

@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new MarginLayoutParams(getContext(), attrs);
}

綜上罕拂,就是流式布局的基本原理揍异。如有錯誤,歡迎指出討論爆班。

4 寫在最后

在這個微涼的早餐終于完成了這篇文章的編寫衷掷,前幾天和一朋友喝茶聊天,聊到了文章創(chuàng)作其實也是一種服務柿菩,服務于其他有需求的人戚嗅。而服務意識又是打開另一扇大門的一種重要品質(zhì),未來希望能創(chuàng)作更多更好的服務枢舶。

如果喜歡懦胞,歡迎點贊與分享,創(chuàng)作不易凉泄,你的支持將是最好的回饋躏尉。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市旧困,隨后出現(xiàn)的幾起案子醇份,更是在濱河造成了極大的恐慌,老刑警劉巖吼具,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異矩距,居然都是意外死亡拗盒,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門锥债,熙熙樓的掌柜王于貴愁眉苦臉地迎上來陡蝇,“玉大人痊臭,你說我怎么就攤上這事〉欠颍” “怎么了广匙?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長恼策。 經(jīng)常有香客問我鸦致,道長,這世上最難降的妖魔是什么涣楷? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任分唾,我火速辦了婚禮,結(jié)果婚禮上狮斗,老公的妹妹穿的比我還像新娘绽乔。我一直安慰自己,他們只是感情好碳褒,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布折砸。 她就那樣靜靜地躺著,像睡著了一般沙峻。 火紅的嫁衣襯著肌膚如雪鞍爱。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天专酗,我揣著相機與錄音睹逃,去河邊找鬼。 笑死祷肯,一個胖子當著我的面吹牛沉填,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播佑笋,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼翼闹,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蒋纬?” 一聲冷哼從身側(cè)響起猎荠,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蜀备,沒想到半個月后关摇,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡碾阁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年输虱,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片脂凶。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡宪睹,死狀恐怖愁茁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情亭病,我是刑警寧澤鹅很,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站罪帖,受9級特大地震影響促煮,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜胸蛛,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一污茵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧葬项,春花似錦泞当、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至嚷量,卻和暖如春陋桂,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蝶溶。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工嗜历, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人抖所。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓梨州,卻偏偏與公主長得像,于是被迫代替她去往敵國和親田轧。 傳聞我的和親對象是個殘疾皇子暴匠,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容