天天看點

自定義View:測量measure,布局layout,繪制draw1. 什麼是View2. View和Activity的差別3. View種類4. ViewGroup5. 為什麼要有ViewGroup?6. View樹7. View的測量、布局、繪制過程8. measure 測量9. layout 布局10. draw 繪制11. 畫布的移動和剪裁12. 動畫的繪制參考連結

1. 什麼是View

在Android的官方文檔中是這樣描述的:表示了使用者界面的基本構模組化塊。一個View占用了螢幕上的一個矩形區域并且負責界面繪制和事件處理。

手機螢幕上所有看得見摸得着的都是View。這一點對所有圖形系統來說都一樣,例如ios的UIView。

2. View和Activity的差別

我們之前學習過android的四大元件,Activity是四大元件中唯一一個用來和使用者進行互動的元件。可以說Activity就是android的視圖層。

如果再細化,Activity相當于視圖層中的控制層,是用來控制和管理View的,真正用來顯示和處理事件的實際上是View。

每個Activity内部都有一個Window對象, Window對象包含了一個DecorView(實際上就是FrameLayout),我們通過setContentView給Activity設定顯示的View實際上都是加到了DecorView中。

3. View種類

android提供了種類豐富的View來應對各種需求,例如提供文字顯示的TextView,提供點選事件的Button,提供圖檔顯示的ImageView,還有各種布局檔案,例如Relativilayout,Linearlayout等等。他們都是繼承自View。

自定義View:測量measure,布局layout,繪制draw1. 什麼是View2. View和Activity的差別3. View種類4. ViewGroup5. 為什麼要有ViewGroup?6. View樹7. View的測量、布局、繪制過程8. measure 測量9. layout 布局10. draw 繪制11. 畫布的移動和剪裁12. 動畫的繪制參考連結

4. ViewGroup

ViewGroup繼承自View,并實作了兩個接口ViewParent和ViewManager。

ViewManager提供了三個抽象方法addView,removeView,updateViewLayout。用來添加、删除、更新布局。

ViewParent主要提供了一系列操作子View的方法例如焦點的切換,顯示區域的控制等等。

5. 為什麼要有ViewGroup?

實際上所有的事情View都能做,包括顯示複雜的界面,我們隻需要設計一個複雜的View即可。例如短信通知的icon,一個可以顯示圖檔又可以顯示文字的View,我們後期學習了View的draw方法後,可以輕松的設計一個View來達到這個效果,但是這樣不僅複雜,而且重用性較差,還會因為一點小改動而重複的創造輪子,這顯然不符合程式員偷懶的原則,是以我們可以完全把ImageView和TextView組合到一起就可以了,這個時候我們就需要一個容器,ViewGroup,來裝這兩個View。

ViewGroup和View最大的不同是可以組合多個View,那麼多個View在一起,該如何擺放,這就是ViewGroup需要解決的問題。

6. View樹

我們看到的界面,都是以一個ViewGroup作為根View,通過往ViewGroup中添加子View(可以是View,也可以是ViewGroup),來組合出各具特色的界面。

這種從根到葉的組合方式,我們可以看做成一個View樹。(類似于XML),而View的顯示和事件處理,都是依賴于這個View樹。

繪制和事件處理的起始點,都是從根View開始一級一級的往下傳遞。我們從任意一層發起繪制,都将回報到根View,然後再從上往下傳遞。

之前我們說過根View就是Window中的DecorView,也就是一個FrameLayout。

6.1 View樹示意圖

自定義View:測量measure,布局layout,繪制draw1. 什麼是View2. View和Activity的差別3. View種類4. ViewGroup5. 為什麼要有ViewGroup?6. View樹7. View的測量、布局、繪制過程8. measure 測量9. layout 布局10. draw 繪制11. 畫布的移動和剪裁12. 動畫的繪制參考連結

對SystemUI,也就是我們常說的StatusBar顯示在哪兒呢,其實SystemUI是一個單獨的App,随着系統啟動而啟動,将會啟動一個系統級服務,接收我們送出的通知,該應用也會有一個window,并且級别比我們普通應用的window要高,是以會顯示在我們的應用的外面,隻不過該window的高度比較小。

7. View的測量、布局、繪制過程

整個android系統 CS架構,view被展示到界面上需要經過3個步驟

  • 需要花多大:measure –> onMeasure –> setMeasuredDimension
  • 畫在什麼地方:layout –> setFrame –> onLayout
  • 怎麼畫:draw –> > onDraw –> dispatchDraw

7.1 顯示一個View需要經過哪些步驟

  • Measure測量一個View的大小
  • Layout擺放一個View的位置
  • Draw畫出View的顯示内容

其中measure和layout方法都是final的,無法重寫,雖然draw不是final的,但是也不建議重寫該方法。這三個方法都已經寫好了View的邏輯,如果我們想實作自身的邏輯,而又不破壞View的工作流程,可以重寫onMeasure、onLayout、onDraw方法。

7.2 如何發起一個View樹的測量/布局/繪制流程

通過調用requestLayout/requestFocus都将發起一個View樹的測量。測量完畢後會進行布局,布局完畢後就會繪制。

如果View的大小沒有發生改變,布局也沒有變化,隻是顯示的内容發生了變化,則可以通過invalidate來請求繪制,此時将不會測量和布局,直接從繪制開始。

7.3 View内部的mPrivateFlags變量

View中有一個私有int變量mPrivateFlags,用于儲存View的狀态,int型32位,通過0/1可以儲存32個狀态的true或者false,采用這種方式可以有效的減少記憶體占用,提高運算效率。

當某一個View發起了測量請求時,将會把mPrivateFlags中的某一位從0變為1,同時請求父View,父View也會把自身的該值從0變為1,同時也将會把其他子View的值從0變為1。這樣一層一層傳遞,最終傳到到DecorView,DecorView的父View是ViewRoot,是以最終都将由ViewRoot來進行處理。

ViewRoot收到請求後,将會從上至下開始周遊,檢查标記,隻要有相對應的标記就執行測量/布局/繪制

當Activity被建立時,會相應的建立一個Window對象,Window對象建立時會擷取應用的WindowManager(注意,這是應用的視窗管理者,不是系統的)。

Activity被建立後,會調用Activity的onCreate方法。我們通過設定setContentView就會調用到Window中的setContextView,進而初始化DecorView。

是以我們需要隐藏标題欄什麼的,都需要在DecorView初始化之前進行設定。

DecorView初始化之後将會被添加到WindowManager中,同時WindowManager中會為新添加的DecorView建立一個對應的ViewRoot,并把DecorView設定給ViewRoot。

是以根View就是DecorView,因為DecorView的父親是ViewRoot,實作自ViewParent接口,但是沒有繼承自View,是以根本不是一個View。

從系統的命名來看,WindowManger繼承自ViewManager,而添加到WindowManager中的是DecorView,不是Window,都說明了其實真正意義上的window就是View。

在ViewRoot的構造方法中會通過getWindowSession來擷取WindowManagerService系統服務的遠端對象(這才是系統級的)。

當ViewRoot的setView方法中将會調用requestLayout進行第一次視圖測量請求。同時sWindowSession.add自身内部的一個W對象,以此達到和WindowManagerService的關聯。

W是一個Binder對象。可以實作跨程序的通信了,并且是一個雙方都掌握着主動調用的跨程序通信方式。

7.4 常用的标記位

  • FORCE_LAYOUT 請求繪制,将從measure開始,,并增加LAYOUT_REQUIRED标記
  • 持有LAYOUT_REQUIRED标記的View将會被執行layout,完畢後會去掉LAYOUT_REQUIRED和FORCE_LAYOUT
  • DRAWN帶有該标簽的将不會被draw,注意,這和上面兩個不一緻,當draw完畢後會加上該标簽,當沒有該标簽才會被draw。

還有一些其他的标記位,大家可以自行閱讀源碼。

7.5 測量/布局/繪制流程

自定義View:測量measure,布局layout,繪制draw1. 什麼是View2. View和Activity的差別3. View種類4. ViewGroup5. 為什麼要有ViewGroup?6. View樹7. View的測量、布局、繪制過程8. measure 測量9. layout 布局10. draw 繪制11. 畫布的移動和剪裁12. 動畫的繪制參考連結

測量事件最終傳遞到decorView的父親ViewRoot那裡,由它的函數performTraversals來執行,聽名字就知道是執行周遊了。

首先它會檢測之前設定的标記為來确定是否需要測量大小,是,就會直接執行decorView的measture方法,該方法内部會測量完自身後,将會繼續周遊所有子View,直到每一個設定有标記的子View都測量完。

然後它會檢測是否需要布局,是,将會執行decorView的layout方法進行,該方法内部也會周遊所有設定有标記位子View。

8. measure 測量

8.1 測量流程

自定義View:測量measure,布局layout,繪制draw1. 什麼是View2. View和Activity的差別3. View種類4. ViewGroup5. 為什麼要有ViewGroup?6. View樹7. View的測量、布局、繪制過程8. measure 測量9. layout 布局10. draw 繪制11. 畫布的移動和剪裁12. 動畫的繪制參考連結

測量View是在measure()方法中,而measure()方法是final修飾的,不允許重寫,但是在measure()方法中回調了onMeasure()方法,是以我們自定義View的時候需要重寫onMeasure()方法,在該方法中實作測量的邏輯

  • 如果是普通View,則直接通過setMeasureDimension()方法設定大小即可
  • 如果是ViewGroup,則需要循環周遊所有子View,調用子View的measure()方法,測量每個子View的大小,等所有的子View都測量完畢,最後通過setMeasureDimension()設定ViewGroup自身的大小

8.2 LayoutParams

每個View都包含一個ViewGroup.LayoutParams類或者其派生類,LayoutParams中包含了View和它的父View之間的關系,而View大小正是View和它的父View共同決定的。

我們設定View的大小,有match_parent、wrap_content和具體的dip值。

match_parent對應值為-1、wrap_conten對應值為-2,具體dip對應其設定的值。在測量時,View的父類從Layout中讀出寬高值,根據不同的值設定不同的計算模式。

布局檔案中所有layout_開頭的在代碼中都是需要通過LayoutParams來設定。

當我們通過addView添加一個子View時,如果它沒有LayoutParams或者是LayoutParams的類型不比對,那麼将會建立一個預設的LayoutParams。

通過布局檔案進行layout_width,layout_height進行設定。通過代碼設定,需要一個LayoutParams來描述。

View view = new View(this);
LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.WRAP_CONTENT);
view.setLayoutParams(lp);
           

8.3 measure

/**
  * This is called to find out how big a view should be. The parent
  * supplies constraint information in the width and height parameters.
  * The actual measurement work of a view is performed in
  * {@link #onMeasure(int, int)}, called by this method. Therefore, only
  * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
  * @param widthMeasureSpec Horizontal space requirements as imposed by the
  *        parent
  * @param heightMeasureSpec Vertical space requirements as imposed by the
  *        parent
  *
  * @see #onMeasure(int, int)
  */
 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    ...
 }
           

measure是final修飾的方法,不可被重寫。在外部調用時,直接調用view.measure(int wSpec, int hSpec)。measure中調用了onMeasure。自定義view時,重寫onMeasure即可

8.4 onMeasure

measure是一個final方法,用來測量View自身的大小,View類該方法體邏輯比較簡單,隻是根據判斷條件決定是否需要調用onMeasure。方法接受兩個參數,分别就是通過MeasureSpec類合成測量模式和大小的寬與高。

實際上View的大小是無限大的,measure測量出來的大小隻是為了layout時父View配置設定給它的顯示區,也就是後來draw時畫布的剪裁大小,和touch事件分發時計算落點是否在它上面。

onMeasure通過父View傳遞過來的大小和模式,以及自身的背景圖檔的大小得出自身最終的大小,通過setMeasuredDimension()方法設定給mMeasuredWidth和mMeasuredHeight。

普通View的onMeasure邏輯大同小異,基本都是測量自身内容和背景,然後根據父View傳遞過來的MeasureSpec進行最終的大小判定,例如TextView會根據文字的長度,文字的大小,文字行高,文字的行寬,顯示方式,背景圖檔,以及父View傳遞過來的模式和大小最終确定自身的大小。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
           

8.4 ViewGroup的onMeasure

ViewGroup是個抽象類,本身沒有實作onMeasure,但是他的子類都有各自的實作,通常他們都是通過measureChildWithMargins函數或者其他類似于measureChild的函數來周遊測量子View,被GONE的子View将不參與測量,當所有的子View都測量完畢後,才根據父View傳遞過來的模式和大小來最終決定自身的大小。

在測量子View時,會先擷取子View的LayoutParams,從中取出寬高,如果是大于0,将會以精确的模式加上其值組合成MeasureSpec傳遞子View,如果是小于0,将會把自身的大小或者剩餘的大小傳遞給子View,其模式判定在前面已經講過。

ViewGroup一般都在測量完所有子View後才會調用setMeasuredDimension()設定自身大小。

如果是一個View,重寫onMeasure時要注意:如果在使用自定義view時,用了wrap_content。那麼在onMeasure中就要調用setMeasuredDimension,來指定view的寬高。如果使用的fill_parent或者一個具體的dp值。那麼直接使用super.onMeasure即可。

如果是一個ViewGroup,重寫onMeasure時要注意:首先,結合上面兩條,來測量自身的寬高。然後,需要測量子View的寬高。測量子view的方式有:

getChildAt(int index),可以拿到index上的子view。通過getChildCount得到子view的數目,再循環周遊出子view。接着,subView.measure(int wSpec, int hSpec),使用子view自身的測量方法

或者調用viewGroup的測量子view的方法:

//某一個子view,多寬,多高, 内部減去了viewGroup的padding值
measureChild(subView, int wSpec, int hSpec); 

//所有子view 都是 多寬,多高, 内部調用了measureChild方法
measureChildren(int wSpec, int hSpec);

//某一個子view,多寬,多高, 内部減去了viewGroup的padding值、margin值和傳入的寬高wUsed、hUsed  
measureChildWithMargins(subView, intwSpec, int wUsed, int hSpec, int hUsed); 
           

Tips:自定義ViewGroup的時候,通常繼承FrameLayout,這樣就不必實作onMeasure()方法,讓FrameLayout幫我們實作測量的工作,我們實作onlayout()即可

  • onFinishInflate()

    當布局加載完成的時候的回調,自定義View的時候我們可以在該方法中擷取View的寬高

  • onSizeChange()

    當view的大小發生變化的時候的回調

    • requestLayout()

      重新布局,包括測量measure和布局onlayout

  • resolveSize(int size, int measureSpec)

    算出來的size和測量出來的spec那個合适用那個

public static int resolveSize(int size, int measureSpec) {
        return resolveSizeAndState(size, measureSpec, ) & MEASURED_SIZE_MASK;
    }

 public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);
        final int result;
        switch (specMode) {
            case MeasureSpec.AT_MOST:
                if (specSize < size) {
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    result = size;
                }
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }
           

8.5 setMeasuredDimension

8.6 MeasureSpec

一個MeasureSpec封裝了從父容器傳遞給子容器的布局要求,更精确的說法應該這個MeasureSpec是由父View的MeasureSpec和子View的LayoutParams通過簡單的計算得出一個針對子View的測量要求,這個測量要求就是MeasureSpec

這是一個含mode和size的結合體,不需要我們來具體的關心。當在測量時,可以調用MeasureSpec.getSize|getMode 得到相應的size和mode。然後使用MeasureSpec.makeMeasureSpec(size,mode); 來建立MeasureSpec對象。那麼mode是怎麼來的呢?是根據使用該自定義view時的layoutWith|height參數決定的,是以不能自己随便new一個。而size可以自己指定,也可以直接使用 measureSpec.getSize。

MeasureSpe描述了父View對子View大小的期望。裡面包含了測量模式和大小。

MeasureSpe類把測量模式和大小組合到一個int型的數值中,其中高2位表示模式,低30位表示大小。

我們可以通過以下方式從MeasureSpec中提取模式和大小,該方法内部是采用位移計算。

int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
           

也可以通過MeasureSpec的靜态方法把大小和模式合成,該方法内部隻是簡單的相加。

MeasureSpec.makeMeasureSpec(specSize,specMode);
           

采用這種方式,是為了提升效率,因為onMeasure在繪制過程中會被大量遞歸調用。

MeasureSpec中的測量模式有以下三種

  • EXACTLY(-1):精确的,表達了父View期望子View的大小就是父View通過MeasureSpec傳遞過來的大小。
  • AT_MOST(-2):最多的,表達了父View期望子View通過測量自身的大小來決定自己的大小,但是最多不要超過MeasureSpec傳遞過來的大小。
  • UNSPECIFIED(0):未指明,通常這時候MeasureSpec傳遞過來的大小也是0,說明父View不對子View的大小做任何期望,随子View自己決定。

通常情況下,我們應該遵守這種規則,當然如果也特殊需求也可以不遵守。但是不遵守該方式,在後面的layout中父View給你的視圖大小仍然是它給的期望值。

8.6.1 常用方法

  • MeasureSpec.getSize(widthMeasureSpec) 擷取view的寬

    -MeasureSpec.getMode(int measurespec) 擷取測量模式

    -MeasureSpec.makeMeasureSpec(size,mode) 組裝32位的測量政策,高2位:mode,低30位:size

8.6.2 測量政策

  • MeasureSpec.AT_MOST

    表示子布局被限制在一個最大值内,一般當childView設定其寬、高為wrap_content時,ViewGroup會将其設定為AT_MOST

  • MeasureSpec.EXACTLY

    表示設定了精确的值,一般當childView設定其寬、高為精确值、match_parent時,ViewGroup會将其設定為EXACTLY

  • MeasureSpec.UNSPECIFIED

    表示子布局想要多大就多大,一般出現在AadapterView的item的heightMode中、ScrollView的childView的heightMode中;此種模式比較少見

8.6.3 擷取View的寬高

getMeasuredHeight(),測量後的高度,實際高度。擷取測量完的高度,隻要在onMeasure方法執行完,就可以用它擷取到寬高,在自定義控件内部多使用這個使用view.measure(0,0)方法可以主動通知系統去測量,然後就可以直接使用它擷取寬高

getHeight(),顯示的高度。必須在onLayout方法執行完後,才能獲得寬高

8.6.4 measure(0,0)

view.measure(0,0)主動通知系統去測量

View view = new View(context);
view.measure(,);//等價于下面的代碼

// MeasureSpec.UNSPECIFIED = 0
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(,MeasureSpec.UNSPECIFIED);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(,MeasureSpec.UNSPECIFIED);
view.measure(widthMeasureSpec,heightMeasureSpec);

int measureWidth = view.getMeasuredWidth();
           
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        int width = view.getWidth();
    }
});
           

8.6.5 getMeasuredWidth()與getWidth()的差別

首先getMeasureWidth()方法在measure()過程結束後就可以擷取到了,而getWidth()方法要在layout()過程結束後才能擷取到。

getMeasureWidth()方法中的值是通過setMeasuredDimension()方法來進行設定的,而getWidth()方法中的值則是通過layout(left,top,right,bottom)方法設定的。

getWidth():隻有調用了onLayout()方法,getWidth()才指派,顯示的寬度

getMeasureWidth():擷取測量完的寬度,隻要在onMeasure()方法執行完,就可以用它擷取到高度,實際的寬度

8.6.6 getHeight()和getMeasuredHeight()的差別

getMeasuredHeight():擷取測量完的高度,隻要在onMeasure方法執行完,就可以用它擷取到寬高,在自定義控件内部多使用這個。使用view.measure(0,0)方法可以主動通知系統去測量,然後就可以直接使用它擷取寬高

getHeight():必須在onLayout方法執行完後,才能獲得寬高

view.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
      @Override
      public void onGlobalLayout() {              
            headerView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
            int headerViewHeight = headerView.getHeight(); //直接可以擷取寬高
    }
});     
           

8.6.7 getChildMeasureSpec

getChildMeasureSpec( )的總體思路就是通過其父視圖提供的MeasureSpec參數得到specMode和specSize,并根據計算出來的specMode以及子視圖的childDimension(layout_width和layout_height中定義的)來計算自身的measureSpec,如果其本身包含子視圖,則計算出來的measureSpec将作為調用其子視圖measure函數的參數,同時也作為自身調用setMeasuredDimension的參數,如果其不包含子視圖則預設情況下最終會調用onMeasure的預設實作,并最終調用到setMeasuredDimension,而該函數的參數正是這裡計算出來的

自定義View:測量measure,布局layout,繪制draw1. 什麼是View2. View和Activity的差別3. View種類4. ViewGroup5. 為什麼要有ViewGroup?6. View樹7. View的測量、布局、繪制過程8. measure 測量9. layout 布局10. draw 繪制11. 畫布的移動和剪裁12. 動畫的繪制參考連結
/**
     * Does the hard part of measureChildren: figuring out the MeasureSpec to
     * pass to a particular child. This method figures out the right MeasureSpec
     * for one dimension (height or width) of one child view.
     *
     * The goal is to combine information from our MeasureSpec with the
     * LayoutParams of the child to get the best possible results. For example,
     * if the this view knows its size (because its MeasureSpec has a mode of
     * EXACTLY), and the child has indicated in its LayoutParams that it wants
     * to be the same size as the parent, the parent should ask the child to
     * layout given an exact size.
     *
     * @param spec The requirements for this view
     * @param padding The padding of this view for the current dimension and
     *        margins, if applicable
     * @param childDimension How big the child wants to be in the current
     *        dimension
     * @return a MeasureSpec integer for the child
     */
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(, specSize - padding);

        int resultSize = ;
        int resultMode = ;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= ) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= ) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= ) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ?  : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ?  : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
           

8.7 measureChild

/**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * The heavy lifting is done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param parentHeightMeasureSpec The height requirements for this view
     */
    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);
    }
           

8.8 measureChildren

/**
     * Ask all of the children of this view to measure themselves, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * We skip children that are in the GONE state The heavy lifting is done in
     * getChildMeasureSpec.
     *
     * @param widthMeasureSpec The width requirements for this view
     * @param heightMeasureSpec The height requirements for this view
     */
    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = ; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }
           

8.9 measureChildWithMargins

/**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding
     * and margins. The child must have MarginLayoutParams The heavy lifting is
     * done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param widthUsed Extra space that has been used up by the parent
     *        horizontally (possibly by other children of the parent)
     * @param parentHeightMeasureSpec The height requirements for this view
     * @param heightUsed Extra space that has been used up by the parent
     *        vertically (possibly by other children of the parent)
     */
    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
           

9. layout 布局

自定義View:測量measure,布局layout,繪制draw1. 什麼是View2. View和Activity的差別3. View種類4. ViewGroup5. 為什麼要有ViewGroup?6. View樹7. View的測量、布局、繪制過程8. measure 測量9. layout 布局10. draw 繪制11. 畫布的移動和剪裁12. 動畫的繪制參考連結

Layout方法中接受四個參數,是由父View提供,指定了子View在父View中的左、上、右、下的位置。父View在指定子View的位置時通常會根據子View在measure中測量的大小來決定。

子View的位置通常還受有其他屬性左右,例如父View的orientation,gravity,自身的margin等等,特别是RelativeLayout,影響布局的因素非常多。

9.1 setFrame

setFrame方法是一個隐藏方法,是以作為應用層程式員來說,無法重寫該方法。該方法體内部通過比對本次的l、t、r、b四個值與上次是否相同來判斷自身的位置和大小是否發生了改變。

如果發生了改變,将會調用invalidate請求重繪。

記錄本次的l、t、r、b,用于下次比對。

如果大小發生了變化,onSizeChanged方法,該方法在大多數View中都是空實作,程式員可以重寫該方法用于監聽View大小發生變化的事件,在可以滾動的視圖中重載了該方法,用于重新根據大小計算出需要滾動的值,以便顯示之前顯示的區域。

9.2 View的layout()

public final void layout(int l, int t, int r, int b) {
    .....
    //設定View位于父視圖的坐标軸
    boolean changed = setFrame(l, t, r, b);
    //判斷View的位置是否發生過變化,看有必要進行重新layout嗎
    if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
        if (ViewDebug.TRACE_HIERARCHY) {
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
        }
        //調用onLayout(changed, l, t, r, b); 函數
        onLayout(changed, l, t, r, b);
        mPrivateFlags &= ~LAYOUT_REQUIRED;
    }
    mPrivateFlags &= ~FORCE_LAYOUT;
    .....
}
           

9.3 onLayout()

setFrame(l, t, r, b) 設定View位于父視圖的坐标軸

onLayout是ViewGroup用來決定子View擺放位置的,各種布局的差異都在該方法中得到了展現。

onLayout比layout多一個參數,changed,該參數是在setFrame通過比對上次的位置得出是否發生了變化,通常該參數沒有被使用的意義,因為父View位置和大小不變,并不能代表子View的位置和大小沒有發生改變。

int childCount = getChildCount() ;
for(int i= ;i<childCount ;i++){
    View child = getChildAt(i) ;
    //整個layout()過程就是個遞歸過程
    child.layout(l, t, r, b) ;
}

public final int getMeasuredWidth() {
    return mMeasuredWidth & MEASURED_SIZE_MASK;
}
public final int getWidth() {
    return mRight - mLeft;
}
           

View中:

public void layout(int l,int t,int r,int b) {
     ...
     onLayout
     ...
}
//changed 表示是否有新的位置或尺寸
protected void onLayout(boolean changed,int left,int top,int right,int bottom) {
     //空實作
}
           

ViewGroup中:

public final void layout(int l,int t,int r,int b) {
     ...
     super.layout(l, t, r, b);
     ...
}
//changed 表示是否有新的位置或尺寸
protected abstract void onLayout(boolean changed, int l,int t, int r,int b);
           

說明:

  • 自定義一個view時,建議重寫onLayout,以設定它的位置。

    在外部調用時,調用layout(),觸發設定位置。

  • 自定義一個viewGroup時,必須且隻能重寫onLayout。

    需要在設定子view的位置:調用subview.layout(); 觸發

10. draw 繪制

自定義View:測量measure,布局layout,繪制draw1. 什麼是View2. View和Activity的差別3. View種類4. ViewGroup5. 為什麼要有ViewGroup?6. View樹7. View的測量、布局、繪制過程8. measure 測量9. layout 布局10. draw 繪制11. 畫布的移動和剪裁12. 動畫的繪制參考連結

draw同樣是由ViewRoot的performTraversals方法發起,它将調用DecorView的draw方法,并把成員變量canvas傳給給draw方法。而在後面draw周遊中,傳遞的都是同一個canvas。是以android的繪制是同一個window中的所有View都繪制在同一個畫布上。等繪制完成,将會通知WMS把canvas上的内容繪制到螢幕上。

10.1 draw的流程

  1. 繪制背景
  2. 繪制漸變效果(通常不繪制)
  3. 調用onDraw
  4. 調用dispatchDraw
  5. 調用onDrawScrollBars

繪制流程

  • Step 1, draw the background, if needed 繪制背景
  • Step 2, save the canvas’ layers
  • Step 3, draw the content 繪制内容
  • Step 4, draw the children 繪制子view
  • Step 5, draw the fade effect and restore layers
  • Step 6, draw decorations (scrollbars) 對View的滾動條進行繪制

10.2 onDraw()

繪制視圖自身,View用來繪制自身的實作方法,如果我們想要自定義View,通常需要重載該方法。TextView中在該方法中繪制文字、光标和CompoundDrawable,ImageView中相對簡單,隻是繪制了圖檔

10.3 dispatchDraw(canvas)

  • 先根據自身的padding剪裁畫布,所有的子View都将在畫布剪裁後的區域繪制。
  • 周遊所有子View,調用子View的computeScroll對子View的滾動值進行計算。
  • 根據滾動值和子View在父View中的坐标進行畫布原點坐标的移動,根據子在父View中的坐标計算出子View的視圖大小,然後對畫布進行剪裁,請看下面的示意圖。
  • dispatchDraw的邏輯其實比較複雜,但是幸運的是對子View流程都采用該方式,而ViewGroup已經處理好了,我們不必要重載該方法對子View進行繪制事件的派遣分發。

用來繪制子View的,周遊子View然後drawChild(),drawChild()方法實際調用的是子View.draw()方法,ViewGroup類已經為我們實作繪制子View的預設過程,這個實作基本能滿足大部分需求,是以ViewGroup類的子類(LinearLayout,FrameLayout)也基本沒有去重寫dispatchDraw方法

無論是View還是ViewGroup對它們倆的調用順序都是onDraw()->dispatchDraw()

但在ViewGroup中,當它有背景的時候就會調用onDraw()方法,否則就會跳過onDraw()直接調用dispatchDraw();是以如果要在ViewGroup中繪圖時,往往是重寫dispatchDraw()方法。dispatchDraw()方法内部周遊子view,調用子view的繪制方法來完成繪制工作

在View中,onDraw()和dispatchDraw()都會被調用的,是以我們無論把繪圖代碼放在onDraw()或者dispatchDraw()中都是可以得到效果的,但是由于dispatchDraw()的含義是繪制子控件,是以原則來上講,在繪制View控件時,我們是重新onDraw()函數

在繪制View控件時,需要重寫onDraw()函數,在繪制ViewGroup時,需要重寫dispatchDraw()函數。

在自定義控件public class CircleProgressView extends LinearLayout的時候,如果不設定背景的話setBackground()的話,是不會走onDraw()方法的

dispatchDraw()繪制具體的内容(圖檔和文本)

View中:

public void draw(Canvas canvas) {
/*
1. Draw the background   繪制背景
2. If necessary, save the canvas' layers to prepare for fading  如有必要,顔色漸變淡之前儲存畫布層(即鎖定原有的畫布内容)
3. Draw view's content  繪制view的内容
4. Draw children    繪制子view
5. If necessary, draw the fading edges and restore layers   如有必要,繪制顔色漸變淡的邊框,并恢複畫布(即畫布改變的内容附加到原有内容上)
6. Draw decorations (scrollbars for instance)   繪制裝飾,比如滾動條
*/
   ...
   if (!dirtyOpaque) {
       drawBackground(canvas); //背景繪制
   }
   // skip step 2 & 5 if possible (common case) 通常情況跳過第2和第5步
   ...
   if (!dirtyOpaque) onDraw(canvas); //調用onDraw
   dispatchDraw(canvas);   //繪制子view
   onDrawScrollBars(canvas); //繪制滾動條
   ...
}
protected void dispatchDraw(Canvas canvas) { //空實作 }
protected void onDraw(Canvas canvas) { //空實作 }
           

ViewGroup中:

protected void dispatchDraw(Canvas canvas) {
    ...
    drawChild(...); //繪制子view
    ...
}

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
}
           

說明:

  • 自定義一個view時,重寫onDraw。

    調用view.invalidate(),會觸發onDraw和computeScroll()。前提是該view被附加在目前視窗上view.postInvalidate(); //是在非UI線程上調用的

  • 自定義一個ViewGroup,重寫onDraw。

    onDraw可能不會被調用,原因是需要先設定一個背景(顔色或圖)。表示這個group有東西需要繪制了,才會觸發draw,之後是onDraw。是以,一般直接重寫dispatchDraw來繪制viewGroup

  • 自定義一個ViewGroup,dispatchDraw會調用drawChild。

11. 畫布的移動和剪裁

11.1 畫布的移動和剪裁1

下面是一個ViewGroup視圖,綠色原點代表其原點,根據padding剪裁畫布後,黃色區域代表其剪裁後的畫布區域,畫布的原點将會移到黃色原點處

自定義View:測量measure,布局layout,繪制draw1. 什麼是View2. View和Activity的差別3. View種類4. ViewGroup5. 為什麼要有ViewGroup?6. View樹7. View的測量、布局、繪制過程8. measure 測量9. layout 布局10. draw 繪制11. 畫布的移動和剪裁12. 動畫的繪制參考連結

11.2 畫布的移動和剪裁2

在ViewGroup中,我們放一個TextView,Viewgroup完全滿足TextView的測量大小,給了它合适的顯示區域,也就是layout中設定的位置和它的大小一緻。畫布的原點會移動到粉色原點處。此時畫布剪裁為粉色區域這麼大。

自定義View:測量measure,布局layout,繪制draw1. 什麼是View2. View和Activity的差別3. View種類4. ViewGroup5. 為什麼要有ViewGroup?6. View樹7. View的測量、布局、繪制過程8. measure 測量9. layout 布局10. draw 繪制11. 畫布的移動和剪裁12. 動畫的繪制參考連結

11.3 畫布的移動和剪裁3

如果TextView的内容足夠多,onMeasure的時候我們不理會父View給的參數,直接根據自身的内容來設定大小,但是父View在onLayout的時候配置設定的位置還是它期望的大小,也就是黑色的邊框,這個時候粉色區域是TextView的大小,但是畫布仍舊是黑色邊框,畫布原點仍舊是粉色原點

自定義View:測量measure,布局layout,繪制draw1. 什麼是View2. View和Activity的差別3. View種類4. ViewGroup5. 為什麼要有ViewGroup?6. View樹7. View的測量、布局、繪制過程8. measure 測量9. layout 布局10. draw 繪制11. 畫布的移動和剪裁12. 動畫的繪制參考連結

11.4 畫布的移動和剪裁4

我們為了看到其他的區域文字,對TextView進行了scroll的滾動,這個時候畫布的剪裁大小任然是黑色邊框,但是原點由透明原點根據TextView的滾動值進行移動到了TextView的原點,繪制會從textView的原點進行繪制,但是因為他們超出了畫布的剪裁區域,将不會把資料繪制到畫布上。

自定義View:測量measure,布局layout,繪制draw1. 什麼是View2. View和Activity的差別3. View種類4. ViewGroup5. 為什麼要有ViewGroup?6. View樹7. View的測量、布局、繪制過程8. measure 測量9. layout 布局10. draw 繪制11. 畫布的移動和剪裁12. 動畫的繪制參考連結

12. 動畫的繪制

  • 動畫就是讓畫面“動”起來,其原理就是不斷的繪制,但是每次繪制都有差別。
  • 在ViewGroup的drawChild方法中會判斷child是否包含動畫,如果包含,則根據動畫類計算出動畫執行的區域矩形,判斷動畫是否啟動了,啟動了就擷取動畫目前的值,例如位移值等等。然後根據值對畫布進行剪裁調整,執行子View的draw進行繪制。
  • 判斷動畫是否結束,如果沒有,則調用invalidate再次請求繪制。

12.1 動畫繪制1

在ViewGroup中有一個紅色的子View,将執行一個位移動畫,位移動畫将執行到A的位置,那麼将會先根據動畫參數計算出A位置的矩形大小。

自定義View:測量measure,布局layout,繪制draw1. 什麼是View2. View和Activity的差別3. View種類4. ViewGroup5. 為什麼要有ViewGroup?6. View樹7. View的測量、布局、繪制過程8. measure 測量9. layout 布局10. draw 繪制11. 畫布的移動和剪裁12. 動畫的繪制參考連結

12.2 動畫繪制2

剪裁畫布區域A的位置,把該畫布交給子View,讓其執行draw方法,那麼View的内容都将會被繪制到A區域,而V所處的位置并沒有發生變化。View繼續移動到B位置。

自定義View:測量measure,布局layout,繪制draw1. 什麼是View2. View和Activity的差別3. View種類4. ViewGroup5. 為什麼要有ViewGroup?6. View樹7. View的測量、布局、繪制過程8. measure 測量9. layout 布局10. draw 繪制11. 畫布的移動和剪裁12. 動畫的繪制參考連結

12.3 動畫繪制3

此時剪裁畫布區域B的位置,把該畫布交給子View,讓其執行draw方法,那麼View的内容都将會被繪制到B區域,但是B區域有一部分已經超過了ViewGroup畫布區域。超出的地方雖然被繪制了,但是不會添加到畫布上,也就不會顯示出來

自定義View:測量measure,布局layout,繪制draw1. 什麼是View2. View和Activity的差別3. View種類4. ViewGroup5. 為什麼要有ViewGroup?6. View樹7. View的測量、布局、繪制過程8. measure 測量9. layout 布局10. draw 繪制11. 畫布的移動和剪裁12. 動畫的繪制參考連結

參考連結

  • Android onMeasure、Measure、measureChild、measureChildren 一些簡要說明
  • Android layout、onLayout 一些簡要說明
  • Android draw、onDraw、dispatchDraw、invalidate、computeScroll 一些簡要說明

繼續閱讀