概述
關於自定義View,看到一篇文章,思路清晰,幫到了我,想說記錄下來,總結下
Android中View框架的工作機制中,主要有三個過程:
1、View樹的測量(measure)Android View框架的measure機制
2、View樹的佈局(layout) Android View框架的layout機制
3、View樹的繪製(draw)Android View框架的draw機制
View框架的工作流程爲:測量每個View大小(measure)-->把每個View放置到相應的位置(layout)-->繪製每個View(draw)。
開發人員在繪製UI的時候,基本都是通過XML佈局文件的方式來配置UI,而每個View必須要設置的兩個羣屬性就是layout_width和layout_height,這兩個屬性代表着當前View的尺寸。
所以這兩個屬性的值是必須要指定的,這兩個屬性的取值只能爲三種類型:
1、固定的大小,比如100dp。
2、剛好包裹其中的內容,wrap_content。
3、想要和父佈局一樣大,match_parent / fill_parent。
由於Android希望提供一個更優雅的GUI框架,所以提供了自適應的尺寸,也就是 wrap_content 和 match_parent 。
試想一下,那如果這些屬性只允許設置固定的大小,那麼每個View的尺寸在繪製的時候就已經確定了,所以可能都不需要measure過程。但是由於需要滿足自適應尺寸的機制,所以需要一個measure過程。
measure過程都幹了點什麼事?
由於上面提到的自適應尺寸的機制,所以在用自適應尺寸來定義View大小的時候,View的真實尺寸還不能確定。但是View尺寸最終需要映射到屏幕上的像素大小,所以measure過程就是幹這件事,把各種尺寸值,經過計算,得到具體的像素值。measure過程會遍歷整棵View樹,然後依次測量每個View真實的尺寸。具體是每個ViewGroup會向它內部的每個子View發送measure命令,然後由具體子View的onMeasure()來測量自己的尺寸。最後測量的結果保存在View的mMeasuredWidth和mMeasuredHeight中,保存的數據單位是像素。
對於自適應的尺寸機制,如何合理的測量一顆View樹?
系統在遍歷完佈局文件後,針對佈局文件,在內存中生成對應的View樹結構,這個時候,整棵View樹種的所有View對象,都還沒有具體的尺寸,因爲measure過程最終是要確定每個View打的準確尺寸,也就是準確的像素值。但是剛開始的時候,View中layout_width和layout_height兩個屬性的值,都只是自適應的尺寸,也就是match_parent和wrap_content,這兩個值在系統中爲負數,所以系統不會把它們當成具體的尺寸值。所以當一個View需要把它內部的match_parent或者wrap_content轉換成具體的像素值的時候,他需要知道兩個信息。
1、針對於match_parent,父佈局當前具體像素值是多少,因爲match_parent就是子View想要和父佈局一樣大。
2、針對wrap_content,子View需要根據當前自己內部的content,算出一個合理的能包裹所有內容的最小值。但是如果這個最小值比當前父佈局還大,那不行,父佈局會告訴你,我只有這麼大,你也不應該超過這個尺寸。
也就是說,在measure過程中,ViewGroup會根據自己當前的狀況,結合子View的尺寸數據,進行一個綜合評定,然後把相關信息告訴子View,然後子View在onMeasure自己的時候,一邊需要考慮到自己的content大小,一邊還要考慮的父佈局的限制信息,然後綜合評定,測量出一個最優的結果。
那麼ViewGroup是如何向子View傳遞限制信息的?
談到傳遞限制信息,那就是MeasureSpec類了,該類貫穿於整個measure過程,用來傳遞父佈局對子View尺寸測量的約束信息。簡單來說,該類就保存兩類數據。
1、子View當前所在父佈局的具體尺寸。
2、父佈局對子View的限制類型。
那麼限制類型又分爲三種類型:
1、UNSPECIFIED,不限定。意思就是,子View想要多大,我就可以給你多大,你放心大膽的measure吧,不用管其他的。也不用管我傳遞給你的尺寸值。(其實Android高版本中推薦,只要是這個模式,尺寸設置爲0)
2、EXACTLY,精確的。意思就是,根據我當前的狀況,結合你指定的尺寸參數來考慮,你就應該是這個尺寸,具體大小在MeasureSpec的尺寸屬性中,自己去查看吧,你也不要管你的content有多大了,就用這個尺寸吧。
3、AT_MOST,最多的。意思就是,根據我當前的情況,結合你指定的尺寸參數來考慮,在不超過我給你限定的尺寸的前提下,你測量一個恰好能包裹你內容的尺寸就可以了。
當自定義View的時候,也需要處理measure過程,主要有兩種情況。
1、繼承自View的子類。
需要覆寫onMeasure來正確測量自己。最後都需要調用setMeasuredDimension來保存測量結果
一般來說,自定義View的measure過程僞代碼爲:
2、繼承自ViewGroup的子類。
不但需要覆寫onMeasure來正確測量自己,可能還要覆寫一系列measureChild方法,來正確的測量子view,比如ScrollView。或者乾脆放棄父類實現的measureChild規則,自己重新實現一套測量子view的規則,比如RelativeLayout。最後都需要調用setMeasuredDimension來保存測量結果。
一般來說,自定義ViewGroup的measure過程的僞代碼爲:
layout過程都幹了點什麼事?
由於View是以樹結構進行存儲,所以典型的數據操作就是遞歸操作,所以,View框架中,採用了內部自治的layout過程。
每個葉子節點根據父節點傳遞過來的位置信息,設置自己的位置數據,每個非葉子節點,除了負責根據父節點傳遞過來的位置信息,設置自己的位置數據外(如果有父節點的話),還需要根據自己內部的layout規則(比如垂直排布等),計算出每一個子節點的位置信息,然後向子節點傳遞layout過程。
對於ViewGroup,除了根據自己的parent傳遞的位置信息,來設置自己的位置之外,還需要根據自己的layout規則,爲每一個子View計算出準確的位置(相對於子View的父佈局的位置)。
對於View,根據自己的parent傳遞的位置信息,來設置自己的位置。
View對象的位置信息,在內部是以4個成員變量的保存的,分別是mLeft、mRight、mTop、mBottom。他們的含義如圖所示。
View:
1、layout
/**
分配一個位置信息到一個View上面,每個parent會調用children的layout方法來設置children的位置。最好不要覆寫該方法,有children的viewGroup,應該覆寫onLayout方法
*/
public void layout(int l, int t, int r, int b) ;
ViewGroup:
ViewGroup中,只需要覆寫onLayout方法,來計算出每一個子View的位置,並且把layout流程傳遞給子View。
源代碼:
ViewGroup沒有實現,具體可以參考LinearLayout和RelativeLayout的onLayout方法。雖然各個具體實現都很複雜,但是基本流程是一樣的,可以參考下面的僞代碼
結論
一般來說,自定義View,如果該View不包含子View,類似於TextView這種的,是不需要覆寫onLayout方法的。而含有子View的,比如LinearLayout這種,就需要根據自己的佈局規則,來計算每一個子View的位置。
draw過程都幹了點什麼事?
View框架中,draw過程主要是繪製View的外觀。ViewGroup除了負責繪製自己之外,還需要負責繪製所有的子View。而不含子View的View對象,就負責繪製自己就可以了。
draw過程的主要流程如下:
1、繪製 backgroud(drawBackground)
2、如果需要的話,保存canvas的layer,來準備fading(不是必要的步驟)
3、繪製view的content(onDraw方法)
4、繪製children(dispatchDraw方法)
5、如果需要的話,繪製fading edges,然後還原layer(不是必要的步驟)
6、繪製裝飾器、比如scrollBar(onDrawForeground)
View:
1、draw
/**
繪製一個View以及他的子View。最好不要覆寫該方法,應該覆寫onDraw方法來繪製自己。
*/
public void draw(Canvas canvas);
ViewGroup:
1、dispatchDraw
/** 繪製子View,View類是空實現,ViewGroup類中有實現 */
protected void dispatchDraw(Canvas canvas);
代碼示例:
public class VerticalOffsetLayout extends ViewGroup { private static final int OFFSET = 100; private Paint mPaint; public VerticalOffsetLayout(Context context) { super(context); init(context, null, 0); } public VerticalOffsetLayout(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs, 0); } public VerticalOffsetLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr); } private void init(Context context, AttributeSet attrs, int defStyleAttr) { mPaint = new Paint(Color.BLUE); mPaint.setAntiAlias(true); mPaint.setAlpha(125); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width = 0; int height = 0; int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); ViewGroup.LayoutParams lp = child.getLayoutParams(); int childWidthSpec = getChildMeasureSpec(widthMeasureSpec, 0, lp.width); int childHeightSpec = getChildMeasureSpec(heightMeasureSpec, 0, lp.height); child.measure(childWidthSpec, childHeightSpec); } switch (widthMode) { case MeasureSpec.EXACTLY: width = widthSize; break; case MeasureSpec.AT_MOST: case MeasureSpec.UNSPECIFIED: for (int i = 0; i < childCount; i++) { View child = getChildAt(i); int widthAddOffset = i * OFFSET + child.getMeasuredWidth(); width = Math.max(width, widthAddOffset); } break; default: break; } switch (heightMode) { case MeasureSpec.EXACTLY: height = heightSize; break; case MeasureSpec.AT_MOST: case MeasureSpec.UNSPECIFIED: for (int i = 0; i < childCount; i++) { View child = getChildAt(i); height = height + child.getMeasuredHeight(); } break; default: break; } setMeasuredDimension(width, height); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int left = 0; int right = 0; int top = 0; int bottom = 0; int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); left = i * OFFSET; right = left + child.getMeasuredWidth(); bottom = top + child.getMeasuredHeight(); child.layout(left, top, right, bottom); top += child.getMeasuredHeight(); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int x = getWidth()/2; int y = getHeight()/2; canvas.drawCircle(x, y, Math.min(x, y), mPaint); } }