一個(gè)FlowLayout帶你學(xué)會(huì)自定義ViewGroup

時(shí)間過(guò)得真快救湖,又到了寫(xiě)博客的時(shí)候了(/▽╲)逆瑞。這次按照計(jì)劃記錄一個(gè)簡(jiǎn)單的自定義ViewGroup:流布局FlowLayout的實(shí)現(xiàn)過(guò)程糠雨,將自定義控件知識(shí)儲(chǔ)備-View的繪制流程自定義控件知識(shí)儲(chǔ)備-LayoutParams的那些事里的知識(shí)點(diǎn)結(jié)合起來(lái)蛙奖,付諸實(shí)踐亮垫。

1. 前言

早在學(xué)習(xí)Java的Swing基礎(chǔ)知識(shí)的時(shí)候,就見(jiàn)到過(guò)里面的流布局FlowLayout躺涝,基本的效果就是讓加入此容器的控件自左往右依次排列厨钻,如果當(dāng)前行的寬度不足以容納下一個(gè)控件,就會(huì)將此控件放置到下一行坚嗜。其實(shí)這也跟css里向左浮動(dòng)的效果很相似夯膀。

在Android的世界里,系統(tǒng)是沒(méi)有提供類(lèi)似FlowLayout布局的容器的苍蔬。當(dāng)然了诱建,現(xiàn)在官方給我們提供了更強(qiáng)大也更復(fù)雜的FlexLayout了。不過(guò)嘛碟绑,本篇博客是總結(jié)一個(gè)自定義ViewGroup的實(shí)現(xiàn)流程俺猿,所以需要找一個(gè)難易適中的實(shí)例來(lái)進(jìn)行分析,也就是FlowLayout了格仲。(是的押袍,我就是挑軟柿子捏︿( ̄︶ ̄)︿)。

2. 效果

閑話(huà)少說(shuō)凯肋,還是先來(lái)看看蘑菇君寫(xiě)的FlowLayout的功能:

  • 支持最基本的從左至右的排序谊惭,空間不足則換行
  • 支持設(shè)置子控件間的水平和豎直的間隔(也可以通過(guò)給每個(gè)child設(shè)置margin來(lái)實(shí)現(xiàn),不過(guò)沒(méi)有統(tǒng)一設(shè)置來(lái)的方便)
  • 支持繪制行之間的分割線(xiàn)
  • 支持FlowLayout本身的Gravity和child views的Gravity
  • 處理好FlowLayout的padding和child views的margin

這些都是FlowLayout基本的功能侮东,效果如下圖所示:

FlowLayout效果展示

是不是感覺(jué)還行圈盔?至少一般的情況下是能滿(mǎn)足大部分人的需求滴。o( ̄▽?zhuān)?d

3. 分析

列舉一下自定義ViewGroup的流程:

  1. 自定義屬性:如果ViewGroup需要用到自定義屬性悄雅,則需要聲明驱敲、設(shè)置、解析并獲取自定義屬性值煤伟。
  2. 測(cè)量:在onMeasure方法里處理AT_MOSTEXACTLY兩種測(cè)量模式下ViewGroup的寬高和children的寬高癌佩。(UNSPECIFIED模式可以暫不考慮)
  3. 布局:在onLayout方法里確定children的位置木缝。
  4. 繪制:如果ViewGroup里需要繪制,則重寫(xiě)onDraw方法围辙,按邏輯繪制我碟。比如FlowLayout可以在每一行之間繪制一條分隔線(xiàn)。
  5. 處理LayoutParams:如果要為children定義布局屬性姚建,如layout_gravity矫俺,則需要自定義LayoutParams,并且重寫(xiě)ViewGroup相關(guān)的方法掸冤。
  6. 處理滑動(dòng)事件:在本FlowLayout里暫時(shí)用不上...( ╯▽╰)

上面的步驟可能有所遺漏厘托,不過(guò)也差不多啦。下面蘑菇君要根據(jù)上述的流程來(lái)一步一步的分析FlowLayout的源碼稿湿,源碼可能有點(diǎn)長(zhǎng)铅匹,有些細(xì)節(jié)上的邏輯看不懂也莫方,只要了解流程對(duì)應(yīng)的實(shí)現(xiàn)方式和注意事項(xiàng)就好饺藤,有興趣的話(huà)可以稍后自己下載源碼分析具體的邏輯實(shí)現(xiàn)包斑。

好滴,那就讓我們來(lái)一步一步的看涕俗,這個(gè)FlowLayout是如何在我手里...被玩殘的...

3.1 自定義屬性

3.1.1 聲明屬性

首先罗丰,自定義屬性的第一步當(dāng)然是聲明屬性,而最常使用的方式當(dāng)然是在xml資源文件里(一般來(lái)說(shuō)就是attrs.xml文件)聲明需要使用的屬性:

   <declare-styleable name="FlowLayout">
        <attr name="android:gravity"/>
        <attr name="horizonSpacing" format="dimension|reference"/>
        <attr name="verticalSpacing" format="dimension|reference"/>
        <attr name="dividerColor" format="color|reference"/>
        <attr name="dividerWidth" format="dimension|reference"/>
    </declare-styleable>

    <declare-styleable name="FlowLayout_Layout">
        <attr name="android:layout_gravity"/>
    </declare-styleable>

這里需要注意兩個(gè)地方:

  1. 我們聲明了兩個(gè)declare-styleable再姑,一個(gè)是為FlowLayout自身設(shè)置自定義屬性萌抵;另一個(gè)是為孩子們提供額外屬性,需要在自定義的LayoutParams里解析獲取屬性值元镀。

  2. 大家都知道绍填,我們?cè)趚ml布局文件里使用自定義屬性時(shí),需要引入命名空間

xmlns:app="http://schemas.android.com/apk/res-auto"

使用自定義屬性時(shí)栖疑,需要加上前綴app(或者是其它命名沐兰,只要一一對(duì)應(yīng))。但是有時(shí)候啊蔽挠,我們自定義的屬性名已經(jīng)在系統(tǒng)中存在了,而且語(yǔ)義與我們想要的也很符合瓜浸,比如如andrioid:text澳淑、android:gravity等等。這個(gè)時(shí)候估計(jì)誰(shuí)都會(huì)有一種“拿來(lái)主義”的沖動(dòng):直接使用系統(tǒng)里已經(jīng)存在的屬性名就好了嘛插佛,多“原生”杠巡!既然有這種“邪惡”的需求,那Google工程師自然是要滿(mǎn)足滴(~ ̄▽?zhuān)?~雇寇。

gravity屬性為例氢拥,我們只要在declare-styleable里直接寫(xiě)上<attr name="android:gravity"/>即可蚌铜,不過(guò)這里要注意的是不需要也不能再加上format屬性,加上format屬性就代表著這是在聲明一個(gè)新的屬性嫩海,不加則代表這是在使用已存在的一個(gè)屬性冬殃。

3.1.2 使用屬性

使用屬性就比較簡(jiǎn)單了:

<wang.mogujun.widget.FlowLayout
        android:id="@+id/flow2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:background="#6A6A6A"
        android:gravity="start"
        android:padding="8dp"
        app:horizonSpacing="8dp"
        app:verticalSpacing="12dp"
        app:dividerColor="#cccccc"
        app:dividerWidth="2dp"
        >

3.1.3 解析并獲取屬性

在xml設(shè)置了相應(yīng)的屬性后,就需要在FlowLayout里解析并獲取屬性值了:


public static final int DEFAULT_SPACING = 8;
    public static final int DEFAULT_DIVIDER_COLOR = Color.parseColor("#ececec");
    public static final int DEFAULT_DIVIDER_WIDTH = 3;

    private int mGravity = (isIcs() ? Gravity.START : Gravity.LEFT) | Gravity.TOP;

    private int mVerticalSpacing; //vertical spacing
    private int mHorizontalSpacing; //horizontal spacing
    private int mDividerColor;
    private int mDividerWidth;
    
private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {

        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout, defStyleAttr, defStyleRes);

        try {
            mHorizontalSpacing = (int) ta.getDimension(R.styleable.FlowLayout_horizonSpacing, DEFAULT_SPACING);
            mVerticalSpacing = (int) ta.getDimension(R.styleable.FlowLayout_verticalSpacing, DEFAULT_SPACING);
            mDividerWidth = (int) ta.getDimension(R.styleable.FlowLayout_dividerWidth, DEFAULT_DIVIDER_WIDTH);
            mDividerColor = ta.getColor(R.styleable.FlowLayout_dividerColor, DEFAULT_DIVIDER_COLOR);
            int index = ta.getInt(R.styleable.FlowLayout_android_gravity, -1);
            if (index > 0) {
                setGravity(index);
            }
            initPaint();
        } finally {
            ta.recycle();
        }
        setWillNotDraw(false);

    }

一般來(lái)說(shuō)叁怪,我們的自定義屬性都得給個(gè)默認(rèn)值审葬,大家都這么懶,不能強(qiáng)人所難對(duì)不對(duì)奕谭。這默認(rèn)值可以通過(guò)常量直接寫(xiě)在自定義類(lèi)里涣觉,如上述代碼所示。也可以寫(xiě)在xml資源文件里血柳,提供給別人統(tǒng)一修改官册。

其次呢,英明神武的蘑菇君自然也得提供方法讓別人方便的通過(guò)代碼去動(dòng)態(tài)修改這些屬性啦(真不要臉~~( ﹁ ﹁ ) ~~~):

 public void setHorizontalSpacing(int pixelSize) {
        mHorizontalSpacing = pixelSize;
        requestLayout();
    }

    public void setVerticalSpacing(int pixelSize) {
        mVerticalSpacing = pixelSize;
        requestLayout();
    }

    public void setDividerColor(@ColorInt int color) {
        mDividerColor = color;
        mDividerPaint.setColor(color);
        invalidate();
    }
    ...

關(guān)于自定義屬性的一些詳細(xì)知識(shí)可以參考文章: Android 深入理解Android中的自定義屬性

3.2 測(cè)量

在自定義ViewGroup時(shí)难捌,測(cè)量流程一般是所有流程中最為復(fù)雜的一環(huán)膝宁。因?yàn)槲覀儾粌H要測(cè)量ViewGroup自身的尺寸,還得測(cè)量所有孩子的尺寸栖榨。而ViewGroup和孩子們之間的尺寸又是相互影響的昆汹。

如下圖所示,在我們的FlowLayout里婴栽,當(dāng)寬的測(cè)量模式為AT_MOST(比如FlowLayout的布局屬性android:layout_widthwrap_content時(shí))满粗,F(xiàn)lowLayout的測(cè)量寬度應(yīng)該是所有行里最長(zhǎng)的那一行的寬度,在下圖中就是第二行的寬度愚争。而當(dāng)高的測(cè)量模式為AT_MOST映皆,F(xiàn)lowLayout的測(cè)量高度應(yīng)該是所有行的高度總和。

而對(duì)于child view來(lái)說(shuō)轰枝,也有個(gè)小小的限制:當(dāng)FlowLayout的layout_heightwrap_content捅彻,而child的layout_heightmatch_parent時(shí),我希望child的測(cè)量高為它所處那一行的高度鞍陨,而不是整個(gè)FlowLayout的高度或者是wrap_content步淹。這也挺合情合理的吧,比如下圖中第一行的child 再見(jiàn)這群坑比layout_heightmatch_parent诚撵,所以它就和第一行的高度一樣高缭裆。

寬高為wrap_content時(shí)的FlowLayout

可能說(shuō)得大家都有點(diǎn)暈了X﹏X,還是來(lái)一起看看onMeasure方法的源碼吧:

 //保存所有child view
private final List<List<View>> mLines = new ArrayList<>();
//保存所有行高
private final List<Integer> mLineHeights = new ArrayList<>();
//保存所有行寬
private final List<Integer> mLineWidths = new ArrayList<>();

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

        mLines.clear();
        mLineHeights.clear();
        mLineWidths.clear();

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

        int widthUsed = getPaddingLeft() + getPaddingRight() + mHorizontalSpacing;
        int lineWidth = widthUsed;
        int lineHeight = 0;

        int childCount = getChildCount();
        List<View> lineViews = new ArrayList<>();
        
        for (int i = 0; i < childCount; i++) {

            View child = getChildAt(i);

            if (child.getVisibility() == View.GONE) {
                continue;
            }

            LayoutParams lp = (LayoutParams) child.getLayoutParams();
            //測(cè)量每個(gè)child的寬高寿烟,每個(gè)child可用的最大寬高為sizeWidth-spacing-padding-margin
            measureChildWithMargins(child, widthMeasureSpec, mHorizontalSpacing * 2, heightMeasureSpec, mVerticalSpacing * 2);

            int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            //判斷這一行是否還能容下這個(gè)child
            if (lineWidth + childWidth + mHorizontalSpacing > sizeWidth) {
                //需要換行澈驼,則記錄這一行的寬度,高度筛武,下一行的初始寬度缝其,初始高度
            
                mLineWidths.add(lineWidth);
                lineWidth = widthUsed + childWidth + mHorizontalSpacing;

                mLineHeights.add(lineHeight);
                lineHeight = childHeight;

                mLines.add(lineViews);
                lineViews = new ArrayList<>();
            } else {//容得下挎塌,則累加這一行的寬度,記錄這一行的高度
                lineWidth += childWidth + mHorizontalSpacing;
                lineHeight = Math.max(lineHeight, childHeight);
            }

            lineViews.add(child);

        }
        //最后一行的處理
        mLineHeights.add(lineHeight);
        mLineWidths.add(lineWidth);
        mLines.add(lineViews);

        int maxWidth = Collections.max(mLineWidths);

        processChildHeights();//計(jì)算所有行的累積高度
        int totalHeight = getChildHeights();

        //TODO 處理getMinimumWidth/height的情況

        //設(shè)置自身的測(cè)量寬高
        setMeasuredDimension(
                (modeWidth == MeasureSpec.EXACTLY) ? sizeWidth : Math.min(maxWidth, sizeWidth),
                (modeHeight == MeasureSpec.EXACTLY) ? sizeHeight : Math.min(totalHeight, sizeHeight));
                
        //重新測(cè)量child的lp.height為MATCH_PARENT時(shí)的child的尺寸
        remeasureChild(widthMeasureSpec);
    }



上面的代碼邏輯都有注釋?zhuān)嘈糯蠹叶寄芾砬宕蟾诺倪壿嬆诒摺簳r(shí)沒(méi)理解也沒(méi)關(guān)系榴都,稍后自己去看代碼再加上自己的思考肯定能看懂滴。(蘑菇君自我感覺(jué)腦子轉(zhuǎn)的算慢的假残,看Github上的FlowLayout源碼花了蠻久時(shí)間才弄懂大概邏輯缭贡,自己畫(huà)圖呀,運(yùn)行demo呀,弄懂了以后辉懒,才開(kāi)始自己動(dòng)手寫(xiě)自己的FlowLayout...(??????)??)

這里要特別注意的是對(duì)children的測(cè)量過(guò)程阳惹。在上面的代碼中,我使用了ViewGroup類(lèi)里提供的measureChildWithMargins方法去測(cè)量每個(gè)child眶俩,對(duì)這個(gè)方法的具體剖析莹汤,可以去看自定義控件知識(shí)儲(chǔ)備-View的繪制流程,這篇文章講的很詳細(xì)颠印。但在上文中有提到過(guò)纲岭,我們對(duì)child有個(gè)限制:

當(dāng)child的layout_heightmatch_parent時(shí),child的測(cè)量高為它所處那一行的高度线罕,而不是整個(gè)FlowLayout的高度或者是wrap_content止潮。

但是這個(gè)child所處那一行的高度是那一行所有child的高度的最大值,所以只有在完成這一行所有child的測(cè)量后钞楼,才知道這一行的高度是多少喇闸。所以上面的要求無(wú)法滿(mǎn)足呀!我在測(cè)量該child的高度的時(shí)候询件,還不知道這一行的高度是多少叭颊А!

這就尷尬了

該怎么辦呢宛琅?其實(shí)也簡(jiǎn)單刻蟹,既然當(dāng)時(shí)測(cè)量某child的時(shí)候還不知道那一行的高度,那就在第一次所有child都測(cè)量完成后嘿辟,再對(duì)那些layout_heightmatch_parent的child測(cè)量一遍就好啦舆瘪。所以在上面onMeasure方法里的最后調(diào)用了remeasureChild這個(gè)方法去重新測(cè)量一遍child:

private void remeasureChild(int parentWidthSpec) {
        int numLines = mLines.size();
        for (int i = 0; i < numLines; i++) {//遍歷每一行
            int lineHeight = mLineHeights.get(i);
            List<View> lineViews = mLines.get(i);
            int children = lineViews.size();
            for (int j = 0; j < children; j++) {
                View child = lineViews.get(j);
                LayoutParams lp = (LayoutParams) child.getLayoutParams();
                if (lp.height == LayoutParams.MATCH_PARENT) {//對(duì)高為match_parent的child進(jìn)行處理
                    if (child.getVisibility() == View.GONE) {
                        continue;
                    }

                    int widthUsed = lp.leftMargin + lp.rightMargin +
                            getPaddingLeft() + getPaddingRight() + 2 * mHorizontalSpacing;
                    //再次調(diào)用child的measure方法進(jìn)行測(cè)量        
                    child.measure(
                            getChildMeasureSpec(parentWidthSpec, widthUsed, lp.width),
                            MeasureSpec.makeMeasureSpec(lineHeight - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY)
                    );
                }
            }
        }
    }

從這里我們也看得出來(lái),一個(gè)View的onMeasure方法是很有可能被調(diào)用多次來(lái)確定最終的測(cè)量寬高的红伦,所以下次遇到打印日志里或者斷點(diǎn)調(diào)試下發(fā)現(xiàn) onMeasure方法多次運(yùn)行介陶,莫要方呀o( ̄??)。

3.3 布局

布局過(guò)程呢色建,就稍微簡(jiǎn)單一些,因?yàn)槲覀冊(cè)?code>onMeasure方法里已經(jīng)將所有child的寬高和位于哪一行等信息都計(jì)算好了舌缤,只要遍歷children調(diào)用它們的layout方法放置好它們就行箕戳。不過(guò)這里有點(diǎn)麻煩的就是某残,我們需要支持FlowLayout自身的gravity屬性和children的 gravity屬性。那就得根據(jù)具體的gravity來(lái)計(jì)算相應(yīng)的偏移量了陵吸,代碼如下:

//根據(jù)gravity計(jì)算FlowLayout的垂直方向上的偏移量
private void processVerticalGravityMargin() {
        int verticalGravityMargin;
        int childHeights = getChildHeights();
        switch ((mGravity & Gravity.VERTICAL_GRAVITY_MASK)) {
            case Gravity.TOP://頂部
            default:
                verticalGravityMargin = 0;
                break;
            case Gravity.CENTER_VERTICAL://垂直居中
                verticalGravityMargin = Math.max((getHeight() - childHeights) / 2, 0);
                break;
            case Gravity.BOTTOM://底部
                verticalGravityMargin = Math.max(getHeight() - childHeights, 0);
                break;
        }
        mVerticalGravityMargin = verticalGravityMargin;
    }

//根據(jù)gravity計(jì)算FlowLayout的水平方向上的偏移量
    private void processHorizontalGravityMargins() {
        mLineMargins.clear();
        float horizontalGravityFactor;
        switch ((mGravity & Gravity.HORIZONTAL_GRAVITY_MASK)) {
            case Gravity.LEFT://水平靠左
            default:
                horizontalGravityFactor = 0;
                break;
            case Gravity.CENTER_HORIZONTAL://水平居中
                horizontalGravityFactor = .5f;
                break;
            case Gravity.RIGHT://水平靠右
                horizontalGravityFactor = 1;
                break;
        }

        int linesNum = mLineWidths.size();
        for (int i = 0; i < linesNum; i++) {
            int lineWidth = mLineWidths.get(i);
            mLineMargins.add((int) ((getWidth() - lineWidth) * horizontalGravityFactor) + getPaddingLeft() + mHorizontalSpacing);
        }
    }

給FlowLayout設(shè)置gravity的效果如下:

內(nèi)容居中:

FlowLayout內(nèi)容居中

內(nèi)容在右下角:

FlowLayout內(nèi)容在右下角

計(jì)算好了每行的偏移量后玻墅,layout方法的邏輯就很清晰了:

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

        processHorizontalGravityMargins();
        processVerticalGravityMargin();

        int numLines = mLines.size();
        int left;
        int top = getPaddingTop() + mVerticalSpacing + mVerticalGravityMargin;

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

            int lineHeight = mLineHeights.get(i);
            List<View> lineViews = mLines.get(i);
            left = mLineMargins.get(i);
            int children = lineViews.size();

            for (int j = 0; j < children; j++) {

                View child = lineViews.get(j);

                if (child.getVisibility() == View.GONE) {
                    continue;
                }

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

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

                int gravityMargin = 0;
                //根據(jù)child的gravity計(jì)算child的相應(yīng)偏移量
                if (Gravity.isVertical(lp.gravity)) {
                    switch (lp.gravity) {
                        case Gravity.TOP:
                        default:
                            gravityMargin = 0;
                            break;
                        case Gravity.CENTER_VERTICAL:
                        case Gravity.CENTER:
                            gravityMargin = (lineHeight - childHeight - lp.topMargin - lp.bottomMargin) / 2;
                            break;
                        case Gravity.BOTTOM:
                            gravityMargin = lineHeight - childHeight - lp.topMargin - lp.bottomMargin;
                            break;
                        //TODO 水平方向上可以支持gravity么?
                    }
                }

                child.layout(left + lp.leftMargin,
                        top + lp.topMargin + gravityMargin,
                        left + lp.leftMargin + childWidth,
                        top + lp.topMargin + gravityMargin + childHeight);

                Log.i(TAG, String.format("child (%d,%d) position: (%d,%d,%d,%d)",
                        i, j, child.getLeft(), child.getTop(), child.getRight(), child.getBottom()));

                left += childWidth + lp.leftMargin + lp.rightMargin + mHorizontalSpacing;

            }

            top += lineHeight + mVerticalSpacing;
        }

    }

3.4 繪制

本FlowLayout支持繪制分割線(xiàn)壮虫,這也是很容易的繪制澳厢,只要找準(zhǔn)每條分割線(xiàn)的位置就行。不過(guò)萬(wàn)變不離其宗嘛囚似,我現(xiàn)在能畫(huà)一條線(xiàn)剩拢,下次就能畫(huà)一個(gè)圓,再下次就能畫(huà)個(gè)雞蛋饶唤,再再下次我就能飛上天徐伐,畫(huà)出太陽(yáng)肩并肩...∧伎瘢咳咳办素,扯遠(yuǎn)了,我們還是來(lái)看看onDraw方法里的繪制邏輯:

@Override
    protected void onDraw(Canvas canvas) {

        int top = getPaddingTop() + mVerticalSpacing + mVerticalGravityMargin;
        int numLines = mLines.size();
        for (int i = 0; i < numLines; i++) {
            int lineHeight = mLineHeights.get(i);
            top += lineHeight + mVerticalSpacing;
            canvas.drawLine(getPaddingLeft(), top - mVerticalSpacing / 2, 
            getWidth() - getPaddingRight(), top - mVerticalSpacing / 2, mDividerPaint);
        }

    }

確實(shí)很簡(jiǎn)單祸穷,遍歷每一行性穿,在兩行的中間根據(jù)配置的顏色和寬度畫(huà)出一條線(xiàn)段即可。

不過(guò)這里要注意View的一個(gè)特殊方法:setWillNotDraw雷滚,來(lái)看一下這個(gè)方法的源碼:

/**
     * If this view doesn't do any drawing on its own, set this flag to
     * allow further optimizations. By default, this flag is not set on
     * View, but could be set on some View subclasses such as ViewGroup.
     *
     * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
     * you should clear this flag.
     *
     * @param willNotDraw whether or not this View draw on its own
     */
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }

從這個(gè)方法的注釋中可以看出需曾,如果一個(gè)View不需要繪制任何內(nèi)容,那么設(shè)置這個(gè)標(biāo)記位為true后揭措,系統(tǒng)會(huì)進(jìn)行相應(yīng)的優(yōu)化胯舷。默認(rèn)情況下,View沒(méi)有啟用這個(gè)優(yōu)化標(biāo)記位绊含,而ViewGroup會(huì)默認(rèn)啟用這個(gè)標(biāo)記位桑嘶。

當(dāng)我們的自定義ViewGroup需要通過(guò)重寫(xiě)onDraw來(lái)繪制內(nèi)容時(shí),我們需要顯式地關(guān)閉WILL_NOT_DRAW這個(gè)標(biāo)記位躬充。

所以逃顶,在這個(gè)FlowLayout的構(gòu)造方法里,我們可以調(diào)用setWillNotDraw(false)來(lái)進(jìn)行優(yōu)化充甚。

3.5 處理LayoutParams

幾乎每個(gè)自定義ViewGroup都得自定義自己的LayoutParams以政,來(lái)給children提供更好的服務(wù)。在本FlowLayout里伴找,能給children帶來(lái)的就是gravity屬性的支持盈蛮。來(lái)看看自定義的LayoutParams:

 public static class LayoutParams extends MarginLayoutParams {

        public int gravity = -1;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);

            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.FlowLayout_Layout);

            try {
                gravity = a.getInt(R.styleable.FlowLayout_Layout_android_layout_gravity, -1);
            } finally {
                a.recycle();
            }
        }

        public LayoutParams(int width, int height) {
            super(width, height);
            gravity = Gravity.TOP;
        }

        public LayoutParams(int width, int height, int gravity) {
            super(width, height);
            this.gravity = gravity;
        }

        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }

    }

同時(shí),F(xiàn)lowLayout還需要對(duì)以下幾個(gè)方法進(jìn)行重寫(xiě):

@Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return super.checkLayoutParams(p) && p instanceof LayoutParams;
    }

    @Override
    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new LayoutParams(p);
    }

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

啥技矮?不知道為啥要按上述代碼那樣做抖誉?那是時(shí)候去看看自定義控件知識(shí)儲(chǔ)備-LayoutParams的那些事了殊轴。看完了你就大徹大悟袒炉,遁入......咳咳旁理。

3. 展示

哎呀呀,這篇文章已經(jīng)夠長(zhǎng)了我磁,我就不貼資源文件孽文,截圖等東西啦,大家有需要的話(huà)夺艰,可以去Github上下載源碼進(jìn)行學(xué)習(xí)芋哭。

Github地址: https://github.com/yisizhu520/FlowLayout

PS:蘑菇君寫(xiě)的這個(gè)FlowLayout肯定還存在bug,而且我自己也知道幾個(gè)不影響使用的小bug劲适,但是我沒(méi)有去改楷掉,等待有緣人去發(fā)現(xiàn)哈(≧?≦)?。

也歡迎大家去提交issue和pull request霞势,一起交流烹植,一起進(jìn)步龙优。

4. 總結(jié)

終于寫(xiě)完這篇博客了溯香,真是寫(xiě)死我了?(T?T)。希望這篇文章除了能加深自己對(duì)自定義ViewGroup的理解外赃梧,還能幫助到大家固以。以前一直以為自己了解了自定義ViewGroup的一些知識(shí)墩虹,想要寫(xiě)一個(gè)容器控件出來(lái)應(yīng)該不難的。然而憨琳,紙上得來(lái)終覺(jué)淺诫钓,當(dāng)自己真的開(kāi)始寫(xiě)的時(shí)候,發(fā)現(xiàn)滿(mǎn)滿(mǎn)的都是細(xì)節(jié)篙螟,滿(mǎn)滿(mǎn)的都是套路菌湃。比如在FlowLayout里的測(cè)量、布局遍略、繪制都得考慮到間距的問(wèn)題惧所,什么margin啊,padding啊绪杏,spacing啊下愈,都需要小心對(duì)待。不過(guò)蕾久,最終還是在不斷的調(diào)試和修改中寫(xiě)出來(lái)了這個(gè)FlowLayout势似,想想還有點(diǎn)小激動(dòng)呢!以后要做的應(yīng)該就是不斷的練習(xí)和總結(jié),畢竟編程這件事履因,沒(méi)啥好說(shuō)的辖佣,just code it!

just code it

我是蘑菇君搓逾,我為自己帶鹽

5. 參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市杯拐,隨后出現(xiàn)的幾起案子霞篡,更是在濱河造成了極大的恐慌,老刑警劉巖端逼,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件朗兵,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡顶滩,警方通過(guò)查閱死者的電腦和手機(jī)余掖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)礁鲁,“玉大人盐欺,你說(shuō)我怎么就攤上這事〗龃迹” “怎么了冗美?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)析二。 經(jīng)常有香客問(wèn)我粉洼,道長(zhǎng),這世上最難降的妖魔是什么叶摄? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任属韧,我火速辦了婚禮,結(jié)果婚禮上蛤吓,老公的妹妹穿的比我還像新娘宵喂。我一直安慰自己,他們只是感情好柱衔,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布樊破。 她就那樣靜靜地躺著,像睡著了一般唆铐。 火紅的嫁衣襯著肌膚如雪哲戚。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,007評(píng)論 1 284
  • 那天艾岂,我揣著相機(jī)與錄音顺少,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛脆炎,可吹牛的內(nèi)容都是我干的梅猿。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼秒裕,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼袱蚓!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起几蜻,我...
    開(kāi)封第一講書(shū)人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤喇潘,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后梭稚,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體颖低,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年弧烤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了忱屑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡暇昂,死狀恐怖莺戒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情话浇,我是刑警寧澤脏毯,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站幔崖,受9級(jí)特大地震影響食店,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜赏寇,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一吉嫩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧嗅定,春花似錦自娩、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至碎乃,卻和暖如春姊扔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背梅誓。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工恰梢, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留佛南,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓嵌言,卻偏偏與公主長(zhǎng)得像嗅回,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子摧茴,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

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