【轉載】自定義View學習筆記之詳解onMeasure

網上對自定義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

 

enter description here

DecorView的結構.png

 

onMeasure

大多數狀況下,咱們若是在佈局文件中,對自定義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

經過前文的描述,咱們已經能夠動手重寫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" />

 

enter description here

1944426-850897e838b2b8c5.jpg

 


咱們將其中的寬改成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" />

 

enter description here

1944426-6c6ecd36247edea4.jpg

咱們看到寬度已經不是原先的match_parent了。

 


注意
若是咱們不處理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" />

 

enter description here

1944426-77cc365e7a504e95.jpg

看來margin屬性的效果生效了,可是因爲咱們並無處理margin屬性,而margin屬性是由父容器控制的,所以,咱們自定義View中就不須要作特殊處理。可是padding屬性就須要咱們作處理。

 

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) 

 

enter description here

log.png

咱們看到onMeasure進行了兩次測量。當開啓了旋轉時,每當手機旋轉,咱們就要從新measure,而後會調用onSizeChanged()方法。這個方法頭兩個參數是當前尺寸大小,後兩個是上一次測量的尺寸。
二、在onLayout過程後,咱們就能夠調用getWidth()方法和getHeight()方法來獲取視圖的寬高了。getWidth()方法和getMeasureWidth()的值基本相同。但getMeasureWidth()方法在measure()過程結束後就能夠獲取到了,而getWidth()方法要在layout()過程結束後才能獲取到。另外,getMeasureWidht()方法中的值是經過setMeasuredDimension()方法來進行設置的,而getWidth()方法中的值是經過視圖右邊的座標減去左邊的座標計算出來的。
三、Activity中須要View的寬高時,onCreate、onStart、onResume中都是沒法獲取的。這是因爲View的生命週期和Activity的生命週期不是同步的。解決方法有以下三種:

 

  • 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());
    }
}

 

enter description here

1944426-10adc634d9e337f9.png

 

  • 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

相關文章
相關標籤/搜索