【Android】自定義控件之View原理與使用
一、簡介
不論在學習Android還是在做Android開發,我們都離不開View,所以學好View對一個Android開發人員來說尤為重要。Android中的每個控件都會在界面上得到一塊矩形的區域,而在Android中,控件大致被分為 兩類 ,即 ViewGroup 控件和 View 控件。ViewGroup控件作為父控件可以包含多個View控件,并管理其包含的View控件。下面分條來對View做一個簡單的介紹。
二、View的原理介紹
- View表示的的屏幕上的某一塊矩形的區域,而且所有的View都是矩形的;
- 如同簡介中介紹,View是不能添加子View的,而ViewGroup是可以添加子View的。ViewGroup之所以能夠添加子View,是因為它實現了兩個接口: ViewParent 和 ViewManager ;
- Activity之所以能加載并且控制View,是因為它包含了一個Window,所有的圖形化界面都是由View顯示的而Service之所以稱之為沒有界面的activity是因為它不包含有Window,不能夠加載View;
- 一個View有且只能有一個父View;
- 在Android中Window對象通常由PhoneWindow來實現的,PhoneWindow將一個DecorView設置為整個應用窗口的根View,即 DecorView為整個Window界面的最頂層View 。也可以說DecorView將要顯示的具體內容呈現在了PhoneWindow上;
- DecorView是FrameLayout的子類,它繼承了FrameLayout,即頂層的FrameLayout的實現類是Decorview,它是在phoneWindow里面創建的;
- 頂層的FrameLayout的父view是Handler,Handler的作用除了線程之間的通訊以外,還可以跟WindowManagerService進行通訊;
- windowManagerService是后臺的一個服務,它控制并且管理者屏幕;
- 一個應用可以有很多個window,其由windowManager來管理,而windowManager又由windowManagerService來管理;
- 如果想要顯示一個view那么他所要經歷三個方法: 1.測量measure , 2.布局layout , 3.繪制draw 。
三、View的測量/布局/繪制過程
顯示一個View主要進過以下三個步驟:
- 1、Measure測量一個View的大小
- 2、Layout擺放一個View的位置
- 3、Draw畫出View的顯示內容
其中measure和layout方法都是final的,無法重寫,雖然draw不是final的,但是也不建議重寫該方法。
這三個方法都已經寫好了View的邏輯,如果我們想實現自身的邏輯,而又不破壞View的工作流程,可以重寫onMeasure、onLayout、onDraw方法。下面來一一介紹這三個方法。
Paste_Image.png
3.1 View的測量
Android系統在繪制View之前,必須對View進行測量,即告訴系統該畫一個多大的View,這個過程在onMeasure()方法中進行。測量過程如下圖所示:
Paste_Image.png
3.1.1 MeasureSpec類
Android系統給我們提供了一個設計小而強的工具類——— MeasureSpec類
1、MeasureSpe描述了父View對子View大小的期望。里面包含了測量模式和大小。
2、MeasureSpe類把測量模式和大小組合到一個32位的int型的數值中,其中高2位表示模式,低30位表示大小而在計算中使用位運算的原因是為了提高并優化效率。
3、我們可以通過以下方式從MeasureSpec中提取模式和大小,該方法內部是采用位移計算。
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
也可以通過MeasureSpec的靜態方法把大小和模式合成,該方法內部只是簡單的相加。
MeasureSpec.makeMeasureSpec(specSize,specMode);
3.1.2 測量模式
在對View進行測量時,Android提供了三種測量模式:
-
1. EXACTLY
即 精確值模式 ,當控件的layout_width屬性或layout_height屬性指定為具體數值時,例如android:layout_width="100dp",或者指定為match_parent屬性時,系統使用的是EXACTLY 模式。
-
2. AT_MOST
即 最大值模式 ,當控件的layout_width屬性或layout_height屬性指定為warp_content時,控件大小一般隨著控件的子控件或者內容的變化而變化,此時控件的尺寸只要不超過父控件允許的最大尺寸即可。
-
3.UNSPECIFIED
這個屬性很奇怪,因為它不指定其大小測量的模式,View想多大就多大,通常情況下在繪制自定義View時才會使用。
View默認的onMeasure()方法只支持EXACTLY模式,所以如果在自定義控件的時候不重寫onMeasure()方法的話,就只能使用EXACTLY模式,且控件只可以響應你指定的具體寬高值或者是match_parent屬性。如果要讓自定義的View支持wrap_content屬性,那么就必須重寫onMeasure()方法來指定wrap_content時的大小。
而通過上面介紹的MeasureSpec這個類,我們就可以獲取View的測量模式和View想要繪制的大小。
3.1.3 MeasureSpec判定規則
在自定義View的時候要通過判斷測量的模式,給出不同的測量值,下面的一張圖表羅列了 MeasureSpec判定規則。
Paste_Image.png
3.1.4 實例演示
step1 :自定義一個類繼承FrameLayout重寫構造方法:
public class CustomView extends FrameLayout {
//構造方法省略...
}
step2 :重寫onMeasure()方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
通過查看super.onMeasure()方法,發現系統最終會調用setMeasureDimension(int measuredWidth, int measuredHeight)方法將測量后的寬高設置進去,從而完成測量工作。所以接下來要做的就是將最終測量后的寬高值作為參數設置給setMeasureDimension()方法,即重寫的onMeasure()方法代碼如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasureDimension(
measureWidth(widthMeasureSpec),
measureHeigth(heightMeasureSpec));
}
因為在上面我們調用了自定義的measureWidth()方法和measureHeight()方法對寬高進行了重新定義,接下來我們就來自定義測量值。
step3 :自定義測量值:
這里以measureWidth()方法為例,來進行自定義測量值操作。
首先,從MeasureSpec對象中獲取到測量模式和測量大小值:
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);
其次,通過判斷測量模式,給出不同的測量值:
①當specMode為EXACTLY時,直接使用指定的specSize即可;
②當specMode為其他兩種模式時,需要給它一個默認的大小。
注: 如果指定的是wrap_content屬性,即AT_MOST模式,則需要取出我們指定的大小與specSize中 最小 的一個來作為最后的測量值。參考代碼如下:
private int measureWidth(int measureSpec) {
int width = 0;
/**
* 1、從MeasureSpec對象中提出出具體的測量模式和大小
*/
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);
/**
* 2、通過判斷測量模式,給出不同的測量值
*/
if (specMode == MeasureSpec.EXACTLY) { // match_parent , accurate
width = specSize;
} else {
width = 200; //給一個默認的大小
if (specMode == MeasureSpec.AT_MOST) { // wrap_content
width = Math.min(width,specSize); //注意取兩者之間小的值
}
}
return width;
}</code></pre>
對于measureHeight()方法基本上與上面的measureWidth()方法一致,此處省略。
通過以上三個步驟即可搞定View的測量,接下來簡單介紹一下布局。
3.1.5 拓展
如果想在activity中的onCreat()方法中獲取控件測量以后的寬跟高,那么可以用下面的方法:
final TextView tv = (TextView) findViewById(R.id.tv);
tv.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
int measuredWidth = tv.getMeasuredWidth();
int measuredHeight = tv.getMeasuredHeight();
}
});
3.2 View的布局
首先我們來看一下layout布局流程圖:

Paste_Image.png
接下來一一介紹上面流程圖中的三個參數:
-
layout
①Layout方法中接受四個參數,是由父View提供,指定了子View在父View中的左、上、右、下的位置。父View在指定子View的位置時通常會根據子View在measure中測量的大小來決定。
②子View的位置通常還受有其他屬性左右,例如父View的orientation,gravity,自身的margin等等,特別是RelativeLayout,影響布局的因素非常多。
③layout方法雖然可以被復寫,但是不建議去復寫,我們可以直接調用layout方法去確定自身的位置, 而且可以去復寫onLayout方法去確定子view的位置
-
setFrame
①setFrame方法是一個隱藏方法,所以作為應用層程序員來說,無法重寫該方法。該方法體內部通過比對本次的l、t、r、b四個值與上次是否相同來判斷自身的位置和大小是否發生了改變。
②如果發生了改變,將會調用invalidate請求重繪。
③記錄本次的l、t、r、b,用于下次比對。
④如果大小發生了變化,onSizeChanged方法,該方法在大多數View中都是空實現,我們可以重寫該方法用于監聽View大小發生變化的事件,在可以滾動的視圖中重載了該方法,用于重新根據大小計算出需要滾動的值,以便顯示之前顯示的區域。
- onLayout
①onLayout是ViewGroup用來決定子View擺放位置的,各種布局的差異都在該方法中得到了體現。
② onLayout比layout多一個參數,changed,該參數是在setFrame通過比對上次的位置得出是否發生了變化,通常該參數沒有被使用的意義,因為父View位置和大小不變,并不能代表子View的位置和大小沒有發生改變。
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// super.onLayout(changed, left, top, right, bottom);
//重寫~ ~ ~(略)
}
3.3 View的繪制
draw方法繪制要遵循一定的順序:
1.畫背景
2,5.畫邊緣
3.畫自身: ondraw方法
4.畫子View: dispatchDraw方法
6.畫滾動條
首先我們來看一下draw繪制流程:

Paste_Image.png
以下是對上面三個方法的說明:
-
draw
draw是由ViewRoot的performTraversals方法發起,它將調用DecorView的draw方法,并把成員變量canvas傳給給draw方法。而在后面draw遍歷中,傳遞的都是同一個canvas。所以android的繪制是同一個window中的所有View都繪制在同一個畫布上。等繪制完成,將會通知WMS把canvas上的內容繪制到屏幕上。自定義View時一般不重寫該方法。
-
onDraw
①View用來繪制自身的實現方法,如果我們想要自定義View,通常需要重載該方法。
②TextView中在該方法中繪制文字、光標和CompoundDrawable
③ImageView中相對簡單,只是繪制了圖片
Canvas canvas = new Canvas(bitmap);
之所以要傳入一個bitmap,是因為傳進來的bitmap與通過這個bitmap創建的Canvas畫布是緊緊聯系在一起的,這個過程稱之為裝載畫布。
canvas.drawBitmap(bitmap, 0, 0, null);
Canvas mCanvas = new Canvas(bitmap);
通過mCanvas將繪制效果作用在了bitmap上,再通過invalidate()刷新的時候,我們就會發現通過onDraw()方法畫出來的bitmap已經發生了改變。
-
dispatchDraw
先根據自身的padding剪裁畫布,所有的子View都將在畫布剪裁后的區域繪制。
遍歷所有子View,調用子View的computeScroll對子View的滾動值進行計算。
根據滾動值和子View在父View中的坐標進行畫布原點坐標的移動,根據子在父View中的坐標計算出子View的視圖大小,然后對畫布進行剪裁。
dispatchDraw的邏輯其實比較復雜,但是幸運的是對子View流程都采用該方式,而ViewGroup已經處理好了,我們不必要重載該方法對子View進行繪制事件的派遣分發。
重寫時,千萬千萬不要注釋了super.方法
好了,關于View的原理與使用就介紹到這里,關于View的更深層的理解有機會再進行探討。
來自:http://www.jianshu.com/p/a3014f8442b0