View 的工作原理主要包含 View 的三大流程Measure、Layout和Draw,即測量、布局、繪制。
measure過程
View:
通過measure方法就完成了其測量。measure()是final的方法,子類無法重寫此方法,measure()中會調用View的onMeasure(widthMeasureSpec, heightMeasureSpec)方法并傳遞MeasureSpec給他,MeasureSpec在上一篇博文已經講過。
onMeasure()會執行
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
方法設置View寬/高的測量值
public static int getDefaultSize(int size, int measureSpec) {
int result = 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;
}
getDefaultSize()
作用是返回一個默認的值,如果MeasureSpec沒有強制限制的話則使用提供的大小,否則在允許范圍內可任意指定大小,第一個參數size為提供的默認大小,第二個參數為測量的大小。
直接繼承View的自定義控件需要重寫onMeasure方法并設置wrap_content時自身大小,否則在布局中使用就相當于使用match_parent。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);//都為最大模式時(wrap_content)
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, mHeight);
}
}
上面的代碼中給View指定了一個默認的內部寬/高,mWidth和mHeight,在wrap_content時設置默認的大小,非wrap_content時沿用系統的測量值。
ViewGroup:
與View的過程不同,除了完成自己的測量過程,還會遍歷去調用所有子元素的measure方法,各個子元素再遞歸去執行這個過程。ViewGroup是一個抽象類,沒有重寫View的onMeasure,不同的ViewGroup子類有不同的布局特性,這導致它們的測量細節各不相同。
layout過程
這個方法是用于給視圖進行布局的,也就是確定視圖的位置。ViewRoot的performTraversals()方法會在measure結束后繼續執行,并調用View的layout()方法來執行此過程。layout方法確定View本身位置,而onLayout方法會確定所有子元素的位置。
layout完成后可以通過getWidth和getHeight獲取最終寬高。
draw過程
draw的作用是將View繪制到屏幕上,其過程如下幾步:
-
drawBackground(canvas);
繪制背景 -
onDraw(canvas);
繪制自己本身 -
dispathDraw(canvas);
繪制children -
onDrawScrollBars(canvas);
繪制裝飾
dispathDraw會遍歷調用所有子元素的draw方法,如此draw事件就一層一層傳遞下去。
自定義View
自定義View分類:
- 繼承View重寫onDraw方法(需自己支持wrap_content、padding)
- 繼承特定的View(比如TextView等,不需要自己支持wrap_content、padding)
- 繼承特定的ViewGroup(如LinearLayout)
- 繼承ViewGroup派生特殊的Layout(比較復雜)
繼承View重寫onDraw
在布局文件使用自定義屬性:首先在values新建attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<attr name="circle_color" format="color"/>
</declare-styleable>
</resources>
在布局文件添加
xmlns:app="http://schemas.android.com/apk/res-auto"
繪制一個圓的過程:
public class CircleView extends View {
private int mColor = Color.BLACK;//圓的顏色
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);//定義畫筆
public CircleView(Context context) {
super(context);
}
public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);//加載自定義屬性
mColor = a.getColor(R.styleable.CircleView_circle_color, Color.BLACK);//默認值為藍色
a.recycle();//釋放資源
init();
}
private void init() {
mPaint.setColor(mColor);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, 200);//都為最大模式時(wrap_content)
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, 200);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
int radius = Math.min(width, height) / 2;
canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, radius, mPaint);
}
}