問:自定義View有幾個構(gòu)造函數(shù),及自定義View的主要流程
答:自定義View中共有四個構(gòu)造函數(shù),一般只需要實現(xiàn)一個參數(shù)及兩個參數(shù)的構(gòu)造函數(shù)即可。自定義View過程中,主要流程有:measure、layout、draw即 測量、布局、繪制,這里面涉及到MeasureSpec、Paint、Canvas、Path等很多重要類。
自定義View的實現(xiàn)方式有很多:自定義組合控件、繼承系統(tǒng)View 如繼承TextView、繼承系統(tǒng)ViewGroup 如繼承LinearLayout、繼承View、繼承ViewGroup等。
自定義View的四個構(gòu)造方法:
class MyView : View {
/**
* 在java代碼里new的時候會用到
*/
constructor(context: Context?) : super(context) {}
/**
* 在xml布局文件中使用時自動調(diào)用
*/
constructor(context: Context?, @Nullable attrs: AttributeSet?) : super(context, attrs) {}
/**
* 不會自動調(diào)用,如果有默認(rèn)style時,在第二個構(gòu)造函數(shù)中調(diào)用
*/
constructor(context: Context?, @Nullable attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {}
/**
* 只有在API版本>21時才會用到
* 不會自動調(diào)用,如果有默認(rèn)style時,在第二個構(gòu)造函數(shù)中調(diào)用
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
constructor(context: Context?, @Nullable attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
}
}
自定義View的三個主要流程:measure、layout、draw
1、Measure測量流程,從View的measure方法為入口,該方法只是做了一些初始化,之后調(diào)用onMeasure方法。來看onMeasure()方法:
//View的onMeasure源碼
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
首先看到的是onMeasure()方法要求傳入兩個int類型的參數(shù),分別是寬高。這里需要了解一下 MeasureSpec 是什么東西。
MeasureSpec是View的內(nèi)部類,值保存在一個int值當(dāng)中,一個int有32位,前兩位是 mode(模式),后30位是 size(大小) 即:MeasureSpec = mode + size
其中mode的值有三種,UNSPECIFIED,EXACTLY、AT_MOST,
| 模式 | 意義 | 對應(yīng) |
|---|---|---|
| EXACTLY | 精準(zhǔn)模式,View需要一個精確值,這個值即為MeasureSpec當(dāng)中的Size | match_parent |
| AT_MOST | 最大模式,View的尺寸有一個最大值,View不可以超過MeasureSpec當(dāng)中的Size值 | wrap_content |
| UNSPECIFIED | 無限制,View對尺寸沒有任何限制,View設(shè)置為多大就應(yīng)當(dāng)為多大 | 不怎么用 |
在ViewGroup中的 MeasureSpec測量源碼如下:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//獲取測量模式
int specMode = MeasureSpec.getMode(spec);
//獲取測量大小
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// 當(dāng)父View要求一個精確值時,為子View賦值
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
//如果子view有自己的尺寸,則使用自己的尺寸
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//當(dāng)子View是match_parent,將父View的大小賦值給子View
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 如果子View是wrap_content,設(shè)置子View的最大尺寸為父View
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父布局給子View一個最大界限
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// 如果子view有自己的尺寸,則使用自己的尺寸
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 當(dāng)子View是match_parent,父View的尺寸為子View的最大尺寸
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 如果子View是wrap_content,父View的尺寸為子View的最大尺寸
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父布局對子View沒有做任何限制
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
//如果子view有自己的尺寸,則使用自己的尺寸
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//因父布局沒有對子View做出限制,當(dāng)子View為MATCH_PARENT時則大小為0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//因父布局沒有對子View做出限制,當(dāng)子View為WRAP_CONTENT時則大小為0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//通過Mode 和 Size 生成新的SpecMode 返回
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
注:這里只是給子View設(shè)置了MeasureSpec參數(shù),真正的大小是在View中具體設(shè)置的,只是給子View做了一個限制。子View的測量模式由自身的LayoutParams和父View的MeasureSpec來決定。
回過頭來看View.onMeasure()方法:就一行代碼,但是有三個函數(shù):
setMeasuredDimension(int measuredWidth, int measuredHeight) :該方法用來設(shè)置View的寬高
getDefaultSize(int size, int measureSpec): 該方法用來獲取View默認(rèn)的寬高
getSuggestedMinimumWidth(): 該方法用于獲取Android:minWidth屬性的值,如果沒有則為0(如果有背景還需要判斷背景與mMinWidth的大小,取大值)
這里面其實最重要的是getDefaultSize方法,對于AT_MOST和EXACTLY在View當(dāng)中的處理是完全相同的,在我們自定義View時要對這兩種模式做出處理。
/**
* 有兩個參數(shù)size和measureSpec
* 1、size表示View的默認(rèn)大小,它的值是通過`getSuggestedMinimumWidth()方法來獲取的,之后我們再分析。
* 2、measureSpec則是我們之前分析的MeasureSpec,里面存儲了View的測量值以及測量模式
*/
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
//從這里我們看出,對于AT_MOST和EXACTLY在View當(dāng)中的處理是完全相同的。所以在我們自定義View時要對這兩種模式做出處理。
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
ViewGroup除了測量自身,還需要測量子View的大小,ViewGroup中提供了對子View的測量方法:measureChildren(),在measureChildren中遍歷所有子View,調(diào)用measureChild(),在measureChild中調(diào)用了View的measure()方法,讓子View測量自身大小。
2、layout布局流程,layout()過程,對于View來說用來計算View的位置參數(shù),對于ViewGroup來說,除了要測量自身位置,還需要測量子View的位置。其實layout最重要的在自定義ViewGroup時的重寫,對其子類進(jìn)行布局。
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//onLayout方法是一個空實現(xiàn),我們在自定義View的時候關(guān)注重新這個onLayout方法即可,可以看看LinearLayout的onLayout方法
onLayout(changed, l, t, r, b);
//省略很多代碼....
}
}
3、draw繪制流程:draw繪制流程就是繪制View的過程,整個過程可以分為6個步驟:
1.如果需要,繪制背景
2.如果有必要,保存當(dāng)前canvas
3.繪制View的內(nèi)容
4.繪制子View
5.如果有必要,繪制邊緣,陰影等
6.繪制裝飾,如滾動條等
public void draw(Canvas canvas) {
int saveCount;
// 1. 如果需要,繪制背景
if (!dirtyOpaque) {
drawBackground(canvas);
}
// 2. 如果有必要,保存當(dāng)前canvas。
final int viewFlags = mViewFlags;
if (!verticalEdges && !horizontalEdges) {
// 3. 繪制View的內(nèi)容。
if (!dirtyOpaque) onDraw(canvas);
// 4. 繪制子View。
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// 如果有必要,繪制邊緣,陰影等
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// 6. 繪制裝飾,如滾動條等等。
onDrawForeground(canvas);
// we're done...
return;
}
}
/**
* 1.繪制View背景
*/
private void drawBackground(Canvas canvas) {
//獲取背景
final Drawable background = mBackground;
if (background == null) {
return;
}
setBackgroundBounds();
//獲取便宜值scrollX和scrollY,如果scrollX和scrollY都不等于0,則會在平移后的canvas上面繪制背景。
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
/**
* 3.繪制View的內(nèi)容,該方法是一個空的實現(xiàn),在各個業(yè)務(wù)當(dāng)中自行處理。
*/
protected void onDraw(Canvas canvas) {
}
/**
* 4. 繪制子View。該方法在View當(dāng)中是一個空的實現(xiàn),在各個業(yè)務(wù)當(dāng)中自行處理。
* 在ViewGroup當(dāng)中對dispatchDraw方法做了實現(xiàn),主要是遍歷子View,并調(diào)用子類的draw方法,一般我們不需要自己重寫該方法。
*/
protected void dispatchDraw(Canvas canvas) {
}
4、Paint、Canvas、Path等相關(guān)內(nèi)容可以看看: 扔物線的自定義View