最近年末了,打算把本身的Android知識都整理一下。java
Android技能樹系列:android
Android基礎知識git
Android技能樹 — 動畫小結github
Android技能樹 — View事件體系小結canvas
Android技能樹 — Android存儲路徑及IO操做小結數組
數據結構基礎知識
算法基礎知識
此次是相對View作個小結,主要是View的工做原理,繪製流程等。爲何要總結這塊,由於平時自定義View的狀況多多少少都會遇到,若是能深入瞭解這塊知識,對自定義View的掌握才能更透徹。有些人可能會說那我確定不會的,我也不用看這個總結文章了,不要緊,我此次寫的很簡單,基本你們都能理解。看完後,你們應該都會本身寫效果不復雜的自定義View和自定義ViewGroup。
PS: 非廣告。我自己View的相關知識也是之前從其餘地方學到的。我比較推薦這塊內容看(Android開發藝術探索 和 扔物線的View相關內容。因此文中有些的知識點也會引用這二塊地方。)
以下圖所示:我主要是整理了這些相關知識:
咱們能夠看大分類:
咱們知道一個View要繪製好,是要有三步的(我估計百分之99.9的人都知道這三步): measure測量,layout肯定位置,而後draw畫出來。因此我此次也是主要這三步來講明的。而你們可能看到這裏有一個額外的ViewRoot的知識點,主要是給前面的三步作個補充知識。
ps:不看其實問題也不大,不想了解的直接看本文的主要的measure,layout,draw三步曲。
ViewRoot
字面意思是否是讓你感受是整個ViewTree的根節點。錯!ViewRoot不是View,它的實現類是ViewRootImpl
,它是DecorView
和WindowManager
之間的紐帶。因此ViewRoot
更恰當來講是DecorView
的「管理者」。
(PS:下次面試官問你ViewRoot是啥,你可別說是ViewTree的根節點。哈哈。)
因此這時候既然開始整個界面要繪製了。明顯就是ViewRoot開始發起調用方法,畢竟「管理者」麼。因此View的繪製流程是從ViewRoot
的performTraversals
方法開始的。因此performTraversals
方法依次調用performMeasure
,performLayout
和performDraw
三個方法。由於這三個方法及後面的方法調用都差很少,咱們以performMeasure
爲例,performMeasure
會調用measure
方法,而measure
方法又會調用onMeasure
方法(PS:是否是就發現了爲啥咱們平時都是重寫onMeasure
方法了。),而後又會在onMeasure
方法裏面去調用全部子View的measure
過程。
咱們能夠看到思惟腦圖中有提到頂級View就是DecorView
,那DecorView
是什麼呢? DecorView
是一個FrameLayout,裏面包含了一個豎向的LinearLayout
,通常來講這個LinearLayout是有上下二部分(這裏具體跟Android SDK和主題有關):
是否是看到了熟悉的Content這個名字,沒錯。咱們在Activity裏面設置佈局setContentView
就是把咱們的佈局加到這個id爲android.R.id.content
的FrameLayout
裏面。
咱們如今正式進入View整個繪製流程:
你們能夠看到,爲了方便你們理解,我寫了二個現實生活場景故事對比。
咱們能夠看到,咱們的氣球放到櫃子裏面,決定氣球大小的因素有二個:櫃子給它的限制,還有它自身的因素(質量好壞,好的能吹的很大)。而咱們的View也是同樣的,首先咱們用MeasureSpec來決定咱們的View大小,那咱們的MeasureSpec和睦球同樣,也受到二個因素的影響:
總結起來就是一句話:在測量過程當中,系統會將View的LayoutParams根據父容器ViewGroup所施加的規則下,轉換得出相對應的MeasureSpec,而後根據這個MeasureSpec來測量出View的高/寬。
可能你們會問什麼是MeasureSpec,別急,咱們立刻就來介紹
其實直接看腦圖,應該就能看得懂吧,主要是這麼幾個知識點:
沒錯,經過對比,咱們能夠發現規律原來很簡單。由於咱們腦子裏面能夠用這個氣球的對比故事更好的理解。
我作一個總結表格:(要理解上面的分析過程,而不是背下這個表格,背下來沒啥意思)
經過上面咱們已經知道MeasureSpec是用來肯定View的測量的,也已經能根據不一樣的狀況來得到相應的MeasureSpec了。那咱們的到底應該在哪裏去建立MeasureSpec呢?而後給子View去約束呢?
其實奧祕就在咱們平時重寫的onMeasure()
方法中:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
複製代碼
咱們是否是看到了onMeasure
方法裏面傳入了(int widthMeasureSpec, int heightMeasureSpec)
,沒錯,這裏傳入的二個參數,就是當前你重寫這個方法的所在的View(子View或者ViewGroup)的進行過一系列的操做最後得到的MeasureSpec。
那咱們拿到這二個參數後,View仍是不知道咱們到底給它的寬和高是多少。應該確定最後是咱們調用類型:view.setMeasureWidth(XX),view.setMeasureHeight(XX)
這樣,它才能被設置測量的寬和高。沒錯,setMeasuredDimension(int measuredWidth, int measuredHeight)
方法就是咱們用來設置view的測量寬和高。
固然你可能會問,那我若是直接調用這個方法來設置view的寬和高,那我感受我不用MeasureSpec
都不要緊啊。好比下面的代碼:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//沒有使用相應的MeasureSpec
setMeasuredDimension(100,100);
}
複製代碼
沒錯,咱們能夠不是經過正規的測量過程來決定測量的寬和高,咱們就是任性的直接定了寬高是100。可是這樣就不符合規則流程了。並且作出來的東西也不會特別好。好比這時候,你在xml中對你的view設置match_parent
,wrap_content
,200dp
就會都無效,由於代碼最後都是用了100。
咱們前面提過,自定義View是要重寫onMeasure()方法的,咱們再仔細分析下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//咱們通常會本身寫的代碼
........
........
.......
}
複製代碼
咱們能夠看到,主要分爲二塊:
咱們根據不一樣的狀況一步步來看這些代碼的做用。
super.onMeasure() 分析1 :好比咱們的自定義View直接繼承了View.java:
public class DemoView extends View {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
}
}
複製代碼
咱們能夠查看super.onMeasure
方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
);
}
複製代碼
咱們看到果真調用了setMeasureDimension方法來進行寬高的設置了。
PS:接下來的源碼這個分析能夠不看,直接看結論。嘿嘿。嘿嘿。我知道不少人都不想看。
咱們能夠看到主要是三個方法(咱們這裏就看width的測量):
1和2的方法先不看,咱們起碼知道了。咱們最終肯定一個View的測量大小,是經過setMeasuredDimension來設置的(其實我感受我說的廢話,看這個方法的名字就很明確了)。
咱們再回頭來看1中的方法:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
複製代碼
若是咱們的View沒有設置background,則返回的最小值爲mMinWidth(啥是mMinWidth?????就是咱們在xml設置的android:minWidth
的值)。若是咱們設置了background,則獲取mBackground.getMinimumWidth()
(其實這個方法就是返回Drawable的原始寬度)。最後返回max(mMinWidth, mBackground.getMinimumWidth())
兩者中的最大值。
咱們再來看2中的方法:
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;
}
複製代碼
其實上面咱們的MeasureSpec的建立規則會的話,其實應該就能看的懂。若是是specMode是UNSPECIFIED
,則返回咱們1中的方法getSuggestedMinimumWidth
獲取到的值,若是是AT_MOST
和EXACTLY
,則直接返回specSize。(View源碼這裏的寬度的建立規則和咱們前面講的測量的規則區別就在於,當specMode是UNSPECIFIED
的時候,返回的是getSuggestedMinimumWidth
的值,而咱們是返回了0。)
結論1:若是寫的自定義View是直接繼承View的,並且寫了super.measure(),則會默認給這個View設置了一個測量寬和高(這個寬高是多少?若是沒有設置背景,則是xml裏面設置的android:minWidth/minHeight(這個屬性默認值是0),若是有背景,則取背景Drawable的原始高寬值和android:minWidth/minHeight兩者中的較大者。)
super.onMeasure() 分析2 :好比咱們的自定義View繼承了現有的控件,好比ImageView.java:
public class Image2View extends ImageView {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
複製代碼
這時候咱們的super.onMeasure()
方法調用的就是ImageView
裏面的onMeasure
方法了:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//ImageView 的一大堆計算寬高的代碼。
......
......
......
//固然最終確定要把算好的寬高告訴View
setMeasuredDimension(widthSize, heightSize);
}
複製代碼
咱們發現若是咱們的View直接繼承ImageView,ImageView已經運行了一大堆已經寫好的代碼測出了相應的寬高。咱們能夠在它基礎上更改便可。
好比咱們的Image2View是一個自定義的正方形的ImageView,:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//這裏已經幫咱們測好了ImageView的規則下的寬高,而且經過了setMeasuredDimension方法賦值進去了。
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//咱們這裏經過getMeasuredWidth/Height放來獲取已經賦值過的測量的寬和高
//而後在ImageView幫咱們測量好的寬高中,取小的值做爲正方形的邊。
//而後從新調用setMeasuredDimension賦值進去覆蓋ImageView的賦值。
//咱們從頭到位都沒有進行復雜測量的操做,全靠ImageView。哈哈
int width = getMeasuredWidth();
int height = getMeasuredHeight();
if (width < height) {
setMeasuredDimension(width, width);
} else {
setMeasuredDimension(height, height);
}
}
複製代碼
結論2:若是寫的自定義View是繼承現有控件的,並且寫了super.measure(),則會默認使用那個現有控件的測量寬高,你能夠在這個已經測量好的寬高上作修改,固然也能夠所有從新測過再改掉。
super.onMeasure() 分析3:咱們寫的本身的代碼與super.measure的先後位置關係
咱們能夠看到,無論你是繼承View仍是現有的控件(好比ImageView),super.onMeasure()
中都默認會按照本身的邏輯測量一個寬和高,而後調用setMeasuredDimension()
方法賦值進去。
setMeasuredDimension()
賦值。public class Image2View extends ImageView {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//這裏的super.onMeasure()方法裏面,已是調用了ImageView的onMeasure()方法。
//因此已經進行了測量了。而且在這個方法最後調用了setMeasuredDimension(widthSize, heightSize);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//因此你不寫任何東西,這個測量結果都已經肯定過了,由於已經執行過了setMeasuredDimension。
//但好比你想要在ImageView的基礎上,讓這個ImageView變成一個正方形的ImageView。
//由於測出來的寬高可能不一樣,是一個矩形。咱們就須要手動的再去設置一次寬和高。
int width = getMeasuredWidth();//獲取ImageView源碼裏面已經測量好的寬度
int height = getMaxHeight();//獲取ImageView源碼裏面已經測量好的高度
if (width < height) {
setMeasuredDimension(width, width);
} else {
setMeasuredDimension(height, height);
}
}
}
複製代碼
咱們發現,咱們是在已經咱們繼承的現有的控件幫咱們測量好寬高後,能夠再次在這個已經測量好的寬高的基礎上進行更改。咱們並無用到咱們前面學到的MeasureSpec的知識,由於super.onMeasure()中已經幫咱們把MeasureSpec處理好了。
public class CircleView extends View {
public CircleView(Context context) {
super(context);
}
public CircleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//View測量寬高的三步曲
//1.設置默認值,wrap_content的狀況下的值。
//由於wrap_content只是說不超過某個最大值,若是不設置默認值,效果與Match_parent同樣了。
int defaultWidthSize = 200;
int defaultHeightSize = 200;
//2.調用resolveSize()方法,把MeasureSpec和咱們的默認值放進去
//這個方法返回一個最終根據你傳入的默認值及MeasureSpec共同做用後的最終結果
defaultWidthSize = resolveSize(defaultWidthSize, widthMeasureSpec);
defaultHeightSize = resolveSize(defaultHeightSize, heightMeasureSpec);
//調用setMeasuredDimension方法賦值寬和高
setMeasuredDimension(defaultWidthSize, defaultHeightSize);
}
}
複製代碼
是否是超級超級超級簡單。你們可能就會問,那個resolveSize()
方法是什麼,怎麼這麼神奇。
PS:下面的resolveSize()源碼分析不看也沒啥關係,反正會用就好了。哈哈,不影響使用。
咱們能夠來看下它的源碼:
public static int resolveSize(int size, int measureSpec) {
return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
}
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
//1.拿到specMode 和 specSize
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
//2.根據不一樣的specMode來進行判斷最終值是什麼
switch (specMode) {
case MeasureSpec.AT_MOST:
/*
2.1若是specMode是AT_MOST模式,咱們原本應該直接是specSize
可是若是咱們的默認值比咱們的specSize大就很尷尬了。氣球默認的大小都裝不進櫃子了。這時候咱們View的大小要設置成specSize,若是默認大小比咱們的specSize小就不要緊,直接爲默認值。
*/
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
/*
2.2若是是EXACTLY,直接就是specSize
*/
case MeasureSpec.EXACTLY:
result = specSize;
break;
/*
2.3若是是UNSPECIFIED模式,則直接就是咱們設的默認值
*/
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
複製代碼
在講ViewGroup的測量前面,我要提問個問題,你們應該知道了某個View的MeasureSpec在是在onMeasure()方法的參數裏面傳進來的。咱們是直接拿來用了。那又是那裏調用了onMeasure()方法幫忙把這二個參數帶進來的呢。這二個參數又是哪裏生成的呢?
答案就是這個子View的父容器給它的。父容器在他本身的onMeasure()方法裏面會根據本身的onMeasure()傳進來的MeasureSpec,及這個子View的自身的LayoutParams狀況,生成相應的childMeasureSpec,而後調用子View的measure()傳遞進去的(前面提過,measure()方法會調用onMeasure()方法。)
好比咱們寫一個圓形排布的ViewGroup(LinearLayout是一排的排布)。
public class CircleLayout extends ViewGroup {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//1.父容器的onMeasure()傳進來的二個參數widthMeasureSpec和 heightMeasureSpec
//2.還差子View的LayoutParams,獲取子View的LayoutParams
//3.經過兩者產生新的MeasureSpec而後給子View。
//4.而產生的新的ChildMeasureSpec的規則就是咱們前面表格總結過的規則。
/*
PS:下面這段是我寫的代碼,並非正確的,由於父容器可能包含多個子View,
因此到某個子View的時候,給它的specSize應該是父容器的剩餘空間,
因此傳入的父容器的可用空間原本是不停的減小的,外加還有margin,padding值也要減去。
我就是主要意思下,讓你們懂得原理。
*/
//先判斷初始時候父容器的大小,由於父容器也是個View,因此也是三步曲。
//設置默認值(能夠是0,由於父容器通常默認不會佔有空間)
int defaultWidthSize = 500;
int defaultHeightSize = 500;
//resolveSize處理獲取寬和高
int resultWidthSize = resolveSize(defaultWidthSize, widthMeasureSpec);
int resultHeightSize = resolveSize(defaultHeightSize, heightMeasureSpec);
//好比咱們這裏以width爲例子:
//咱們前面提過了,最終給子View的MeasureSpec是由父View的MeasureSpec與子View的LayoutParam共同肯定。
//先獲取父View的MeasureSpec的mode和size
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);
//根據不一樣的SpecMode及子View的LayoutParams來產生新的ChildMeasureSpec。
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
LayoutParams params = view.getLayoutParams();
int childWidthSpec, childHeightSpec;
//先根據父View的MeasureSpec來進行大分類:
switch (specMode) {
case MeasureSpec.EXACTLY:
//說明是固定值,好比100dp等
if (params.width >= 0) {
resultSize = params.width;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = specSize;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = specSize;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST:
.....
.....
break;
case MeasureSpec.UNSPECIFIED:
.....
.....
break;
}
childWidthSpec = MeasureSpec.makeMeasureSpec(resultWidthSize, MeasureSpec.EXACTLY);
getChildAt(i).measure(childWidthSpec, childHeightSpec);
}
/*
可能有人說,生成新的規則我都懂,可是每次都要寫上面一大段的代碼,
我不想寫自定義ViewGroup了。我仍是放棄吧,別急,你們也發現上面的規則的確是固定的。
那有沒有相似咱們在上面設置本身寬高時候的相似resolveSize的方法呢。
若是沒有特定的需求,的確咱們不須要寫上面一大段。
有二種方法。
*/
//方法1:能夠經過調用measureChildren()一會兒把全部的子View測量好
measureChildren(widthMeasureSpec, heightMeasureSpec);
//方法2:經過measureChild()一個個來測量。
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
measureChild(view , widthMeasureSpec,heightMeasureSpec);
}
//設置父容器的大小
setMeasuredDimension(XXXX,XXXX)
}
}
複製代碼
沒錯,最後咱們能夠用measureChildren(widthMeasureSpec, heightMeasureSpec);
和measureChild(view , widthMeasureSpec,heightMeasureSpec);
方法來,咱們也知道它的內部確定也是根據相應的規則,生成對應的childMeasureSpec,而後調用child的measure方法。
咱們能夠看下源碼(PS:不想看仍是不要緊,能夠跳過):
//measureChildren其實只是幫咱們遍歷了全部的View,幫咱們把可見的View分別調用measureChild方法來處理。
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
//而measureChild方法裏面就是獲取子View的LayoutParams和傳進來的MeasureSpec,
//把這兩者經過getChildMeasureSpec方法得到一個新的childMeasureSpec,而後傳給child.measure方法。
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製代碼
若是具體想看getChildMeasureSpec
作了什麼,可有再去看下源碼,可是他們生成的規則跟咱們前面講的仍是同樣的。我這裏很少說了。
這個就十分簡單了。直接看腦圖便可。
這塊比較簡單,我也很少說了。(別吐槽我,這文章太多了。寫太多沒人會耐心看完。)
咱們都知道View的大小和位置都肯定好了,確定就差繪畫了。
咱們都知道是經過draw()方法來繪製的。
而draw()方法具體作了什麼呢,咱們能夠看源碼這個方法的工做過程的介紹:
draw()源碼裏面的介紹:
/*
* 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)
*/
複製代碼
分別是先繪製背景,而後繪製本身的內容,而後繪製子View的內容,最後畫裝飾和前景。
推薦你們看扔物線大佬的文章,講的很清楚,我就不花大篇幅寫基礎了。
HenCoder Android 自定義 View 1-5: 繪製順序
咱們知道不論是onDraw(Canvas canvas)
,dispatchDraw(Canvas canvas)
,onDrawForeground(Canvas canvas)
等都是參數是Canvas(畫布)。因此咱們知道了是用Canvas來繪畫。
這裏也是推薦扔物線大佬的相關文章,講的很細,我也再也不大篇幅的寫各類基礎使用知識。
HenCoder Android 開發進階: 自定義 View 1-1 繪製基礎
HenCoder Android 開發進階:自定義 View 1-4 Canvas 對繪製的輔助
Canvas怎麼使用呢: 主要分爲二大塊:
其中幾何變化又分爲二維變換和三維變換:
咱們知道Paint是畫筆,咱們能夠設置顏色,畫筆粗細等。
繼續推薦扔物線大佬的相關文章(基礎我就不寫了):
HenCoder Android 開發進階: 自定義 View 1-2 Paint 詳解
有錯誤的地方,請你們輕點噴,我膽子很小的。。。。