Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> android-----View工作原理系列(一)

android-----View工作原理系列(一)

編輯:關於Android編程

這幾天開始了View工作原理的學習,當然最初肯定是從View的繪制過程開始的,至於其中的源碼分析網上挺多的,我只會在隨後的博客中做些總結,並不從代碼層面進行分析,畢竟網上資料已經很多了,這篇博客我主要涉及的是invalidate以及與之有關的postInvalidate最後講解下requestLayout,他們三個都是用於視圖重繪的,同樣我也不會從代碼層面進行分析,首先會通過實例來提出一個問題,之後把invalidate源碼層面的執行過程通過總結的方式呈現出來;

本文涉及到的源碼是android4.3版本的;

好了,我們開始吧!!!

我們查看下測試的布局文件:

 


    
         
    
比較簡單就是RelativeLayout嵌套LinearLayout,LinearLayout嵌套TextView,只不過這裡我們都是自定義的而已,主要是為了能夠打印出我們想要的Log信息,在MyRelativeLayout和MyLinearLayout中我們重寫了invalidate()、invalidateDrawable(Drawable drawable)、invalidate(int l, int t, int r, int b)、invalidateChildInParent(int[] location, Rect dirty),在MyTextView中我們重寫了invalidate()、invalidateDrawable(Drawable drawable)、invalidate(int l, int t, int r, int b);

先來看看測試效果:

\

可以看到我們的測試樣例是在點擊紅色區域(表示MyTextView)的時候會將該區域顏色設置為黑色,調用的方法是setBackgroundColor,而這個方法會去調用MyTextView的invalidateDrawable,invalidateDrawable方法會去調用invalidate(int l, int t, int r, int b),那麼我們就可以在這個方法裡面獲取到布局的四個點的值啦!

下面我們查看Logcat輸出:

\ 可以看到首先調用的是我們MyTextView的invalidateDrawable,隨後獲取到他的四個點的值,下面我們對輸出一一來進行解釋:

首先需要明確的是Logcat這裡的輸出數字是像素值,也就是pix指,而我們布局中設置的是dip值,當然因為dp=dip,所以設置dp也一樣,需要知道的是這裡1dp/dip=1.5pix;

先來看這5行輸出:

07-04 05:48:05.936: I/System.out(2239): MyTextView-->invalidate-->l: 0 t: 0 r: 75 b: 75
07-04 05:48:05.945: I/System.out(2239): MyLinearLayout-->invalidateChildInParent-->before-->l: 0 t: 0 r: 75 b: 75
07-04 05:48:05.945: I/System.out(2239): MyLinearLayout-->invalidateChildInParent-->location[0]: 30
07-04 05:48:05.945: I/System.out(2239): MyLinearLayout-->invalidateChildInParent-->location[1]: 37

07-04 06:01:11.445: I/System.out(2303): MyLinearLayout-->invalidateChildInParent-->after-->l: 30 t: 37 r: 105 b: 112

\

從上面的圖中你會發現,我們最裡面的正方形也就是MyTextView的寬度和高度為75pix,所以在點擊MyTextView之後輸出的(0,0,75,75)是相對於黑色框的左上角而言的,也就是把黑色框的左上角作為原點;接著會對MyTextView的父布局MyLinearLayout進行查看是否需要重新繪制,其實就是設置一些標志啦,隨後的重繪工作會根據這些標志來判斷是否需要重新繪制,可以發現這裡調用的invalidateChildInParent方法,此處的30表示MyTextView的左邊緣距離MyLinearLayout左邊緣的像素距離,37值的是MyTextView的上邊緣距離MyLinearLayout上邊緣的像素距離,那麼在invalidateChildInParent執行結束之後輸出的位置(30,37,105,112)指的是黑色框的左,上,右,下,四個位置相對於綠色框左上角的位置,也就是此時把綠色框的左上角作為了原點;

在將MyLinearLayout標志設置結束之後,接下就是MyLinearLayout的父布局MyRelativeLayout了,我們來看下隨後的輸出:

07-04 06:01:11.445: I/System.out(2303): MyRelativeLayout-->invalidateChildInParent-->before-->l: 30 t: 37 r: 105 b: 112
07-04 06:01:11.445: I/System.out(2303): MyRelativeLayout-->invalidateChildInParent-->location[0]: 165
07-04 06:01:11.445: I/System.out(2303): MyRelativeLayout-->invalidateChildInParent-->location[1]: 270
07-04 06:01:11.445: I/System.out(2303): MyRelativeLayout-->invalidateChildInParent-->after-->l: 195 t: 307 r: 270 b: 382

在上面的MyLinearLayout結束之後,黑色框的位置變成了(30,37,105,112),當然這也就是開始執行MyRelativeLayout的位置了,此處的165,270代表的含義見下圖:

\

即165表示MyLinearLayout左邊緣和MyRelativeLayout左邊緣的距離,270表示MyLinearLayout上邊緣和MyRelativeLayout上邊緣的距離,執行完MyRelativeLayout的invalidateChildInParent之後,MyTextView的位置變成了(195,307,270,382),也就是l和r分別加了165,t和b分別加了270,這時候MyTextView的位置是相對於MyRelativeLayout左上角的;

這樣,我們對上面測試的輸出解釋完了,你會發現在MyTextView進行重繪的過程中,首先執行的是MyTextView的invalidate,接著執行的是MyTextView父布局的invalidateChildInParent方法,這個過程和我們之前的事件分發過程以及View的measure、layout、draw過程正好相反,原因在於他是我們measure、layout、draw過程的前一步操作,也就是設置一些需不需要重繪的標志信息,隨後的重繪過程會根據這些標志信息進行是否重繪的操作,在這裡有一點需要說明一下,其實你調用View的invalidate方法進行重繪,實際上重繪的時候是只有draw過程的,並不涉及到measure以及layout過程,這一點和通過requestLayout重繪有點區別;

下面我用敘述的方式講解下invalidate的執行流程:

我們平常使用invalidate的方式是:view.invalidate();

(1):這裡的invalidate()實際上調用的是invalidate(true),這裡的參數true表示進行的是否是完全重繪,完全與否就是是否使用繪制緩存繪制的問題;

(2):進入invalidate之後首先會判斷是否要跳過繪制,接著分硬件加速可用與不可用兩種情況來調用ViewParent的invalidateChild方法,這裡的ViewParent一般來說指的就是ViewGroup,因為ViewGroup實現了ViewParent接口,ViewGroup中的invalidateChild方法需要兩個參數,第一個參數就是我們當前的View了,第2個參數是我們想要重繪區域的矩陣(實際上就是左、上、右、下四個位置),如果是硬件加速可用的話,傳入的第2個參數是null,表示繪制整個View的層級,硬件加速不可用的話,則繪制我們傳進來Rect對象的局域;

(3):接下來進行到ViewGroup的invalidateChild方法裡面了,這個方法裡面存在一個do while循環,這個循環的主要作用是將View的父View以及祖先View的可顯矩陣和當前View的矩陣做運算,計算出當前View的Rect相對於不同祖先View的坐標值而已,在do while循環裡面可以看到最關鍵的是執行ViewParent的invalidateChildInParent方法了,這個方法在ViewGroup以及ViewRootImpl都有實現,如果parent類型是ViewGroup的話,執行的是ViewGroup的invalidateChildInParent方法,ViewGroup裡面的invalidateChildInParent主要進行的也是一些矩陣相交的運算,代碼有點復雜,只需要了解流程就可以了,因為我們的每一個窗體的最上層肯定就是ViewRootImpl,而他是不屬於ViewGroup類型的,那麼到最後執行的將是ViewRootImpl的invalidateChildInParent方法了;

(4):ViewRootImpl裡面的invalidateChildInParent代碼比較關鍵,他首先會檢查當前線程是否是UI線程,這點很容易理解,如果不是UI線程,你都沒權利更新UI的,更別說View的繪制了,隨後你會看到一個scheduleTraversals方法,而這個方法最終會通過postCallback來調用TraversalRunnable對象的run方法,而在run方法裡面會執行doTraversal方法,在doTraversal方法裡面就會執行我們很熟悉的performTraversals方法了,也就是我們常見的分析View繪制過程博客最開始執行的方法了;

(5):接下來的過程就是一般View的繪制過程了,執行measure、layout、draw三個步驟,先測量父View接著測量子View,繪制父View,繪制子View,但是對於調用View的invalidate方法進行重繪來說是不會執行measure、layout這兩個過程的,具體是怎麼屏蔽這兩個過程的呢?是通過在從子View到父View的計算矩陣的過程中沒有設置與measure、layout過程有關的標志實現的;

介紹完invalidate的過程,那postInvalidate又是怎麼執行的呢?其實從上面的分析中你也看出來了,invalidate是只適用於UI線程的,非UI線程中執行invalidate方法是會拋異常的,那麼怎麼樣才能在非UI線程中進行View視圖重繪呢?就是我們這裡的postInvalidate了,其實了解Handler機制的一下子就會明白這裡的post是什麼意思了,因為從postInvalidate調用invalidate源碼比較少,所以我貼出來分析一下:

首先是postInvalidate方法,他有兩個重載實現:

 

  public void postInvalidate() {
        postInvalidateDelayed(0);
    }
 public void postInvalidate(int left, int top, int right, int bottom) {
        postInvalidateDelayed(0, left, top, right, bottom);
    }
第1個是我們經常用的形式,第2個是對指定區域進行重繪,一般我們自己的話很少使用;

 

我們這裡只分析第一個postInvalidate方法,可以看到他執行的是postInvalidateDelayed,傳入的參數是0,來看postInvalidateDelayed方法:

 

    public void postInvalidateDelayed(long delayMilliseconds) {
        // We try only with the AttachInfo because there's no point in invalidating
        // if we are not attached to our window
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
        }
    }
可以看到調用的是ViewRootImpl對象的dispatchInvalidateDelayed方法,將當前View作為參數傳遞進去;

 

 

 public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
        Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
        mHandler.sendMessageDelayed(msg, delayMilliseconds);
    }
在dispatchInvalidateDelayed方法中我們將View封裝成Message對象,通過handler對象將他發送出去,這裡的mHandler是ViewRootHandler類型的對象,所以我們需要查看mHandler的handleMessage中case為MSG_INVALIDATE的執行語句:

 

 

          case MSG_INVALIDATE:
                ((View) msg.obj).invalidate();
                break;
可以看到他執行的就是View裡面的invalidate方法了;

至此,我們知道了postInvalidate實際上就是在非UI線程下的invalidate而已啦;

最後就是講下requestLayout了,有時候我們在進行View繪制的時候也會通過調用view.requestLayout來實現,那麼requestLayout和invalidate方式有什麼區別呢?requestLayout是怎麼實現的呢?鑒於他的源碼比較短,在這裡我從源碼角度講解下:

先來看看View的requestLayout方法:

 

public void requestLayout() {
        if (mMeasureCache != null) mMeasureCache.clear();

        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
            // Only trigger request-during-layout logic if this is the view requesting it,
            // not the views in its parent hierarchy
            ViewRootImpl viewRoot = getViewRootImpl();
            if (viewRoot != null && viewRoot.isInLayout()) {
                if (!viewRoot.requestLayoutDuringLayout(this)) {
                    return;
                }
            }
            mAttachInfo.mViewRequestingLayout = this;
        }

        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;

        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
            mAttachInfo.mViewRequestingLayout = null;
        }
    }

代碼16/17行會進行一些標志的設置,這個和invalidate的過程是一樣的,在invalidate裡面,我們同樣也會進行一些標志的設定,接著有父布局的話執行第20行父布局的requestLayout方法,如果父布局還有父布局的話,還會繼續調用父布局的requestLayout方法,也就是requestLayout層層向上傳遞,這點和invalidate的過程是類似的,知道DecorView,即根View,而跟View又會傳給他的父布局ViewRootImpl也就是最終會執行ViewRootImpl的requestLayout方法,我們快來看看:

 

@Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }
在這個方法裡我們看到了scheduleTraversals,這個方法在invalidate裡面出現過,接下來的過程就和上面的過程一樣了;

 

至此,invalidate、postInvalidate以及requestLayout分析結束了,帶著為什麼既要有invalidate還要有requestLayout的疑問,我們來進行總結下:

(1):postInvalidate是在非UI線程中通過handler調用invalidate的方式實現視圖重繪的;

(2):invalidate是在UI線程中執行的,如果在非UI線程中執行會拋異常;

(3):requestLayout的執行流程和invalidate很類似,都是從當前需要重繪的view開始層層向上傳遞,在這個過程中設置一些標志用來在隨後的重繪過程中根據這些標志查看是否需要重繪;

(4):最後就是為什麼有invalidate還要requestLayout了,原因就是加入我們只需要重繪View,也就是View自身的LayoutParams屬性值並沒有發生變化的話,這時候我們只需要invalidate方法來刷新View即可,他並不會執行View繪制中的measure和layout這兩個過程;但是當我們View的LayoutParams屬性發生變化的時候,invalidate顯然不能達到要求,因為我們需要measure和layout過程,那麼這時候就可以調用requestLayout來實現了,這是兩者的最大區別;

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved