前言
我們經常使用TextView、button等控件,但是有些同學對於它們是如何顯示和擴展的卻並不那麼熟悉。而這一塊的知識也進階高手必備的,寫這一篇文章是想把view繪製這塊的技術全面總結一下。我們知道,,Activity作爲應用程序的載體負責向用戶展現界面並提供了窗口進行視圖繪製。
Android View繪製及事件(二)setContentView()源碼,LayoutInflater加載View的過程
上一篇講解了,當調用 Activity 的setContentView 方法後會調用PhoneWindow 類的setContentView方法,最終會生成一個繼承FrameLayout的PhoneWindow的內部類DecorView對象。DecorView容器中包含根佈局,通過findViewById()找到一個id爲content的FrameLayout的根佈局,Activity加載佈局的xml最後通過 LayoutInflater.inflate() 將xml文件中的內容解析成View層級體系,最後填加到id爲content的FrameLayout根佈局中。LayoutInflater.inflate() 會調用 createViewFromTag解析該元素拿到View類型的temp對象實例,再調用rInflate採用遞歸解析temp中的子View,並將這些子View添加到temp中。
好了,上面大致認識了一下View被加載顯示的原理,那麼接下來一起看看View類內部是什麼樣子的?
一、自定義View介紹
1.1 實現方式:
類型 | 定義 |
---|---|
自定義組合控件 | 多個控件組合成爲一個新的控件,方便多處複用 |
繼承系統控件 | 繼承自TextView等系統控件,在系統控件的基礎功能上進行擴展 |
繼承View、ViewGroup | 不復用系統控件邏輯,繼承View、ViewGroup進行功能定義 |
查看本篇,代碼實例教程:Android View繪製及事件(四)自定義組合控件+約束佈局ConstraintLayout+自定義控件屬性
1.2 構造函數:
繼承系統View或直接繼承View,都需要對構造函數進行重寫,構造函數有多個,區別在於方法參數:
public class TestView extends View {
/**
* 在java代碼裏new的時候會用到
* @param context
*/
public TestView(Context context) {
super(context);
}
/**
* 在xml佈局文件中使用時自動調用
* @param context
*/
public TestView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
/**
* 不會自動調用,如果有默認style時,在第二個構造函數中調用
* @param context
* @param attrs
* @param defStyleAttr
*/
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* 只有在API版本>21時纔會用到
* 不會自動調用,如果有默認style時,在第二個構造函數中調用
* @param context
* @param attrs
* @param defStyleAttr
* @param defStyleRes
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
}
1.3 繪製流程
函數 | 定義 |
---|---|
measure() | 測量View的寬高 |
layout() | 計算當前View以及子View位置 |
draw() | 繪製視圖的工作 |
1.4 View的屏幕座標
函數 | 定義 |
---|---|
getTop() | 獲取View到其父佈局頂邊的距離 |
getLeft() | 獲取View到其父佈局左邊的距離 |
getBottom() | 獲取View到其父佈局底邊的距離 |
getRight() |
獲取View到其父佈局右邊的距離 |
二、View繪製流程
2.1 measure()
ViewGroup的測量過程與View有一點點區別,其本身是繼承自View。ViewGroup除了要測量自身寬高外還需要測量各個子View的大小,因此它提供了測量子View的方法measureChildren()以及measureChild()幫助我們對子View進行測量。measureChildren()以及measureChild()的源碼這裏不再分析,大致流程就是遍歷所有的子View,然後調用View的measure()方法,讓子View測量自身大小。
1、 onMeasure()方法
測量視圖大小,整個測量過程的入口位於View的measure方法當中,measure方法又回調OnMeasure。從頂層父View到子View遞歸調用measure()方法。measure()方法當中做了一些參數的初始化之後調用了onMeasure方法。
源碼
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//該方法用來設置View的寬高,在我們自定義View時也會經常用到。
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
//該方法用來獲取View默認的寬高,結合源碼來看。
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
先看getDefautSize()方法的參數 getSuggestedMinimumHeight() 的源碼:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
- 當View沒有設置背景時,默認大小就是mMinWidth,這個值對應Android:minWidth屬性,如果沒有設置時默認爲0.
- 如果有設置背景,則默認大小爲mMinWidth和mBackground.getMinimumWidth()當中的較大值。
再看getDefaultSize(int size , int measureSpec) 源碼:
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當中的處理是完全相同的。所以在我們自定義View時要對這兩種模式做出處理。
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
有兩個參數size和measureSpec
- size表示View的默認大小,它的值是通過參數getSuggestedMinimumWidth()方法來獲取的.
- measureSpec裏面存儲了View的測量值以及測量模式.
2、 MeasureSpec類
在調用onMeasure()時,會根據MeasureSpec類的封裝View尺寸的值來確定View的寬高,MeasureSpec = mode+size ,mode三種類型:UNSPECIFIED、EXACTLY 和AT_MOST。如下:
父mode | 作用 | 對應子View |
---|---|---|
EXACTLY | 精準模式,View需要一個精確值,這個值即爲MeasureSpec當中的Size. | 父佈局沒有做出限制,子View有自己的尺寸,則使用,如果沒有則爲0. |
AT_MOST | 最大模式,View尺寸有一個最大值,不可以超過MeasureSpec當中的Size值. | 父佈局採用精準模式,有確切的大小,如果有大小則直接使用,如果子View沒有大小,子View不得超出父view的大小範圍. |
UNSPECIFIED | 無限制,View對尺寸沒有任何限制,View設置爲多大就應當爲多大. | 父佈局採用最大模式,存在確切的大小,如果有大小則直接使用,如果子View沒有大小,子View不得超出父view的大小範圍. |
在View當中,MeasureSpece的測量代碼:(爲子View設置MeasureSpec參數,子View大小需要在View中具體設置)
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) {
//當父View要求一個精確值時,爲子View賦值
case MeasureSpec.EXACTLY:
//如果子view有自己的尺寸,則使用自己的尺寸
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
//當子View是match_parent,將父View的大小賦值給子View
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
//如果子View是wrap_content,設置子View的最大尺寸爲父View
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
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) {
// 父View的尺寸爲子View的最大尺寸
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.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做出限制,當子View爲MATCH_PARENT時則大小爲0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//因父佈局沒有對子View做出限制,當子View爲WRAP_CONTENT時則大小爲0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
子View的測量模式是由自身LayoutParam和父View的MeasureSpec來決定的!
2.2 Layout()
對於View來說用來計算View的位置參數,進行頁面佈局。對於ViewGroup來說,除了要測量自身位置,還需要測量子View的位置,即從頂層父View向子View的遞歸調用view.layout()方法的過程,父View根據上一步measure子View所得到的佈局大小和佈局參數,將子View放在合適的位置上。
源碼:
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;
//這裏通過setFrame或setOpticalFrame方法確定View在父容器當中的位置。
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//調用onLayout方法。onLayout方法是一個空實現,不同的佈局會有不同的實現。
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
}
}
小結
- 在layout()方法中的四個參數l、t、r、b分別代表View的左、上、右、下四個邊界相對於其父View的距離。
- 在layout()方法中通過setOpticalFrame(l, t, r, b)或 setFrame(l, t, r, b)方法對View自身的位置進行了設置,所以onLayout(changed, l, t, r, b)方法主要是ViewGroup對子View的位置進行計算。
2.2 Draw()
繪製視圖。draw過程也就是View繪製到屏幕上的過程。ViewRoot創建一個Canvas對象,然後調用OnDraw()。整個過程可以分爲6個步驟。
- 繪製背景。
- 保存canvas畫布的圖層。
- 繪製View的內容。
- 繪製子View。
- 繪製邊緣、陰影等效果。
- 繪製前景,如滾動條。
源碼:
public void draw(Canvas canvas) {
int saveCount;
// 1. 如果需要,繪製背景
if (!dirtyOpaque) {
drawBackground(canvas);
}
// 2. 如果有必要,保存當前canvas。
final int viewFlags = mViewFlags;
if (!verticalEdges && !horizontalEdges) {
// 3. 繪製View的內容。
if (!dirtyOpaque) onDraw(canvas);
// 4. 繪製子View。
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// 覆蓋是內容的一部分,在前景下繪製
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// 6. 繪製前景,如滾動條等等。
onDrawForeground(canvas);
return;
}
}
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);
}
}
在onDraw(Canvas canvas)方法中,可以繪製圖片,通過使用canvas、paint、matrix等。
參考鏈接https://www.jianshu.com/p/705a6cb6bfee
注意
一、invalidate()和requestLayout()
invalidate()和requestLayout(),常用於View重繪和更新,其主要區別如下
- invalidate方法只會執行onDraw方法
所以,當我們進行View更新時,若僅View的顯示內容發生改變且新顯示內容不影響View的大小、位置,則只需調用invalidate方法。
- requestLayout方法只會執行onMeasure方法和onLayout方法,並不會執行onDraw方法。
如果,View的寬高及位置發生改變且顯示內容不變,只需調用requestLayout方法;若兩者均發生改變,則需調用兩者,按照View的繪製流程,推薦先調用requestLayout方法再調用invalidate方法
二、invalidate()和postInvalidate()
- invalidate方法用於UI線程中重新繪製視圖
- postInvalidate方法用於非UI線程中重新繪製視圖,省去使用handler