需求
我們需要實(shí)現(xiàn)一個(gè)自定義的Layout,該Layout可以容納若干個(gè)寬高不等的子元素吹榴,元素按照從左到右的順序排列亭敢,當(dāng)元素超出屏幕顯示范圍時(shí),換一行繼續(xù)顯示图筹,like this
View和ViewGroup
Android的界面都是由View
帅刀、ViewGroup
及其派生類(lèi)組合而成,其中远剩,View是ViewGroup及其他UI組件的基類(lèi)劝篷。ViewGroup是放置View的容器,在編寫(xiě)xml布局文件的時(shí)候民宿,View所有以layout開(kāi)頭的屬性都是提供給ViewGroup的,ViewGroup根據(jù)這些屬性來(lái)給childView計(jì)算出測(cè)量模式和建議的寬高像鸡,并將childView繪制在屏幕上的適當(dāng)位置
UI是怎樣被繪制出來(lái)的
UI組件渲染過(guò)程可分為三個(gè)階段:測(cè)量活鹰、布局、繪制.
Measure過(guò)程
Measure過(guò)程的任務(wù)是根據(jù)ViewGroup給的參數(shù)計(jì)算出視圖自身的大小只估,在View中與Measure過(guò)程相關(guān)的方法有measure()
志群、onMeasure()
和setMeasureDimension()
,其中onMeasure()
是我們需要在自定義視圖的時(shí)候重寫(xiě)的方法蛔钙,在measure()
方法中锌云,onMeasure()
被調(diào)用,在onMeasure()
計(jì)算完畢后吁脱,調(diào)用setMeasureDimension()
設(shè)置自身大小桑涎。
自身大小的計(jì)算結(jié)果取決于視圖本身所占區(qū)域的大小及ViewGroup傳遞過(guò)來(lái)的MeasureMode
值,其中MeasureMode
可能取值為UNSPECIFIED
兼贡、EXACTLY
和AT_MOST
攻冷。
UNSPECIFIED
表示childView可將自身大小設(shè)置為自身想要的任意大值,一般出現(xiàn)于AdapterView的item的高度屬性中
EXACTLY
表示childView應(yīng)該將自身大小設(shè)置為ViewGroup指定的大小遍希,當(dāng)View指定了自身寬或高為精確的值或match_parent
時(shí)等曼,ViewGroup會(huì)傳入該Mode
AT_MOST
表示childView可以在一個(gè)限定的最大值范圍內(nèi)設(shè)置自己的大小,當(dāng)View指定自身寬或高為wrap_content
時(shí),ViewGroup會(huì)傳入該Mode
Measure過(guò)程結(jié)束后禁谦,視圖大小即被確定胁黑。
Layout過(guò)程
Layout過(guò)程的任務(wù)是決定視圖的位置,framework調(diào)用View的layout()
方法來(lái)計(jì)算位置州泊,在layout()
方法中丧蘸,onLayout()
方法會(huì)被調(diào)用,這個(gè)方法是需要View的派生類(lèi)重寫(xiě)的拥诡,在此實(shí)現(xiàn)布局邏輯触趴。
Layout是一個(gè)自頂向下遞歸的過(guò)程,先布局容器渴肉,再布局子視圖冗懦,因此,ViewGroup的位置一定程度上決定了它的childView的位置仇祭。
Layout過(guò)程結(jié)束后披蕉,視圖在屏幕上的位置即被確定。
Draw過(guò)程
Draw過(guò)程的任務(wù)是根據(jù)視圖的尺寸和位置乌奇,在相應(yīng)的區(qū)域內(nèi)繪制自身樣式没讲。同樣的,framework會(huì)調(diào)用onDraw()
方法礁苗,我們需要重寫(xiě)onDraw()
方法實(shí)現(xiàn)繪制邏輯爬凑,在View中,可以通過(guò)調(diào)用invalidate()
方法觸發(fā)視圖重繪试伙。
讓View支持Padding和Margin
如上文所說(shuō)嘁信,所有以layout開(kāi)頭的屬性都是交由容器處理的,layout_margin
就是這樣一個(gè)屬性疏叨,在自定義View中可通過(guò)getLayoutParams()
返回的LayoutParams對(duì)象來(lái)獲取到視圖各個(gè)方向的margin值潘靖,容器只需在layout過(guò)程中將margin值作為偏移量加入即可實(shí)現(xiàn)將視圖放置在正確位置。
Padding是視圖的自有屬性蚤蔓,描述其各個(gè)方向邊界到內(nèi)部?jī)?nèi)容的距離卦溢,在代碼中可通過(guò)getPaddingTop(/Left/Right/Bottom)()
來(lái)獲取各個(gè)方向的padding值,在Measure過(guò)程中秀又,需要注意View尺寸包含內(nèi)容區(qū)域加上padding區(qū)域单寂,padding區(qū)域內(nèi)的內(nèi)容將不會(huì)被繪制。
Step by Step實(shí)現(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的時(shí)候順便把位置計(jì)算出來(lái)
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) {
//到最后一個(gè)child的時(shí)候更新高度
sselfWidth = Math.max(currentLineWidth, selfWidth) + getPaddingRight() + getPaddingLeft();
selfHeight += currentLineHeight + getPaddingTop() + getPaddingBottom();
}
}
setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? contentWidth : selfWidth,
heightMode == MeasureSpec.EXACTLY ? contentHeight : selfHeight);
}
經(jīng)過(guò)如上處理疲扎,ViewGroup的尺寸和childView的位置便被計(jì)算出來(lái)壹甥,并且ViewGroup可根據(jù)childView排列情況自動(dòng)調(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)順手計(jì)算出了各個(gè)childView的位置信息,所以在Layout步驟中只需將其按照位置擺放到相應(yīng)區(qū)域即可。
draw
draw方法是由framework調(diào)用僻族,在ViewGroup的onDraw()
方法中繪制的內(nèi)容最后會(huì)被作為ViewGroup的背景述么,所以如果需要更改背景內(nèi)容可重寫(xiě)該方法。draw()
中會(huì)遞歸調(diào)用childView的onDraw()
方法敷钾,調(diào)用完畢后ViewGroup本身和childView都繪制完畢挠锥,一次渲染過(guò)程到此結(jié)束粱侣。
附加特性
可通過(guò)對(duì)ViewGroup設(shè)置LayoutAnimation
來(lái)為childView顯示的過(guò)程附加動(dòng)畫(huà),一個(gè)簡(jiǎn)單的例子:
public FlowLayout(Context context) {
super(context);
setLayoutAnimation(
new LayoutAnimationController(AnimationUtils.loadAnimation(
getContext(), R.anim.list_animation), 0.3f));
}
可以重寫(xiě)addView()
方法為動(dòng)態(tài)添加的View附加動(dòng)畫(huà)情妖,重寫(xiě)removeView()
方法實(shí)現(xiàn)移除View時(shí)的附加動(dòng)畫(huà)。