自定義 FlowLayout

自定義 FlowLayout

在這里什么是一個 FlowLayout 盖奈,就是我們在很多軟件的搜索頁面看到的一些搜索標(biāo)簽氧敢,先上一張圖吧笋庄。


FlowLayout使用例子

就是根據(jù)子 View 的長度自動換行,當(dāng)然 Android 9.0 也出了一個類似的控件看幼,但是考慮到之前的版本,我這里自己擼一個幌陕,也順便熟悉熟悉整個流程诵姜。
這里想寫一個 FlowLayout 也就是重寫一個 ViewGroup,我自己覺得寫一個
ViewGroup 要比寫一個 View 要麻煩搏熄,不僅要考慮適配 WRAP_CONTENT棚唆,同時還要處理自身的長度和子 View 的擺放暇赤,但是我們可以對 Android 的一些 ViewGroup 的實(shí)現(xiàn)有所了解。當(dāng)然有時候可以曲線救國宵凌,有時候我覺得有些太底層的沒必要去自己實(shí)現(xiàn)鞋囊,而且自己寫的肯定沒有 Android 原生的考慮的周全,我就可以直接繼承已有的 ViewGroup 比如 LinearLayout瞎惫、RelativieLayout 之類的溜腐,曲線救國~~

自定義 FlowLayout 的基本步驟

  1. 重寫OnMeasure
  2. 重寫OnLayout
    這里分別是 父ViewGroup 對我們 measure 和 layout 的方法的回調(diào)方法,我們在這里獲取到我們測量的大小和位置微饥,接著可以繼續(xù)去測量我們的子 Item 的大小和位置逗扒。

重寫OnMeasure

空的onMeasure如下所示

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

首先第一步要先獲取測量模式和大小

int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

這個上面的 widthMeasureSpec 和 heightMeasureSpec 變量是OnMeasure的回調(diào)參數(shù),在這里我們獲取到了寬度和高度的大小和分別的測量模式欠橘,測量模式分成三種矩肩,但是我們在這里只討論常見的兩種 EXACTLY 和 AT_MOST, 前者對應(yīng)著指定寬度和 MATCH_PARENT,后者對應(yīng)著 WRAP_CONTENT肃续。我們在這個方法里最想確定自己在 MATCH_PARENT 和 WRAP_CONTENT 下長寬如何黍檩,這里還要注意一點(diǎn)就是如果你重寫一個 ViewGroup,如果沒有針對 WRAP_CONTENT 重寫 始锚,那么你的長寬和 MATCH_PARENT 模式下沒有區(qū)別刽酱。

我們在 onMeasure 主要是測量子 View 所有長度加起來的和,當(dāng)然不是盲目的加瞧捌,廢話不多說先上代碼棵里。

        int wrapWidth = 0;
        int wrapHeight = 0;

        int lineWidth = 0;
        int lineHeight = 0;

        int cCount = getChildCount();

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

            // 得到第 i 個 View
            View child = getChildAt(i);

            // 這里要十分注意,如果這里不先 measureChild 是無法得到 child 的寬度和高度
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            MarginLayoutParams lp = (MarginLayoutParams) child
                    .getLayoutParams();


            // 獲得子 View 所占的寬度和高度的時候要加上他自身的 margin
            int childWidth = child.getMeasuredWidth() + lp.leftMargin
                    + lp.rightMargin;
            int childHeight = child.getMeasuredHeight() + lp.topMargin
                    + lp.bottomMargin;

            // 判斷是否需要換行 要減去左右的 padding
            if (lineWidth + childWidth > sizeWidth - getPaddingLeft() - getPaddingRight()) {

                // 需要換行的時候
                wrapWidth = Math.max(wrapWidth, lineWidth);
                lineWidth = childWidth;
                wrapHeight += lineHeight;
                lineHeight = childHeight;

            } else {

                // 不需要換行的時候
                lineWidth += childWidth;
                lineHeight = Math.max(lineHeight, childHeight);

            }

            // 特殊考慮姐呐,要考慮到獲得 wrapWidth 和 wrapHeight
            if (i == cCount - 1) {
                wrapWidth = Math.max(lineWidth, wrapWidth);
                wrapHeight += lineHeight;
            }

        }

這上面有兩個變量要關(guān)注下殿怜,wrapWidth 和 wrapHeight 記錄著 WRAP_CONTENT 時的長和寬,原因也知道了曙砂,WRAP_CONTENT 的效果需要我們自己實(shí)現(xiàn)退个,要不然就和 MATCH_PARENT 一模一樣辐董。另一個需要注意的點(diǎn)就是上面調(diào)用了很重要的函數(shù)就是 measureChild 這是 ViewGroup 自帶的函數(shù)斗躏,作用就是測量子 View 的長和寬层释,這個很好理解,我作為一個父 ViewGroup笑陈,在WRAP_CONTENT 的模式下际度,連自己的子 View 的長寬都不清楚,我怎么確定自己的長寬大小新锈,最后還需要注意最后一個要另外記錄 wrapWidth 和 wrapHeight 甲脏,其它的大家自己可以通過閱讀代碼理解。

最后我們當(dāng)然要調(diào)用設(shè)置自己寬高的函數(shù) setMeasuredDimension ,代碼如下块请,包括不同的測量模式下娜氏,大小不一樣。

setMeasuredDimension(modeWidth == MeasureSpec.EXACTLY ? sizeWidth : wrapWidth + getPaddingLeft() + getPaddingRight(), modeHeight == MeasureSpec.EXACTLY ? sizeHeight : wrapHeight + getPaddingTop() + getPaddingBottom());

重寫 OnLayout

我們在這個函數(shù)里面通過調(diào)用子 View 的layout來確定子 View 的位置墩新。這個方法的思路和上面的有相似的也有不同的贸弥,我們需要通過兩個變量來儲存每行有哪些 View 和每一行的高度,同時我們還得注意考慮一種情況海渊,如果這個子 View 的狀態(tài)是 GONE, 我們就需要跳過它绵疲,同時在計算間距的時候還別忘了加上 父 ViewGroup 的padding 和自身的 margin ,按照這個思路我們需要遍歷兩遍所有的子 View臣疑,其它的跟上面的也很類似盔憨,關(guān)鍵就是控制哪些 View 是一行的,以及什么時候換行讯沈,這都又自己想要達(dá)到的效果決定的郁岩。這里因?yàn)楦厦娴念愃疲疫@里就不一一講述了缺狠,下面直接給出所有的代碼问慎。

所有代碼

public class FlowLayout extends ViewGroup {

    private static final String TAG = "FlowLayout";

    public FlowLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

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

    public FlowLayout(Context context) {
        this(context, null);
    }


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

        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

        Log.d(TAG, "" + sizeWidth);

        // 如果是warp_content情況下,記錄寬和高
        int width = 0;
        int height = 0;

        // 記錄每一行的寬度與高度
        int lineWidth = 0;
        int lineHeight = 0;

        // 得到內(nèi)部元素的個數(shù)
        int count = getChildCount();

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

            // 通過索引拿到每一個子view
            View child = getChildAt(i);

            // 測量子View的寬和高,系統(tǒng)提供的measureChild
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            // 得到LayoutParams
            MarginLayoutParams lp = (MarginLayoutParams) child
                    .getLayoutParams();

            // 子View占據(jù)的寬度
            int childWidth = child.getMeasuredWidth() + lp.leftMargin
                    + lp.rightMargin;
            // 子View占據(jù)的高度
            int childHeight = child.getMeasuredHeight() + lp.topMargin
                    + lp.bottomMargin;

            // 換行 判斷 當(dāng)前的寬度大于 開辟新行
            if (lineWidth + childWidth > sizeWidth - getPaddingLeft() - getPaddingRight()) {

                // 對比得到最大的寬度
                width = Math.max(width, lineWidth);
                // 重置lineWidth
                lineWidth = childWidth;
                // 記錄行高
                height += lineHeight;
                lineHeight = childHeight;

            } else
            // 未換行
            {
                // 疊加行寬
                lineWidth += childWidth;
                // 得到當(dāng)前行最大的高度
                lineHeight = Math.max(lineHeight, childHeight);
            }

            // 特殊情況,最后一個控件
            if (i == count - 1) {
                width = Math.max(lineWidth, width);
                height += lineHeight;
            }

        }

        setMeasuredDimension(
                modeWidth == MeasureSpec.EXACTLY ? sizeWidth : width + getPaddingLeft() + getPaddingRight(),
                modeHeight == MeasureSpec.EXACTLY ? sizeHeight : height + getPaddingTop() + getPaddingBottom()//
        );


    }

    /**
     * 存儲所有的View
     */
    private List<List<View>> allViews = new ArrayList<List<View>>();

    /**
     * 每一行的高度
     */
    private List<Integer> mLineHeight = new ArrayList<Integer>();


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {


        allViews.clear();
        mLineHeight.clear();

        // 當(dāng)前ViewGroup的寬度
        int width = getWidth();

        int lineWidth = 0;
        int lineHeight = 0;

        // 存放每一行的子view
        List<View> lineViews = new ArrayList<>();

        int count = getChildCount();

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

            View child = getChildAt(i);
            MarginLayoutParams lp = (MarginLayoutParams) child
                    .getLayoutParams();

            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();

            // 如果需要換行
            if (childWidth + lineWidth + lp.leftMargin + lp.rightMargin > width - getPaddingLeft() - getPaddingRight()) {
                // 記錄LineHeight
                mLineHeight.add(lineHeight);
                // 記錄當(dāng)前行的Views
                allViews.add(lineViews);

                // 重置我們的行寬和行高
                lineWidth = 0;
                lineHeight = childHeight + lp.topMargin + lp.bottomMargin;
                // 重置我們的View集合
                lineViews = new ArrayList<View>();
            }
            lineWidth += childWidth + lp.leftMargin + lp.rightMargin;
            lineHeight = Math.max(lineHeight, childHeight + lp.topMargin
                    + lp.bottomMargin);
            lineViews.add(child);

        }// for end

        // 處理最后一行
        mLineHeight.add(lineHeight);
        allViews.add(lineViews);

        // 設(shè)置子View的位置

        int left = getPaddingLeft();
        int top = getPaddingTop();

        // 行數(shù)
        int lineNum = allViews.size();

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

            // 當(dāng)前行的所有的View
            lineViews = allViews.get(i);
            lineHeight = mLineHeight.get(i);

            for (int j = 0; j < lineViews.size(); j++) {
                View child = lineViews.get(j);
                // 判斷child的狀態(tài)
                if (child.getVisibility() == View.GONE) {
                    continue;
                }

                MarginLayoutParams lp = (MarginLayoutParams) child
                        .getLayoutParams();

                int lc = left + lp.leftMargin;
                int tc = top + lp.topMargin;
                int rc = lc + child.getMeasuredWidth();
                int bc = tc + child.getMeasuredHeight();

                // 為子View進(jìn)行布局
                child.layout(lc, tc, rc, bc);

                left += child.getMeasuredWidth() + lp.leftMargin
                        + lp.rightMargin;
            }
            left = getPaddingLeft();
            top += lineHeight;
        }

    }

    /**
     * 與當(dāng)前ViewGroup對應(yīng)的LayoutParams
     */
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }


}

效果圖如下

在布局里面如何使用
效果圖

最后的總結(jié)

自定義一個 ViewGroup 最少需要重寫這兩個方法挤茄,當(dāng)然也可以重寫他自己的 onDraw方法如叼,我們這里最需要注意的就是 onMeasure 里面的測量模式,而且要記住穷劈,要自己來控制 WRAP_CONTENT 下自身的大小笼恰。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市歇终,隨后出現(xiàn)的幾起案子挖腰,更是在濱河造成了極大的恐慌,老刑警劉巖练湿,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異审轮,居然都是意外死亡肥哎,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門疾渣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來篡诽,“玉大人,你說我怎么就攤上這事榴捡¤九” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長达椰。 經(jīng)常有香客問我翰蠢,道長,這世上最難降的妖魔是什么啰劲? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任梁沧,我火速辦了婚禮,結(jié)果婚禮上蝇裤,老公的妹妹穿的比我還像新娘廷支。我一直安慰自己,他們只是感情好栓辜,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布恋拍。 她就那樣靜靜地躺著,像睡著了一般藕甩。 火紅的嫁衣襯著肌膚如雪施敢。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天辛萍,我揣著相機(jī)與錄音悯姊,去河邊找鬼。 笑死贩毕,一個胖子當(dāng)著我的面吹牛悯许,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播辉阶,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼先壕,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了谆甜?” 一聲冷哼從身側(cè)響起垃僚,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎规辱,沒想到半個月后谆棺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡罕袋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年改淑,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片浴讯。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡朵夏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出榆纽,到底是詐尸還是另有隱情仰猖,我是刑警寧澤捏肢,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站饥侵,受9級特大地震影響鸵赫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜爆捞,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一奉瘤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧煮甥,春花似錦盗温、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至双霍,卻和暖如春砚偶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背洒闸。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工染坯, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人丘逸。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓单鹿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親深纲。 傳聞我的和親對象是個殘疾皇子仲锄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345

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