FlowLayout:Android自定義ViewGroup實現(xiàn)

需求

我們需要實現(xiàn)一個自定義的Layout,該Layout可以容納若干個寬高不等的子元素,元素按照從左到右的順序排列,當元素超出屏幕顯示范圍時,換一行繼續(xù)顯示,like this


View和ViewGroup

Android的界面都是由ViewViewGroup及其派生類組合而成,其中,View是ViewGroup及其他UI組件的基類。ViewGroup是放置View的容器,在編寫xml布局文件的時候,View所有以layout開頭的屬性都是提供給ViewGroup的,ViewGroup根據(jù)這些屬性來給childView計算出測量模式和建議的寬高,并將childView繪制在屏幕上的適當位置

UI是怎樣被繪制出來的

UI組件渲染過程可分為三個階段:測量、布局、繪制.

Measure過程

Measure過程的任務(wù)是根據(jù)ViewGroup給的參數(shù)計算出視圖自身的大小,在View中與Measure過程相關(guān)的方法有measure()onMeasure()setMeasureDimension(),其中onMeasure()是我們需要在自定義視圖的時候重寫的方法,在measure()方法中,onMeasure()被調(diào)用,在onMeasure()計算完畢后,調(diào)用setMeasureDimension()設(shè)置自身大小。
自身大小的計算結(jié)果取決于視圖本身所占區(qū)域的大小及ViewGroup傳遞過來的MeasureMode值,其中MeasureMode可能取值為UNSPECIFIEDEXACTLYAT_MOST

UNSPECIFIED

表示childView可將自身大小設(shè)置為自身想要的任意大值,一般出現(xiàn)于AdapterView的item的高度屬性中

EXACTLY

表示childView應(yīng)該將自身大小設(shè)置為ViewGroup指定的大小,當View指定了自身寬或高為精確的值或match_parent時,ViewGroup會傳入該Mode

AT_MOST

表示childView可以在一個限定的最大值范圍內(nèi)設(shè)置自己的大小,當View指定自身寬或高為wrap_content時,ViewGroup會傳入該Mode
Measure過程結(jié)束后,視圖大小即被確定。

Layout過程

Layout過程的任務(wù)是決定視圖的位置,framework調(diào)用View的layout()方法來計算位置,在layout()方法中,onLayout()方法會被調(diào)用,這個方法是需要View的派生類重寫的,在此實現(xiàn)布局邏輯。
Layout是一個自頂向下遞歸的過程,先布局容器,再布局子視圖,因此,ViewGroup的位置一定程度上決定了它的childView的位置。
Layout過程結(jié)束后,視圖在屏幕上的位置即被確定。

Draw過程

Draw過程的任務(wù)是根據(jù)視圖的尺寸和位置,在相應(yīng)的區(qū)域內(nèi)繪制自身樣式。同樣的,framework會調(diào)用onDraw()方法,我們需要重寫onDraw()方法實現(xiàn)繪制邏輯,在View中,可以通過調(diào)用invalidate()方法觸發(fā)視圖重繪。

讓View支持Padding和Margin

如上文所說,所有以layout開頭的屬性都是交由容器處理的,layout_margin就是這樣一個屬性,在自定義View中可通過getLayoutParams()返回的LayoutParams對象來獲取到視圖各個方向的margin值,容器只需在layout過程中將margin值作為偏移量加入即可實現(xiàn)將視圖放置在正確位置。
Padding是視圖的自有屬性,描述其各個方向邊界到內(nèi)部內(nèi)容的距離,在代碼中可通過getPaddingTop(/Left/Right/Bottom)()來獲取各個方向的padding值,在Measure過程中,需要注意View尺寸包含內(nèi)容區(qū)域加上padding區(qū)域,padding區(qū)域內(nèi)的內(nèi)容將不會被繪制。

Step by Step實現(xiàn)需求

onMeasure

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int contentWidth = MeasureSpec.getSize(widthMeasureSpec);
        int contentHeight = MeasureSpec.getSize(heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        //Padding支持
        int topOffset = getPaddingTop();
        int leftOffset = getPaddingLeft();
        int selfWidth = 0, selfHeight = 0;
        int currentLineWidth = 0, currentLineHeight = 0;
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == GONE)
                continue;
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            MarginLayoutParams layoutParams = (MarginLayoutParams) child.getLayoutParams();
            int childWidth = Math.max(child.getMeasuredWidth(), getSuggestedMinimumWidth()) + layoutParams.leftMargin + layoutParams.rightMargin;
            int childHeight = Math.max(child.getMeasuredHeight(), getSuggestedMinimumHeight()) + layoutParams.topMargin + layoutParams.bottomMargin;
            if (currentLineWidth + childWidth > contentWidth  - getPaddingLeft() - getPaddingRight()) {
                //需要另起一行
                currentLineWidth = Math.max(currentLineWidth,childWidth);
                selfWidth = Math.max(selfWidth, currentLineWidth);
                currentLineWidth = childWidth;
                selfHeight += currentLineHeight;
                currentLineHeight = childHeight;
                //Measure的時候順便把位置計算出來
                child.setTag(new Location(child, leftOffset, selfHeight + topOffset, childWidth + leftOffset, selfHeight + child.getMeasuredHeight() + topOffset));
            } else {
                //不需要換行
                child.setTag(new Location(child, currentLineWidth + leftOffset, selfHeight + topOffset, currentLineWidth + child.getMeasuredWidth() + topOffset, selfHeight + child.getMeasuredHeight() + topOffset));
                currentLineWidth += childWidth;
                currentLineHeight = Math.max(currentLineHeight, childHeight);
            }
            if (i == childCount - 1) {
                //到最后一個child的時候更新高度
                sselfWidth = Math.max(currentLineWidth, selfWidth) + getPaddingRight() + getPaddingLeft();
                selfHeight += currentLineHeight + getPaddingTop() + getPaddingBottom();
            }
        }
        setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? contentWidth : selfWidth,
                heightMode == MeasureSpec.EXACTLY ? contentHeight : selfHeight);
    }

經(jīng)過如上處理,ViewGroup的尺寸和childView的位置便被計算出來,并且ViewGroup可根據(jù)childView排列情況自動調(diào)整自身寬高。

onLayout

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                Location location = (Location) child.getTag();
                child.layout(location.left, location.top, location.right, location.bottom);
            }
        }
    }

onMeasure()中我們已經(jīng)順手計算出了各個childView的位置信息,所以在Layout步驟中只需將其按照位置擺放到相應(yīng)區(qū)域即可。

draw

draw方法是由framework調(diào)用,在ViewGroup的onDraw()方法中繪制的內(nèi)容最后會被作為ViewGroup的背景,所以如果需要更改背景內(nèi)容可重寫該方法。draw()中會遞歸調(diào)用childView的onDraw()方法,調(diào)用完畢后ViewGroup本身和childView都繪制完畢,一次渲染過程到此結(jié)束。

附加特性

可通過對ViewGroup設(shè)置LayoutAnimation來為childView顯示的過程附加動畫,一個簡單的例子:

public FlowLayout(Context context) {
        super(context);
        setLayoutAnimation(
                new LayoutAnimationController(AnimationUtils.loadAnimation(
                        getContext(), R.anim.list_animation), 0.3f));
    }

可以重寫addView()方法為動態(tài)添加的View附加動畫,重寫removeView()方法實現(xiàn)移除View時的附加動畫。

實現(xiàn)效果

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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