網上對自定義View總結的文章都不少,可是本身仍是寫一篇,好記性不如多敲字!
其實自定義View就是三大流程,onMeasure、onLayout、onDraw。看名字就知道,onMeasure是用來測量,onLayout佈局,onDraw進行繪製。
那麼什麼時候開始進行View的繪製流程,這就要從ViewRoot和DecorView的概念提及。java
ViewRoot對應於ViewRootImpl類,是鏈接WindowManager和DecorView的紐帶,View的三大繪製流程都是經過ViewRoot來完成的。在ActivityThread中,當Activity被建立時,會將DecorView添加到Window中,同時建立一個ViewRootImpl對象,並將ViewRootImpl對象和DecorView對象創建關聯。android
以上摘自《Android開發藝術探索》第4章View的工做原理
咱們一般開發時,更新UI通常都是不能在子線程中進行,假如在子線程中更新,會拋出異常。這並非由於只有UI線程才能更新UI,而是ViewRootImpl對象是在UI線程中建立。
View的繪製就是從ViewRoot的performTraversals方法開始的。
DecorView是一個頂級View,通常是一個豎直方向的LinearLayout,包含一個titlebar和內容區域。咱們在Activity中setContentView中設置的佈局文件就是加載到內容區域。內容區域是個FrameLayout。web
大多數狀況下,咱們若是在佈局文件中,對自定義View的layout_width和layout_height不設置wrap_content,咱們通常都是不須要進行處理的,可是若是要設置爲wrap_content,咱們須要在測量時,對寬高進行測量。ide
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
重寫onMeasure方法,咱們能夠看到兩個傳入的int值widthMeasureSpec和heightMeasureSpec。Java中int類型是4個字節,也就是32位,這兩個int值中的高2位表明SpecMode,也就是測量模式,低32位則是表明SpecSize也就是在某個測量模式下的大小。
咱們不須要本身寫代碼進行位運算獲得SpecMode和SpecSize,Android內置了MeasureSpec類來處理。函數
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
那SpecMode測量模式佔2位,二進制2位能夠表達最多4種狀況,還好,測量模式只有三種狀況,每一種狀況有其特殊的意思。oop
SpecMode | 含義 |
---|---|
UNSPECIFIED | 父容器不對當前View有任何限制,就是說View能夠取任意大小。 |
EXACTLY | 父容器測量出View須要的精確大小,對於match_parent和具體數值狀況xxdp |
AT_MOST | 當前View所能取的最大尺寸,通常是給定一個大小,View的尺寸不能超過該大小,通常用於warp_content |
如下摘自實驗室小夥伴的總結,《自定義View,這一篇就夠了》。對於咱們在佈局中定義的尺寸和測量模式的對應關係,看了下面的總結,就不會有任何疑惑了。佈局
match_parent:EXACTLY。怎麼理解呢?match_parent就是要利用父View給咱們提供的所剩餘空間,而父View剩餘空間是肯定的,也就是這個測量模式的整數裏面存放的尺寸。
wrap_content:AT_MOST。怎麼理解?就是咱們想要將大小設置爲包裹咱們View內容,那麼尺寸大小就是父View給咱們做爲參考的尺寸,只要不超過這個尺寸就能夠了,具體尺寸就根據咱們的需求去設定。
固定尺寸(如100dp):EXACTLY。怎麼理解呢?用戶本身指定了大小,咱們就不用再去幹涉了,固然是以指定的大小爲主啦。post
經過前文的描述,咱們已經能夠動手重寫onMeasure函數了。spa
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(WRAP_WIDTH, WRAP_HEIGHT);
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(WRAP_WIDTH, height);
} else if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(width, WRAP_HEIGHT);
}
}
只處理AT_MOST狀況也就是wrap_content,其餘狀況則沿用系統的測量值便可。setMeasuredDimension會設置View寬高的測量值,只有setMeasuredDimension調用以後,才能使用getMeasureWidth()和getMeasuredHeight()來獲取視圖測量出的寬高,以此調用這兩個方法獲得的值都會是0。
上述是一個通用的些煩,咱們實現一個自定義View,畫一個圓。
xml佈局以下:線程
<com.zhu.testview.MyView android:id="@+id/my_view" android:layout_width="match_parent" android:layout_height="100dp" android:background="#44ff0000" />
咱們將其中的寬改成wrap_content,並設置默認的寬高爲200;
private final int WRAP_WIDTH = 200;
private final int WRAP_HEIGHT = 200;
<com.zhu.testview.MyView android:id="@+id/my_view" android:layout_width="wrap_content" android:layout_height="100dp" android:background="#44ff0000" />
注意
若是咱們不處理AT_MOST狀況,那麼即便設置了wrap_content,最終的效果也和match_parent同樣,這是由於這種狀況下,View的SpecSize就是父容器測量出來可用的大小。
若是咱們設置了margin會有什麼效果呢?咱們來看看。
<com.zhu.testview.MyView android:id="@+id/my_view" android:layout_width="wrap_content" android:layout_height="100dp" android:layout_margin="20dp" android:background="#44ff0000" />
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
到這裏整個onMeasure過程就基本差很少了。
注意
一、某些極端狀況下,系統可能要屢次measure才能肯定最終測量的寬高,這時onMeasure中拿到的不必定是準確的,因此onLayout或onSizeChanged中獲取寬高。
protected void onSizeChanged(int w, int h, int oldw, int oldh)
Activity中在onWindowFocusChanged中獲取。這時View已經初始化完了,能夠獲取寬高。當Activity窗口得到焦點和失去焦點時均會被調用,所以該函數會被調用屢次。
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
int width = myView.getWidth();
int height = myView.getHeight();
Log.d(TAG, "width: " + width);
Log.d(TAG, "height: " + height);
Log.d(TAG, "measuredWidth: " + myView.getMeasuredWidth());
Log.d(TAG, "measuredHeight: " + myView.getMeasuredHeight());
}
}
view.post(runnable)
經過post將一個runnable放到消息隊列尾部,等到looper調用此runnable,這時View也已經初始化好了。
myView.post(new Runnable() {
@Override public void run() {
Log.d(TAG, "measuredWidth: " + myView.getMeasuredWidth());
Log.d(TAG, "measuredHeight: " + myView.getMeasuredHeight());
}
});
能夠在onCreate、onStart和onResume中調用view.post(runnable)方法。
ViewTreeObserver
使用ViewTreeObserver的回調能夠完成獲取View的寬高。
ViewTreeObserver observer = myView.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override public void onGlobalLayout() {
Log.d(TAG, "observer measuredWidth: " + myView.getMeasuredWidth());
Log.d(TAG, "observer measuredHeight: " + myView.getMeasuredHeight());
}
});
這裏使用了onGlobalLayoutListener接口,當View樹的狀態發生改變或View樹內部的View可見性發生改變時,onGlobalLayout會被回調,這也說明onGlobalLayout會被調用屢次。
做者:拿頭撞雞
連接:http://www.jianshu.com/p/1695988095a5