需求
我們需要實現(xiàn)一個自定義的Layout,該Layout可以容納若干個寬高不等的子元素,元素按照從左到右的順序排列,當元素超出屏幕顯示范圍時,換一行繼續(xù)顯示,like this
View和ViewGroup
Android的界面都是由View
、ViewGroup
及其派生類組合而成,其中,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
可能取值為UNSPECIFIED
、EXACTLY
和AT_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時的附加動畫。