Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 中View的繪制機制源碼分析 一

Android 中View的繪制機制源碼分析 一

編輯:關於Android編程

 

差不多半年沒有寫博客了,一是因為工作比較忙,二是覺得沒有什麼內容值得寫,三是因為自己越來越懶了吧,不過最近我對Android中View的繪制機制有了一些新的認識,所以想記錄下來並分享給大家。在之後的幾篇博客中,我會給大家分享如下的內容:

1、View中measure(),layout(),draw()函數執行過程分析,帶領大家詳細分析View的尺寸測量過程,位置計算,並最終繪制到UI上的過程

2、以LinearLayout為例講解ViewGroup尺寸計算,位置計算,以及繪制過程

3、更深層次的理解LayoutParams的意義

4、LayoutInflater創建View的過程分析,詳細分析inflate(int resource, ViewGroup root, boolean attachToRoot)方法中各個參數的意義

掌握上面幾個知識點對於自定義View有非常重要的意義的,而且據我所知自定義View在面試過程中是必問知識點。

以上內容都是Android中View系統比較重要的一些內容,View系統的功能主要包括用戶輸入消息到消息處理的整個過程,以及UI的繪制,用戶輸入消息以及消息處理的部分我之前也有寫過幾篇文章,如果讀者用興趣可以去了解下:

Android 系統Touch事件傳遞機制 上:http://blog.csdn.net/yuanzeyao/article/details/37961997

Android 系統Touch事件傳遞機制 下:http://blog.csdn.net/yuanzeyao/article/details/38025165

Android 系統Key事件傳遞機制 上:http://blog.csdn.net/yuanzeyao/article/details/13630909

Android 系統Key事件傳遞機制 下:http://blog.csdn.net/yuanzeyao/article/details/13631139

 

由於涉及的內容比較多,所以我打算使用 多篇文章來講解上述內容,敬請期待。

那麼現在就開始學習View的measure過程吧,measure過程主要作用就是計算一個View的大小,這個其實很好理解,因為任何一個View在繪制到UI上時,必須事先知道這個View的大小,不然是無法繪制的。

平時我們在指定一個view的大小時,通常就是在xml文件中設置layout_width和layout_hegiht屬性,這裡我要提出一個問題:為什麼View的寬度和高度對應的屬性名前面有layout而不是直接叫width和height?先記住這個問題吧,等你看完本文的內容相信你就明白了。其實measuer過程就將layout_width和layout_height這些屬性變為具體的數字大小。

 

當我們想要將一個xml文件顯示到UI上時,通常就是將該xml文件的id傳入到Activity的setContentView中去,其實最終就會調用到ViewRoot的performTraversals方法,此方法承擔了Android的View的繪制工作,這個方法代碼非常多,但是邏輯非常簡單,主要包含了三個階段:

第一個階段就是我們今天要學習的measure,第二個階段就是layout,第三個階段就是draw,measure階段就是得到每個View的大小,layout階段就是計算每個View在UI上的坐標,draw階段就是根據前面兩個階段的數據進行UI繪制。

 

首先我們看看ViewRoot的performTraversals方法的部分代碼(使用的2.3代碼,選擇2.3代碼的原因是因為2.3的版本邏輯比4.x版本簡單,而且主要邏輯還是一樣的)

 

    private void performTraversals() {
        // Section one mView就是DecorView,
        final View host = mView;



		//Section two
        int desiredWindowWidth;
        int desiredWindowHeight;
        int childWidthMeasureSpec;
        int childHeightMeasureSpec;

        ...


        Rect frame = mWinFrame;
        if (mFirst) {
            fullRedrawNeeded = true;
            mLayoutRequested = true;

            DisplayMetrics packageMetrics =
                mView.getContext().getResources().getDisplayMetrics();
			//Section three
            desiredWindowWidth = packageMetrics.widthPixels;
            desiredWindowHeight = packageMetrics.heightPixels;

            // For the very first time, tell the view hierarchy that it
            // is attached to the window.  Note that at this point the surface
            // object is not initialized to its backing store, but soon it
            // will be (assuming the window is visible).
           ...

        } else {
			//Section four
            desiredWindowWidth = frame.width();
            desiredWindowHeight = frame.height();
            if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
                if (DEBUG_ORIENTATION) Log.v(ViewRoot,
                        View  + host +  resized to:  + frame);
                fullRedrawNeeded = true;
                mLayoutRequested = true;
                windowResizesToFitContent = true;
            }
        }

   

        boolean insetsChanged = false;

        if (mLayoutRequested) {
            // Execute enqueued actions on every layout in case a view that was detached
            // enqueued an action after being detached
            getRunQueue().executeActions(attachInfo.mHandler);


			...
			//Section five
            childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);

            // Ask host how big it wants to be
            if (DEBUG_ORIENTATION || DEBUG_LAYOUT) Log.v(ViewRoot,
                    Measuring  + host +  in display  + desiredWindowWidth
                    + x + desiredWindowHeight + ...);
			//Section six
            host.measure(childWidthMeasureSpec, childHeightMeasureSpec);

            if (DBG) {
                System.out.println(======================================);
                System.out.println(performTraversals -- after measure);
                host.debug();
            }
        }

        ....
    }

 

上面的代碼就是第一階段的主要代碼,請看代碼中的Section one部分,這裡定義了一個View 類型的變量host,它被賦值mView,這裡我想說的僅僅是mView就是一個界面的DecorView,如果你還不熟悉DecorView可以看看我的另外一篇文章:

《窗口的創建過程》,Section two分別定義了4個int 類型的變量,前面兩個變量在Section three部分或者Section four部分賦值,通常第一次進來是在Section three裡面進行賦值,也就是說desiredWindowWidth和disireWindowHeight分別是手機屏幕的寬和高(當然並不總是這樣的,這裡我們只用考慮簡單的一種情況),在Section five部分分別對childWidthMeasureSpec和childHeightMeasureSpec進行賦值,這裡調用了一個getRootMeasureSpec的方法,我們後面再分析它。在Setion six部分調用host.measure來計算View的大小,到這裡performTraversals中mersure的調用過程就算結束了,但是getRootMeasureSpec和host的measure方法我們還不清楚它們到底做了什麼,下面就來分析這兩個方法吧:

先看看getRootMeasureSpec方法吧。

 

    private int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

看了實現之後,你是不是覺得這個方法實現超簡單,以getRootMeasureSpec(desiredWindowWidth,lp.width)為例,我們知道第一個參數就是屏幕的寬度,第二個參數是一個View的LayoutParams中的width屬性,其實這個參數是在Activity的

 

 void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);
    }

makeVisible方法傳入的,makeVisible是在Activity的onResume裡面調用,我們先不關心這個,我們關心的是這個lp是怎麼創建的,我們看看getWindow.getAttributes()做了什麼吧

 

 

  // The current window attributes.
    private final WindowManager.LayoutParams mWindowAttributes =
        new WindowManager.LayoutParams();

通過源碼,找到Window的getAttributes方法,該方法返回mWindowAttributes值,我們看看WindowManager.LayoutParams這個類的空構造函數吧

 

 

   
        public LayoutParams() {
            super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
            type = TYPE_APPLICATION;
            format = PixelFormat.OPAQUE;
        }

看了構造函數後,我們發現layout_width和laout_height都是MATCH_PARENT。關於lp這個參數我們先看到這裡,我們繼續看getRootMeasureSpec這個方法,

 

這裡出現了一個MeasureSpec的陌生類,先看看MeasureSpec是何方聖神。MeasureSpec是定義在View中的一個內部類,這個類裡面有幾個比較重要的常量:

 

 

       private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;

 

我們知道java中的int類型占用32位,隨意這幾個變量在內存中的表現形式如下:

MODE_MASK: 11000000 00000000 00000000 00000000

UNSPECIFIED: 000000000 00000000 00000000 00000000

EXACTLY: 01000000 00000000 00000000 00000000

AT_MOST: 10000000 00000000 00000000 00000000

也就是說每個高2位表示的model,第30位才真正表示尺寸的大小

 

有了上面的基礎之後,相信理解下面三個方法就不難了

 

/**
         * Creates a measure specification based on the supplied size and mode.
         *
         * The mode must always be one of the following:
         * 
  • *
  • {@link android.view.View.MeasureSpec#UNSPECIFIED}
  • *
  • {@link android.view.View.MeasureSpec#EXACTLY}
  • *
  • {@link android.view.View.MeasureSpec#AT_MOST}
  • *
* * @param size the size of the measure specification * @param mode the mode of the measure specification * @return the measure specification based on size and mode */ public static int makeMeasureSpec(int size, int mode) { return size + mode; } /** * Extracts the mode from the supplied measure specification. * * @param measureSpec the measure specification to extract the mode from * @return {@link android.view.View.MeasureSpec#UNSPECIFIED}, * {@link android.view.View.MeasureSpec#AT_MOST} or * {@link android.view.View.MeasureSpec#EXACTLY} */ public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } /** * Extracts the size from the supplied measure specification. * * @param measureSpec the measure specification to extract the size from * @return the size in pixels defined in the supplied measure specification */ public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); }
第一個方法makeMeasureSpec就是講size和mode相加返回其結果,第二個getMode就是獲取高2位的值,getSize就是獲取低30位的值

 

 

看明白了這裡,我們就回到getRootMeasureSpec吧,我們知道lp.width屬性通常有三種:match_parent(fill_parent),wrap_content,具體一個大小(如100dip),而這裡通過我們上面的分析,知道寬和高均是match_parent。通過代碼我們知道這三種情況對應的mode分別是:

EXACTLY,AT_MOST,EXACTLY,也就是說math_parent和具體的大小(100dip)對應的都是EXACTLY。最後根據得到的mode和屏幕的寬度調用makeMeasureSpec方法得到一個int類型的值賦值給childWidthMeasureSpec,同理得到了childHeightMeasureSpec,並將這兩個值傳入measure中。下面我們就看看measure做了什麼

 

由於這裡調用的是host的measure,而host其實是一個FrameLayout,所以我不打算繼續使用這個例子將View的測量過程了,但是ViewGroup是沒有改寫measure的,所以其實調用的還是View的measure方法,measure方法的源碼如下:

 

 

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||
                widthMeasureSpec != mOldWidthMeasureSpec ||
                heightMeasureSpec != mOldHeightMeasureSpec) {

            // first clears the measured dimension flag
            mPrivateFlags &= ~MEASURED_DIMENSION_SET;

            if (ViewDebug.TRACE_HIERARCHY) {
                ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE);
            }

            // measure ourselves, this should set the measured dimension flag back
            onMeasure(widthMeasureSpec, heightMeasureSpec);

            // flag not set, setMeasuredDimension() was not invoked, we raise
            // an exception to warn the developer
            if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {
                throw new IllegalStateException(onMeasure() did not set the
                        +  measured dimension by calling
                        +  setMeasuredDimension());
            }

            mPrivateFlags |= LAYOUT_REQUIRED;
        }

        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;
    }

我們看到measure方法其實是final的,所以ViewGroup是無法改寫此方法的。通常一個具體的ViewGroup都是改寫onMeasure方法,這點你可以去看看LinearLayout和FrameLayout,他們在onMeasure方法裡面都間接調用了ViewGroup的measureChildWithMargins方法,今天我們就以measureChildWithMargins這個方法為入口分析View的測量過程。measureChildWithMargins方法的源碼如下:

 

    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);
    }
這裡我們簡化下情況,我們假設ViewGroup裡面所有的孩子都是View,沒有ViewGroup。

 

下面我們分三步來分析measureChildWithMargins方法:

1、獲取孩子的LayoutParams

2、調用getChildMeasureSpec方法得到孩子的measureSpec(包括widthSpec和heightSpec)

我們看看getChildMeasureSpec做了什麼,先看看它的幾個參數,以獲取孩子的widthSpec為例 ,第一個參數是ViewGroup的widthSpec,第二個參數是ViewGroup已經被使用的width,第三個是lp.width,接下來看看源碼:

 

   */
    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 = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

相信有了前面的基礎,看這段代碼應該很容易,其實就是根據ViewGroup的mode和size以及lp.width的值來創建View的measureSpec。現在知道我前面提的問題的答案了嗎,為什麼width前面要加一個layout,因為子View的大小時自己(子View)和ViewGroup(父View)共同決定的。

 

 

回到measureChildWithMargins 看第三步:調用了child.measure。並且參數就是第二步中得到的,另外注意這個child就是一個普通的View(因為我們已經假設ViewGroup裡面沒有ViewGroup,只有View)

 

 

由於是View調用measure,所以measure中調用onMeasure也是View中的,我們看看View的onMeasuere方法吧

 

 

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

 

這裡出現了一個重要的方法getDefaultSize,其代碼如下:

 

    public static int getDefaultSize(int size, int measureSpec) {
        int result = 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;
    }

該方法根據measureSpec的mode決定返回值是size還是specSize。在多數情況下載mode是AT_MOST或者EXACTLY,(UNSPECIFIED通常出現在我們為了獲得某個view的大小時,調用此view.measure(0,0)的時候出現.),在onMeasure中會調用setMeasuredDimension()方法將得到的大小分別賦值給mMeasuredWidth,mMeasuredHeight,從而View的大小就測量完成了。

 

代碼如下:

 

  protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= MEASURED_DIMENSION_SET;
    }

到這裡View的測量過程告一段落了,至於ViewGroup的測量過程在下篇文章中使用LinearLayout分析一下吧。

 

 

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