1. 程式人生 > >自定義View:測量measure,佈局layout,繪製draw

自定義View:測量measure,佈局layout,繪製draw

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

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

對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

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

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

然後它會檢測是否需要佈局,是,將會執行decorView的layout方法進行,該方法內部也會遍歷所有設定有標記位子View。

8. measure 測量

8.1 測量流程

measure

測量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, 0) & 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(0,0);//等價於下面的程式碼

// MeasureSpec.UNSPECIFIED = 0
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(0,MeasureSpec.UNSPECIFIED);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0,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,而該函式的引數正是這裡計算出來的

getChildMeasureSpec

/**
     * 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(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                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 >= 0) {
                // 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 >= 0) {
                // 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 ? 0 : 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 ? 0 : 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 = 0; 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 佈局

layout

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=0 ;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 繪製

ondraw

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

11.2 畫布的移動和剪裁2

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

view

11.3 畫布的移動和剪裁3

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

view

11.4 畫布的移動和剪裁4

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

view

12. 動畫的繪製

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

12.1 動畫繪製1

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

animation

12.2 動畫繪製2

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

animation

12.3 動畫繪製3

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

animation

參考連結