Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> View的源碼分析(繪制流程以及刷新機制)

View的源碼分析(繪制流程以及刷新機制)

編輯:關於Android編程

一、基本知識

1:ViewRoot

ViewRoot是連接WindowManager與DecorView的紐帶,View的整個繪制流程的三大步(measure、layout、draw)都是通過ViewRoot完成的。當Activity對象被創建完畢後,會將DecorView添加到Window中(Window是對窗口的抽象,DecorView是一個窗口的頂級容器View,其本質是一個FrameLayout),同時會創建ViewRootImpl(ViewRoot的實現類)對象,並將ViewRootImpl與DecorView建立關聯。關於ViewRoot,我們只需要知道它是聯系GUI管理系統和GUI呈現系統的紐帶。View的繪制流程從ViewRoot的performTraversals方法開始,經過measure、layout、draw三大過程完成對一個View的繪制工作。peformTraversal方法內部會調用measure、layout、draw這三個方法,這三個方法內部又分別調用onMeasure、onLayout、onDraw方法。

2:MeasureSpec

MeasureSpec為一個32位的int值,高2位代表SpecMode,低30位代表SpecSize,前者指測量模式,後者指某種測量模式下的規格大小。在一個View的measure過程中,系統會將該View的LayoutParams結合父容器的“要求”生成一個MeasureSpec,這個MeasureSpec說明了應該怎樣測量這個View。

(1)三種 SpecMode:
UNSPECIFIED:父容器不對View作任何要求,通常用於系統內部,表示一種測量的狀態。

EXACTLY:父容器已經檢測出View所需要的精確大小,這種測量模式下View的測量值就是SpecSize的值。這個SpecMode對應於LayoutParams中的match_parent和給出具體大小這兩種模式。

AT_MOST:父容器指定了一個可用大小即SpecSize,View的大小不能大於此值,可用大小取決於不同View的具體實現。這個SpecMode對應於LayoutParams中的wrap_content。

(2)對於DecorView,它的MeasureSpec由窗口尺寸和其自身的LayoutParams共同確定;對於普通View,他的MeasureSpec由父容器的MeasureSpec和其自身的LayoutParams共

同確定。

二、View的繪制流程及源碼分析

整個View樹的繪圖流程是在ViewRootImpl類的performTraversals()方法開始的,該函數做的執行過程主要是根據之前設置的狀態,判斷是否重新計算視圖大小(measure)、是否重新放置視圖的位置(layout)、以及是否重繪 (draw),其核心也就是通過判斷來選擇順序執行這三個方法中的哪個,如下:

private void performTraversals() {
        ......
        //最外層的根視圖的widthMeasureSpec和heightMeasureSpec由來
        //lp.width和lp.height在創建ViewGroup實例時等於MATCH_PARENT
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
        ......
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        ......
        mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
        ......
        mView.draw(canvas);
        ......
    }
private int getRootMeasureSpec(int windowSize, int rootDimension) { int measureSpec; switch (rootDimension) { case ViewGroup.LayoutParams.MATCH_PARENT: measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); break; case ViewGroup.LayoutParams.WRAP_CONTENT: measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); break; default: measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); break; } return measureSpec; }

 


1:Measure源碼分析
 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ......
        //回調onMeasure()方法
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        ......
    }
(1):普通View的Measure過程

 

普通View的measure方法是由ViewGroup在measureChild方法中調用的(即完成了measure過程從ViewGroup到子View的傳遞),ViewGroup調用其子View的measure時即傳入了該子View的widthMeasureSpec和heightMeasureSpec。注意到measure是一個final方法,因此要實現自定義的measure過程,需要重寫onMeasure方法,onMeasure方法源碼如下

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
             getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
 }
 public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        //通過MeasureSpec解析獲取mode與size
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }
正常情況下(SpecMode為AT_MOST或EXACTLY),getDefaultSize獲取的尺寸大小即為specSize。由以上代碼還可知道,直接繼承View的自定義控件需要重寫onMeasure方法並設置wrap_content時的自身大小,否則在布局中使用wrap_content就相當於使用match_parent的效果。

(2):ViewGroup的繪制過程

ViewGroup中的onMeasure()方法主要是遍歷子View,先測量所有子View的之後設置自身的大小值

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     if (mOriention == VERTICAL) {
         measureVertical(widthMeasureSpec, heightMeasureSpec);
     } else {
         measureHorizontal(widthMeasureSpec, heightMeasureSpec);
     }
 }
以MeasureVertical為例:
 for (int i = 0; i < count; ++i) {
      final View child = getVirtualChildAt(i);
      . . .
      //Determine how big this child would like to be. If this or previous children have given a weight, 

       //then we allow it to use all available space (and we will shrink things later if needed).
     measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, totalHeight == 0 ? mTotalLength : 0);
      
      if (oldHeight != Integer.MIN_VALUE) {
          lp.height = oldHeight;
     }
 
     final int childLength = child.getMeasuredHeight();
     final int totalLength = mTotalLength;
     mTotalLength = Math.max(totalLength, totalLength+childHeight+lp.topMargin+lp.bottomMargin+getNextLocationOffset(child));
 }
在measureChildBeforeLayout方法內部會調用子元素的measure方法,這樣會依次讓每個子元素進入measure過程。mTotalLength表示LinearLayout在豎直方向上的尺寸,每完成一個子元素的measure過程,它的值也會相應增加。測量完子元素後,LinearLayout會測量自身的大小。


對於一般的ViewGroup主要通過其measureChildren方法完成其子View的measure過程,上面垂直LinearLayout中調用的measureChildBeforeLayout可以看做是measureChildren的一個“變種”,measureChildren方法代碼如下:

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);  
              }  
      }  
 }
通過measureChild()完成對子View的Measure();
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);  
 }  

注意在這裡,在執行child.measure方法前,就已經通過getChildMeasureSpec獲取了子View的MeasureSpec。getChildMeasureSpec根據子View的LayoutParams和父容器的MeasureSpec來決定子View的MeasureSpec,getChildMeasureSpec的具體作用是:

a. 當childLayoutParams指定為為具體的大小時:若parentSpecMode為EXACTLY,則childSpecMode為EXACTLY,childSpecSize為childSize(layout_width和layout_height中指定的具體大小);若parentSpecMode為AT_MOST,則childSpecMode和childSpecSize分別為EXACTLY和childSize。

b. 當childLayoutParams為match_parent時:若parentSpecMode為EXACTLY,則childSpecMode和childSpecSize分別為EXACTLY和parentSize(父容器中可用的大小);若parentSpecMode為AT_MOST,則childSpecMode和childSpecSize分別為AT_MOST和parentSize。

c. 當childLayoutParams為wrap_content時:若parentSpecMode為EXACTLY,則childSpecMode和childSpecSize分別為AT_MOST和parentSize;若parentSpecMode為

AT_MOST,則childSpecMode和childSpecSize分別為AT_MOST和parentSize。

2:layout()源碼分析

layout過程用來確定View在父容器中的位置,因而是由父容器獲取子View的位置參數後,調用child.layout方法並傳入已獲取的位置參數,從而完成對子View的layout。當ViewGroup的位置被確定後,它在onLayout中會遍歷所有子元素並調用其layout方法,在layout方法中子元素的onLayout又會被調用。layout方法確定先View本身的位置,再調用onLayout方法確定所有子元素的位置。

以LinearLayout為例:

protected void onLayout(boolean changed, int l, int t, int r, int b) {
     if (mOriention == VERTIVAL) {
         layoutVertical(l, t, r, b);
     } else {
         layoutHorizontal(l, t, r, b);
     }
 }

void layoutVertical(int left, int top, int right, int bottom) {
      . . .
      final int count = getVirtualChildCount();
      for (int i = 0; i < count; i++) {
          final View child = getVirtualChildAt(i);
          if (child == null) {
              childTop += measureNullChild(i);
          } else if (child.getVisibility() != GONE) {
              final int childWidth = child.getMeasuredWidth();
             final int childHeight = child.getMeasuredHeight();
             
             final int LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
             . . .
             if (hasDividerBeforeChildAt(i)) {
                 childTop += mDividerHeight;
             }
  
             childTop += lp.topMargin;
             setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight);
             childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
             
             i += getChildrenSkipCount(child, i);
         }
     }
 }
通過 setChildFrame設置子View的位置。

3:draw()源碼分析

draw()的過程主要有以下幾部分

 public void draw(Canvas canvas) {
        // Step 1, draw the background, if needed
        ......
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)
        ......

        // Step 2, save the canvas' layers
        ......
            if (drawTop) {
                canvas.saveLayer(left, top, right, top + length, null, flags);
            }
        ......

        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children,對當前View的所有子View進行繪制,如果當前的View沒有子View就不需要進行繪制。
        dispatchDraw(canvas);

        // Step 5, draw the fade effect and restore layers
        ......
        if (drawTop) {
            matrix.setScale(1, fadeHeight * topFadeStrength);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, top, right, top + length, p);
        }
        ......

        // Step 6, draw decorations (scrollbars)  對view的滾動條進行繪制
        onDrawScrollBars(canvas);
        ......
    }
三、View的刷新機制的源碼分析

1:invalidate()

public void invalidate(Rect dirty) {
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        //實質還是調運invalidateInternal方法
        invalidateInternal(dirty.left - scrollX, dirty.top - scrollY,
                dirty.right - scrollX, dirty.bottom - scrollY, true, false);
    }
View的invalidate(invalidateInternal)方法實質是將要刷新區域直接傳遞給了父ViewGroup的invalidateChild方法,在invalidate中,調用父View的invalidateChild,這是一個從當前向上級父View回溯的過程,每一層的父View都將自己的顯示區域與傳入的刷新Rect做交集 。所以我們看下ViewGroup的invalidateChild方法,源碼如下:
public final void invalidateChild(View child, final Rect dirty) {
        ViewParent parent = this;
        final AttachInfo attachInfo = mAttachInfo;
        ......
        do {
            ......
            //循環層層上級調運,直到ViewRootImpl會返回null
            parent = parent.invalidateChildInParent(location, dirty);
            ......
        } while (parent != null);
    }

這個過程最後傳遞到ViewRootImpl的invalidateChildInParent方法結束,所以我們看下ViewRootImpl的invalidateChildInParent方法,如下:
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
......
//View調運invalidate最終層層上傳到ViewRootImpl後最終觸發了該方法
scheduleTraversals();
......
return null;
}

看見沒有?這個ViewRootImpl類的invalidateChildInParent方法直接返回了null,也就是上面ViewGroup中說的,層層上級傳遞到ViewRootImpl的invalidateChildInParent方法結束了那個do while循環。看見這裡調運的scheduleTraversals這個方法嗎?scheduleTraversals會通過Handler的Runnable發送一個異步消息,調運doTraversal方法,然後最終調用performTraversals()執行重繪。開頭背景知識介紹說過的,performTraversals就是整個View數開始繪制的起始調運地方,所以說View調運invalidate方法的實質是層層上傳到父級,直到傳遞到ViewRootImpl後觸發了scheduleTraversals方法,然後整個View樹開始重新按照上面分析的View繪制流程進行重繪任務。

2:postInvalidate源碼分析

 

 public void postInvalidate() {
        postInvalidateDelayed(0);
    }

 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;
        //核心,實質就是調運了ViewRootImpl.dispatchInvalidateDelayed方法
        if (attachInfo != null) {
            attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
        }
    }

 public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
        Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
        mHandler.sendMessageDelayed(msg, delayMilliseconds);
    }

public void handleMessage(Message msg) {
    ......
    switch (msg.what) {
    case MSG_INVALIDATE:
        ((View) msg.obj).invalidate();
        break;
    ......
    }
    ......
}


常見的引起invalidate方法操作的原因主要有:

直接調用invalidate方法.請求重新draw,但只會繪制調用者本身。

觸發setSelection方法。請求重新draw,但只會繪制調用者本身。

觸發setVisibility方法。 當View可視狀態在INVISIBLE轉換VISIBLE時會間接調用invalidate方法,繼而繪制該View。當View的可視狀態在INVISIBLE\VISIBLE 轉換為GONE狀態時會間接調用requestLayout和invalidate方法,同時由於View樹大小發生了變化,所以會請求measure過程以及draw過程,同樣只繪制需要“重新繪制”的視圖。

觸發setEnabled方法。請求重新draw,但不會重新繪制任何View包括該調用者本身。

觸發requestFocus方法。請求View樹的draw過程,只繪制“需要重繪”的View。

3:程序在第一次是通過什麼繪制View的呢?

 @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        ......
        //如果mContentParent為空進行一些初始化,實質mContentParent是通過findViewById(ID_ANDROID_CONTENT);獲取的id為content的FrameLayout的布局(不清楚的請先看《Android應用setContentView與LayoutInflater加載解析機制源碼分析》文章)
        if (mContentParent == null) {
            installDecor();
        } 
        ......
        //把我們的view追加到mContentParent
        mContentParent.addView(view, params);
        ......
    }
public void addView(View child, int index, LayoutParams params) { ...... //該方法稍後後面會詳細分析 requestLayout(); //重點關注!!! invalidate(true); ...... }

 


對於requestLayout()的分析
public void requestLayout() {
        ......
        if (mParent != null && !mParent.isLayoutRequested()) {
            //由此向ViewParent請求布局
            //從這個View開始向上一直requestLayout,最終到達ViewRootImpl的requestLayout
            mParent.requestLayout();
        }
        ......
    }

看見沒有,當我們觸發View的requestLayout時其實質就是層層向上傳遞,直到ViewRootImpl為止,然後觸發ViewRootImpl的requestLayout方法,如下就是ViewRootImpl的requestLayout方法:

    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            //View調運requestLayout最終層層上傳到ViewRootImpl後最終觸發了該方法
            scheduleTraversals();
        }
    }

requestLayout()方法會調用measure過程和layout過程,不會調用draw過程,也不會重新繪制任何View包括該調用者本身。
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved