記得剛開始做安卓的時候,一直很好奇EditText、TextView、ListView、Relativelayout等等控件是如何工作的呢,他們的父控件或者頂層控件究竟是怎么樣的呢?
今天我就帶著大家一起去探索一下。
那么View的繪制流程是怎么樣的呢,通過看源碼發現View的繪制流程從ViewRoot的performTraversals方法開始,經過measure、layout和draw三大流程,preformTranversals會依次調用performMeasure、performLayout和preformDraw三個方法,這三個方法分別完成頂級View的measure、layout、和draw這三大流程、performMeasure方法中會調用measure方法,在measure方法中又會調用onMeasure方法,在onMeasure方法中會對所有的子元素進行measure過程,這個時候measure流程就從父容器傳遞到子元素了,這樣就完成了一次measure過程,layout和draw的過程類似,measure過程決定了view的寬高,在幾乎所有的情況下這個寬高都等同于view最終的寬高。layout過程決定了view的四個頂點的坐標和view實際的寬高,通過getWidth和getHeight方法可以得到最終的寬高。draw過程決定了view的顯示。
下面我們來看看performTraversals這個方法:
private void performTraversals() {
.....
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
......
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
......
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
......
mView.draw(canvas);
......
}
接下來我們再看一段Root View的源碼:
/**
* Figures out the measure spec for the root view in a window based on it's
* layout params.
*
* @param windowSize
* The available width or height of the window
*
* @param rootDimension
* The layout params for one dimension (width or height) of the
* window.
*
* @return The measure spec to use to measure the root view.
*/
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
......
}
return measureSpec;
}
上面傳入參數后這個函數走的是MATCH_PARENT,使用MeasureSpec.makeMeasureSpec方法組裝一個MeasureSpec,MeasureSpec的specMode等于EXACTLY,specSize等于windowSize,也就是為何根視圖總是全屏的原因.
接下來我們從源碼看看onMeasure、onLayout、onDraw這三個方法
先來看看是怎么描述measure方法的源碼:
/**
* <p>
* This is called to find out how big a view should be. The parent
* supplies constraint information in the width and height parameters.
* </p>
*
* <p>
* The actual measurement work of a view is performed in
* {@link #onMeasure(int, int)}, called by this method. Therefore, only
* {@link #onMeasure(int, int)} can and must be overridden by subclasses.
* </p>
*
*
* @param widthMeasureSpec Horizontal space requirements as imposed by the
* parent
* @param heightMeasureSpec Vertical space requirements as imposed by the
* parent
*
* @see #onMeasure(int, int)
*/
//final方法,子類不可重寫
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
......
//回調onMeasure()方法
onMeasure(widthMeasureSpec, heightMeasureSpec);
......
}
我們分析上面的注釋知道,measure為整個View樹計算實際的大小,然后設置實際的高和寬,每個View控件的實際寬高都是由父視圖和自身決定的。實際的測量是在onMeasure方法進行,所以在View的子類需要重寫onMeasure方法,這是因為measure方法是final的,不允許重載,所以View子類只能通過重載onMeasure來實現自己的測量邏輯。
onMeasure(widthMeasureSpec, heightMeasureSpec);方法中的這兩個參數都是父View傳遞過來的,也就是代表了父view的規格。總共32位,它由兩部分組成,前2位表示MODE,定義在MeasureSpec類(View的內部類)中,有三種類型,MeasureSpec.EXACTLY表示由父控件確定了大小, MeasureSpec.AT_MOST表示父控件允許子控件設置大小,但是設置的大小一定要在父控件的范圍之內, MeasureSpec.UNSPECIFIED不確定,隨意設,沒限制,但是不管你設置多少(值大于父控件了已經),最后展示出來的,都是父控件的大小。后30位表示size,也就是父View的大小。對于系統Window類的DecorVIew對象Mode一般都為MeasureSpec.EXACTLY ,而size分別對應屏幕寬高。對于子View來說大小是由父View和子View共同決定的。
簡而言之:
widthMeasureSpec 從父控件傳遞過來的寬度(包括模式和大小),
heightMeasureSpec 從父控件傳遞過來的高度(包括模式和大小)
下面來看看onMeasure的源碼:
/**
* <p>
* Measure the view and its content to determine the measured width and the
* measured height. This method is invoked by {@link #measure(int, int)} and
* should be overriden by subclasses to provide accurate and efficient
* measurement of their contents.
* </p>
*
* <p>
* <strong>CONTRACT:</strong> When overriding this method, you
* <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
* measured width and height of this view. Failure to do so will trigger an
* <code>IllegalStateException</code>, thrown by
* {@link #measure(int, int)}. Calling the superclass'
* {@link #onMeasure(int, int)} is a valid use.
* </p>
*
* <p>
* The base class implementation of measure defaults to the background size,
* unless a larger size is allowed by the MeasureSpec. Subclasses should
* override {@link #onMeasure(int, int)} to provide better measurements of
* their content.
* </p>
*
* <p>
* If this method is overridden, it is the subclass's responsibility to make
* sure the measured height and width are at least the view's minimum height
* and width ({@link #getSuggestedMinimumHeight()} and
* {@link #getSuggestedMinimumWidth()}).
* </p>
*
* @param widthMeasureSpec horizontal space requirements as imposed by the parent.
* The requirements are encoded with
* {@link android.view.View.MeasureSpec}.
* @param heightMeasureSpec vertical space requirements as imposed by the parent.
* The requirements are encoded with
* {@link android.view.View.MeasureSpec}.
*
* @see #getMeasuredWidth()
* @see #getMeasuredHeight()
* @see #setMeasuredDimension(int, int)
* @see #getSuggestedMinimumHeight()
* @see #getSuggestedMinimumWidth()
* @see android.view.View.MeasureSpec#getMode(int)
* @see android.view.View.MeasureSpec#getSize(int)
*/
//View的onMeasure默認實現方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
我們可以看見onMeasure默認的實現僅僅調用了setMeasuredDimension,setMeasuredDimension函數是一個很關鍵的函數,它對View的成員變量mMeasuredWidth和mMeasuredHeight變量賦值,measure的主要目的就是對View樹中的每個View的mMeasuredWidth和mMeasuredHeight進行賦值,所以一旦這兩個變量被賦值意味著該View的測量工作結束。既然這樣那我們就看看設置的默認尺寸大小吧,可以看見setMeasuredDimension傳入的參數都是通過getDefaultSize返回的,所以再來看下getDefaultSize方法源碼,如下:
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
//通過MeasureSpec解析獲取mode與size
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
從中可以知道,如果specMode等于AT_MOST或EXACTLY就返回specSize,這是系統默認的規格。
其中getDefaultSize參數的widthMeasureSpec和heightMeasureSpec都是由父View傳遞進來的。getSuggestedMinimumWidth與getSuggestedMinimumHeight都是View的方法,具體如下:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
源碼建議的最小寬度和高度都是由View的Background尺寸與通過設置View的miniXXX屬性共同決定的。
到這里一次最基本的Measure過程就完了,View實際是嵌套的,而且measure是遞歸傳遞的,所以每個View都需要measure。(還有viewGroup的measure,這個稍顯復雜,下一篇我們聊)
對于view控件的子控件來說最好不要重載onMeasure的時候調用setMeasuredDimension來設置任意大小的布局,最好使用默認的值。
接下來開始第二步,layout
private void performTraversals() {
......
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
......
}
可以看見layout方法接收四個參數,這四個參數分別代表相對Parent的左、上、右、下坐標。而且還可以看見左上都為0,右下分別為上面剛剛測量的width和height。
至此又回歸到View的layout(int l, int t, int r, int b)方法中去實現具體邏輯了,所以接下來我們開始分析View的layout過程(View的layout源碼):
public void layout(int l, int t, int r, int b) {
......
//實質都是調用setFrame方法把參數分別賦值給mLeft、mTop、mRight和mBottom這幾個變量
//判斷View的位置是否發生過變化,以確定有沒有必要對當前的View進行重新layout
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//需要重新layout
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//回調onLayout
onLayout(changed, l, t, r, b);
......
}
......
}
再來看看ViewGroup的layout方法:
@Override
public final void layout(int l, int t, int r, int b) {
......
super.layout(l, t, r, b);
......
}
對比上面View的layout和ViewGroup的layout方法可以發現,View的layout方法是可以在子類重寫的,而ViewGroup的layout是不能在子類重寫的,言外之意就是說ViewGroup中只能通過重寫onLayout方法.
而在ViewGroup中onLayout的方法是這樣的:
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
是一個抽象方法,這就是說,只要是ViewGroup的子類就必須重寫onLayout這個方法所以在自定義ViewGroup控件中,onLayout配合onMeasure方法一起使用可以實現自定義View的復雜布局。自定義View首先調用onMeasure進行測量,然后調用onLayout方法動態獲取子View和子View的測量大小,然后進行layout布局。重載onLayout的目的就是安排其children在父View的具體位置,重載onLayout通常做法就是寫一個for循環調用每一個子視圖的layout(l, t, r, b)函數,傳入不同的參數l, t, r, b來確定每個子視圖在父視圖中的顯示位置。
整個layout過程比較容易理解,layout是從頂層父View向子View的遞歸調用view.layout方法的過程,即父View根據上一步measure子View所得到的布局大小和布局參數,將子View放在合適的位置上。具體layout核心主要有以下幾點:
View.layout方法可被重載,ViewGroup.layout為final的不可重載,ViewGroup.onLayout為abstract的,子類必須重載實現自己的位置邏輯。
measure操作完成后得到的是對每個View經測量過的measuredWidth和measuredHeight,layout操作完成之后得到的是對每個View進行位置分配后的mLeft、mTop、mRight、mBottom,這些值都是相對于父View來說的。
凡是layout_XXX的布局屬性基本都針對的是包含子View的ViewGroup的,當對一個沒有父容器的View設置相關layout_XXX屬性是沒有任何意義的。
使用View的getWidth()和getHeight()方法來獲取View測量的寬高,必須保證這兩個方法在onLayout流程之后被調用才能返回有效值。
最后就來分析分析draw這個方法了,View的draw源碼:
public void draw(Canvas canvas) {
......
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
......
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
......
// Step 2, save the canvas' layers
......
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
......
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
......
if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, right, top + length, p);
}
......
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
......
}
從源碼中看出,總共有6步繪制,我們來簡單看看這幾步,
1.對View的背景進行繪制
private void drawBackground(Canvas canvas) {
//獲取xml中通過android:background屬性或者代碼中setBackgroundColor()、setBackgroundResource()等方法進行賦值的背景Drawable
final Drawable background = mBackground;
......
//根據layout過程確定的View位置來設置背景的繪制區域
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
rebuildOutline();
}
......
//調用Drawable的draw()方法來完成背景的繪制工作
background.draw(canvas);
......
}
3.對View的內容進行繪制
/**
* Implement this to do your drawing.
*
* @param canvas the canvas on which the background will be drawn
*/
protected void onDraw(Canvas canvas) {
}
4.對當前View的所有子View進行繪制,如果當前的View沒有子View就不需要進行繪制
/**
* Called by draw to draw the child views. This may be overridden
* by derived classes to gain control just before its children are drawn
* (but after its own view has been drawn).
* @param canvas the canvas on which to draw the view
*/
protected void dispatchDraw(Canvas canvas) {
}
6.對View的滾動條進行繪制
/**
* <p>Request the drawing of the horizontal and the vertical scrollbar. The
* scrollbars are painted only if they have been awakened first.</p>
*
* @param canvas the canvas on which to draw the scrollbars
*
* @see #awakenScrollBars(int)
*/
protected final void onDrawScrollBars(Canvas canvas) {
}
基本上就是這樣的,在繪制控件的過程中常會用到控件的刷新,invalidate這個方法就是重繪,重新走onDraw這個方法。當然這個方法只能在Ui線程中執行,在子線程中只能執行postInvalidate方法了,起到同樣的效果。
上面invalidate以及postInvalidate這兩個暫且稱控件的刷新的方法是在控件大小不變位置不變的前提下,如果大小和位置改變還得調用另一個方法requestLayout(),這個方法的作用就是調用measure過程和layout過程。
到此View繪制流程大致聊完了,下面我們來看一個小小的案例在代碼中熟悉一下:
我們來制作一個壁鐘(我小的時候幾乎每家每戶都有一個,掛在墻上,整點都會報時,估計現在都換成電子時鐘了):
下面我們來分析一下該怎么結合上面View的流程畫出這個鐘:
1.表是圓的,是不是要先畫一個圓出來呢。
2.表是有刻度的,那么怎么給這個圓上面畫出刻度來呢,其實也很簡單,我們先給12點畫一個刻度,出來,在設置或測量出控件的大小來后,通過getWidth()可以獲取空間的寬度,這樣就能得到12點的X坐標getWidth()/2,12點在整個控件的頂部,他的Y坐標顯而易見是0,這樣通過canvas.drawLine(),方法輕易就能畫出刻度來。
3.接下來我們再來分析,按小時總共有12小時,那就是,每兩個時間中間的角度是360/12=30°,這就好辦了,每畫完一次刻度,就讓畫布旋轉30就好了。這樣就可以畫出刻度來(分秒的刻度道理一樣的);
4.在接下來就是如何繪制,時分秒的指針了這個更簡單,起始點都是這個圓表盤的圓心,畫出不同的直線來。
5.最后就是讓時分秒跟著時間動態走了,其實也很簡單,就是不斷重回時分秒之間就可以了,invalidate();調用這個方法;
public class ClockView extends View {
private static final String TAG = ClockView.class.getSimpleName();
/**創建畫圓的畫筆*/
private Paint circlePaint;
/**創建畫圓的畫筆顏色*/
private int circleColor = Color.parseColor("#dddd89");
/**創建畫刻度的畫筆*/
private Paint scalePaint;
/**創建畫刻度的畫筆顏色*/
private int scaleColor = Color.parseColor("#000000");
/**創建畫時、分、秒的畫筆*/
private Paint mPaint;
/**距離圓內部邊緣的距離*/
private int padding = 5;
public ClockView(Context context) {
super(context);
initCirclePaint();
}
public ClockView(Context context, AttributeSet attrs) {
super(context, attrs);
initCirclePaint();
}
public ClockView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initCirclePaint();
}
private void initCirclePaint() {
circlePaint = new Paint();
circlePaint.setColor(circleColor);
circlePaint.setStyle(Paint.Style.STROKE); // 設置畫筆的樣式,STROKE為空心,FILL為實心
circlePaint.setStrokeWidth(5); // 設置空心的邊框寬度
circlePaint.setAntiAlias(true); // 設置畫筆無鋸齒
scalePaint = new Paint();
scalePaint.setStyle(Paint.Style.FILL);
scalePaint.setColor(scaleColor);
scalePaint.setStrokeWidth(5);
scalePaint.setAntiAlias(true);
mPaint = new Paint();
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
}
//--------------------------------- 接下來開始最重要的三個方法啦 ----------------------------------------------------
/**
* @param widthMeasureSpec 從父控件傳遞過來的寬度(包括模式和大小)
* @param heightMeasureSpec 從父控件傳遞過來的高度(包括模式和大小)
*
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(measureWidth(widthMeasureSpec),measureHeight(heightMeasureSpec));
}
/**
*
* @param measureSpec
* @return
*/
private int measureWidth(int measureSpec){
int result = 0;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
switch (mode){
case MeasureSpec.EXACTLY:
result = size;
break;
case MeasureSpec.AT_MOST:
result = getWidth()/2;
break;
case MeasureSpec.UNSPECIFIED:
result = 200;
break;
}
return result;
}
/**
*
* @param measureSpec
* @return
*/
private int measureHeight(int measureSpec){
int result = 0;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
switch (mode){
case MeasureSpec.EXACTLY:
result = size;
Log.e(TAG,"************EXACTLY*************");
break;
case MeasureSpec.AT_MOST:
result = getHeight()/4;
Log.e(TAG,"************AT_MOST*************");
break;
case MeasureSpec.UNSPECIFIED:
result = 200;
Log.e(TAG,"************UNSPECIFIED*************");
break;
}
return result;
}
//---------------------------------接下來是重頭戲OnDraw()方法的實現-------------------------------------------------
/**
* 通過這個方法可以畫出我們所需要的控件
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawCircle(canvas);
drawScale(canvas);
drawPointer(canvas);
}
/**畫出圓*/
private void drawCircle(Canvas canvas){
// 分別是X軸坐標,Y軸坐標,半徑,畫筆
canvas.drawCircle(getWidth()/2,getHeight()/2,getHeight()/2 - padding,circlePaint);
}
/**畫出刻度*/
private void drawScale(Canvas canvas){
/**
* startX:起始端點的X坐標。
* startY:起始端點的Y坐標。
* stopX:終止端點的X坐標。
* stopY:終止端點的Y坐標。
* paint:繪制直線所使用的畫筆。
*
* 至于為什么要這么設置坐標:
* 控件的寬度getWidth()除以2就可以獲取到12點的X坐標,
* 因為限定(到這一步知道)了控件的高度,12點的Y坐標本身應該為0,但是上面畫圓的時候畫筆的寬度為5,和padding是一樣的
* 并且畫的時候把畫筆的寬度減去了,所以這里設置是padding,是讓從遠的外邊距算起,有點繞,需要理解
*/
// canvas.drawLine(getWidth()/2,padding,getWidth()/2,padding + 14,scalePaint); // 這一步只是為了演示劃出的刻度
/**
* 既然可以畫出刻度,那么接下來我們又要分析, 表的刻度(這里只是計算是真的刻度,分針秒針原理一樣的)
* 時針的刻度在一個表盤里有12個,并且3、6、9、12點的時候刻度會略長
* 并且每畫出一個刻度的時候畫布應該旋轉360/12的角度,繼續畫下一個刻度
* 好了分析清楚后,我們就開始畫了
*/
for (int i = 0; i < 12; i++) {
if (i%3 == 0) { // 可以獲取到3的整數倍的點(3、6、9、12)
canvas.drawLine(getWidth()/2,padding,getWidth()/2,padding + 20,scalePaint);
}else{
canvas.drawLine(getWidth()/2,padding,getWidth()/2,padding + 14,scalePaint);
}
/**
* degrees 旋轉的角度
* px 以某個點來旋轉的點的x坐標
* py 以某個點來旋轉的點的y坐標
*/
canvas.rotate(30,getWidth()/2,getHeight()/2);
}
}
/**
* 接下來開始繪制時分秒的指針
* @param canvas 畫布
* 分析:
* 時分秒的指針都是直線canvas.drawLine();
* 時的指針旋轉角度為 360/12
* 分的指針旋轉的角度 360/120
* 秒的指針旋轉的角度 360/1200
*/
private void drawPointer(Canvas canvas){
Time t=new Time();
t.setToNow();
int hour = 1;
if (t.hour > 12) {
hour = t.hour - 12;
}else{
hour = t.hour;
}
int minute = t.minute;
int second = t.second;
// 旋轉的角度
float degrees = hour*30;
mPaint.setColor(Color.BLACK);
mPaint.setStrokeWidth(5);
canvas.save();
canvas.rotate(degrees,getWidth()/2,getHeight()/2);
canvas.drawLine(getWidth()/2,getHeight()/2,getWidth()/2,getHeight()/2 - 90,mPaint);
canvas.restore();
// 分
degrees = minute*3;
mPaint.setColor(Color.BLUE);
mPaint.setStrokeWidth(4);
canvas.save();
canvas.rotate(degrees,getWidth()/2,getHeight()/2);
canvas.drawLine(getWidth()/2,getHeight()/2,getWidth()/2,getHeight()/2 - 70,mPaint);
canvas.restore();
// 秒
degrees = (float) (minute*0.3);
mPaint.setColor(Color.BLUE);
mPaint.setStrokeWidth(3);
canvas.save();
canvas.rotate(degrees,getWidth()/2,getHeight()/2);
canvas.drawLine(getWidth()/2,getHeight()/2,getWidth()/2,getHeight()/2 - 40,mPaint);
canvas.restore();
invalidate();// view的刷新操作,重繪
}
}
到此結束。