Android View繪製13問13答

希爾瓦娜斯女神發表於2016-02-06

1.View的繪製流程分幾步,從哪開始?哪個過程結束以後能看到view?

答:從ViewRoot的performTraversals開始,經過measure,layout,draw 三個流程。draw流程結束以後就可以在螢幕上看到view了。

2.view的測量寬高和實際寬高有區別嗎?

答:基本上百分之99的情況下都是可以認為沒有區別的。有兩種情況,有區別。第一種 就是有的時候會因為某些原因 view會多次測量,那第一次測量的寬高 肯定和最後實際的寬高 是不一定相等的,但是在這種情況下

最後一次測量的寬高和實際寬高是一致的。此外,實際寬高是在layout流程裡確定的,我們可以在layout流程裡 將實際寬高寫死 寫成硬編碼,這樣測量的寬高和實際寬高就肯定不一樣了,雖然這麼做沒有意義 而且也不好。

3.view的measureSpec 由誰決定?頂級view呢?

答:由view自己的layoutparams和父容器  一起決定自己的measureSpec。一旦確定了spec,onMeasure中就可以確定view的寬高了。

頂級view就稍微特殊一點,對於decorView的測量在ViewRootImpl的原始碼裡。

//desire的這2個引數就代表螢幕的寬高,
  childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
  childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
  performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);



  //decorView的measureSpec就是在這裡確定的,其實比普通view的measurespec要簡單的多
  //程式碼就不分析了 一目瞭然的東西
  private static 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;
}

4.對於普通view來說,他的measure過程中,與父view有關嗎?如果有關,這個父view也就是viewgroup扮演了什麼角色?

答:看原始碼:

//對於普通view的measure來說 是由這個view的 父view ,也就是viewgroup來觸發的。
//也就是下面這個measureChildWithMargins方法

protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
         //第一步 先取得子view的 layoutParams 引數值   
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        //然後開始計運算元view的spec的值,注意這裡看到 計算的時候除了要用子view的 layoutparams引數以外
        //還用到了父view 也就是viewgroup自己的spec的值
        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);
}






//這個算view的spec的方法 看上去一大串 但是真的邏輯非常簡單 就是根據父親viewgroup
//的meaurespec 同時還有view自己的params來確定 view自己的measureSpec。
//注意這裡的引數是padding,這個值的含義是 父容器已佔用的控制元件的大小 所以view的Specsize
//的值 你們可以看到 是要減去這個padding的值的。總大小-已經用的 =可用的。 很好理解。

//然後就是下面的switch邏輯 要自己梳理清楚。其實也不難,主要是下面幾條原則
//如果view採用固定寬高,也就是寫死的數值那種。那就不管父親的spec的值了,view的spec 就肯定是exactly 並且大小遵循layout引數裡設定的大小。

//如果view的寬高是match_parent ,那麼就要看父容器viewgroup的 spec的值了,如果父view的spec是exactly模式,
//那view也肯定是exactly,並且大小就是父容器剩下的空間。如果父容器是at_most模式,那view也是at_most 並且不會超過剩餘空間大小

//如果view的寬高是wrap_content, 那就不管父容器的spec了,view的spec一定是at_most 並且不會超過父view 剩餘空間的大小。


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;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

5.view的meaure和onMeasure有什麼關係?

答:看原始碼:

//view的measure是final 方法 我們子類無法修改的。
 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }

        // Suppress sign extension for the low bytes
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

        if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
                widthMeasureSpec != mOldWidthMeasureSpec ||
                heightMeasureSpec != mOldHeightMeasureSpec) {

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

            resolveRtlPropertiesIfNeeded();

            int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
                    mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }

            // flag not set, setMeasuredDimension() was not invoked, we raise
            // an exception to warn the developer
            if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                throw new IllegalStateException("View with id " + getId() + ": "
                        + getClass().getName() + "#onMeasure() did not set the"
                        + " measured dimension by calling"
                        + " setMeasuredDimension()");
            }

            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }

        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;

        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }

//不過可以看到的是在measure方法裡呼叫了onMeasure方法
//所以就能知道 我們在自定義view的時候一定是重寫這個方法!
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

6.簡要分析view的measure流程?

答:先回顧問題4,viewgroup 算出子view的spec以後 會呼叫子view的measure方法,而子view的measure方法 我們問題5也看過了實際上是呼叫的onMeasure方法

所以我們只要分析好onMeasure方法即可,注意onMeasure方法的引數 正是他的父view算出來的那2個spec的值(這裡view的measure方法會把這個spec裡的specSize值做略微的修改 這個部分 不做分析 因為measure方法修改specSize的部分很簡單)。

//可以看出來這個就是setMeasuredDimension方法的呼叫 這個方法看名字就知道就是確定view的測量寬高的
//所以我們分析的重點就是看這個getDefaultSize 方法 是怎麼確定view的測量寬高的
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }


//這個方法特別簡單 基本可以認為就是近似的返回spec中的specSize,除非你的specMode是UNSPECIFIED
//UNSPECIFIED 這個一般都是系統內部測量才用的到,這種時候返回size 也就是getSuggestedMinimumWidth的返回值
 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;
}

//跟view的背景相關 這裡不多做分析了
protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

7.自定義view中 如果onMeasure方法 沒有對wrap_content 做處理 會發生什麼?為什麼?怎麼解決?

答:如果沒有對wrap_content做處理 ,那即使你在xml裡設定為wrap_content.其效果也和match_parent相同。看問題4的分析。我們可以知道view自己的layout為wrap,那mode就是at_most(不管父親view是什麼specmode).

這種模式下寬高就是等於specSize(getDefaultSize函式分析可知),而這裡的specSize顯然就是parentSize的大小。也就是父容器剩餘的大小。那不就和我們直接設定成match_parent是一樣的效果了麼?

解決方式就是在onMeasure裡 針對wrap 來做特殊處理 比如指定一個預設的寬高,當發現是wrap_content 就設定這個預設寬高即可。

8.ViewGroup有onMeasure方法嗎?為什麼?

答:沒有,這個方法是交給子類自己實現的。不同的viewgroup子類 肯定佈局都不一樣,那onMeasure索性就全部交給他們自己實現好了。

9.為什麼在activity的生命週期裡無法獲得測量寬高?有什麼方法可以解決這個問題嗎?

答:因為measure的過程和activity的生命週期  沒有任何關係。你無法確定在哪個生命週期執行完畢以後 view的measure過程一定走完。可以嘗試如下幾種方法 獲取view的測量寬高。

//重寫activity的這個方法
public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus) {
            int width = tv.getMeasuredWidth();
            int height = tv.getMeasuredHeight();
            Log.v("burning", "width==" + width);
            Log.v("burning", "height==" + height);

        }
    }

或者重寫這個方法

@Override
    protected void onStart() {
        super.onStart();
        tv.post(new Runnable() {
            @Override
            public void run() {
                int width = tv.getMeasuredWidth();
                int height = tv.getMeasuredHeight();
            }
        });
    }

再或者:

@Override
    protected void onStart() {
        super.onStart();
        ViewTreeObserver observer = tv.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                int width = tv.getMeasuredWidth();
                int height = tv.getMeasuredHeight();
                tv.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            }
        });
    }

10.layout和onLayout方法有什麼區別?

答:layout是確定本身view的位置 而onLayout是確定所有子元素的位置。layout裡面 就是通過serFrame方法設設定本身view的 四個頂點的位置。這4個位置以確定 自己view的位置就固定了

然後就呼叫onLayout來確定子元素的位置。view和viewgroup的onlayout方法都沒有寫。都留給我們自己給子元素佈局

11.draw方法 大概有幾個步驟?

答: 一共是4個步驟, 繪製背景———繪製自己——–繪製chrildren—-繪製裝飾。

12.setWillNotDraw方法有什麼用?

答:這個方法在view裡。

/**
     * If this view doesn't do any drawing on its own, set this flag to
     * allow further optimizations. By default, this flag is not set on
     * View, but could be set on some View subclasses such as ViewGroup.
     *
     * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
     * you should clear this flag.
     *
     * @param willNotDraw whether or not this View draw on its own
     */
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }

用於設定標誌位的 也就是說 如果你的自定義view 不需要draw的話,就可以設定這個方法為true。這樣系統知道你這個view 不需要draw 可以優化執行速度。viewgroup 一般都預設設定這個為true,因為viewgroup多數都是隻負責佈局

不負責draw的。而view 這個標誌位 預設一般都是關閉的。

13.自定義view 有哪些需要注意的點?

答:主要是要處理wrap_content 和padding。否則xml 那邊設定這2個屬性就根本沒用了。還有不要在view中使用handler 因為人家已經提供了post方法。如果是繼承自viewGroup,那在onMeasure和onLayout裡面 也要考慮

padding和layout的影響。也就是說specSize 要算一下 。最後就是如果view的動畫或者執行緒需要停止,可以考慮在onDetachedFromWindow裡面來做。

針對上述的幾點,給出幾個簡單的自定義view 供大家理解。

給出一個圓形的view 範例:

package com.example.administrator.motioneventtest;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;

/**
 * Created by Administrator on 2016/2/4.
 */
public class CircleView extends View {

    private int mColor = Color.RED;
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

    private void init() {
        mPaint.setColor(mColor);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        //處理為wrap_content時的情況
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, 200);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, 200);
        }

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //處理padding的情況
        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int paddingTop = getPaddingTop();
        final int paddingBottom = getPaddingBottom();


        int width = getWidth() - paddingLeft - paddingRight;
        int height = getHeight() - paddingTop - paddingBottom;
        int radius = Math.min(width, height) / 2;
        canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, radius, mPaint);
    }

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    public CircleView(Context context) {
        super(context);
        init();

    }

    public CircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
}

然後下面再給出一個範例,稍微複雜一點是自定義viewgroup了(主要是加強對onMeasure和onLayout的理解), 需求如下:

一個水平的viewgroup,內部的子元素 為了簡單 我們假定他們的寬高都是一樣的。來寫一個這樣的簡單的viewgroup。

package com.example.administrator.motioneventtest;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;

/**
 * Created by Administrator on 2016/2/4.
 */
//這裡我們只處理了padding的狀態 沒有處理margin的狀態,子view的margin 對measure和layout的影響
//就留給讀者自己完成了
public class CustomHorizontalLayout extends ViewGroup {

    //設定預設的控制元件最小是多少 這裡不提供自定義屬性了 寫死在程式碼裡 你們可以自行擴充
    final int minHeight = 0;
    final int minWidth = 0;


    public CustomHorizontalLayout(Context context) {
        super(context);
    }

    public CustomHorizontalLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomHorizontalLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measureWidth = 0;
        int measureHeight = 0;
        final int childCount = getChildCount();
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        final View childView = getChildAt(0);
        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int paddingTop = getPaddingTop();
        final int paddingBottom = getPaddingBottom();
        //沒有子控制元件 時 我們的寬高要作特殊處理
        if (childCount == 0) {
            //當沒有子控制元件時,如果長寬有一個為wrap 那麼就讓這個控制元件以最小的形式展現
            //這裡我們最小設定為0
            if (widthSpecMode == MeasureSpec.AT_MOST || heightSpecMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(minWidth, minHeight);
            } else {
                //否則根據我們的layout屬性來
                setMeasuredDimension(getLayoutParams().width, getLayoutParams().height);
            }

        } else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            measureWidth = childView.getMeasuredWidth() * childCount;
            measureHeight = childView.getMeasuredHeight();
            setMeasuredDimension(paddingLeft + measureWidth + paddingRight, paddingTop + measureHeight + paddingBottom);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            measureHeight = childView.getMeasuredHeight();
            setMeasuredDimension(paddingLeft + paddingRight + widthSpecSize, paddingTop + paddingBottom + measureHeight);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            measureWidth = childView.getMeasuredWidth() * childCount;
            setMeasuredDimension(paddingLeft + paddingRight + measureWidth, paddingTop + paddingBottom + heightSpecSize);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int paddingTop = getPaddingTop();
        final int paddingBottom = getPaddingBottom();
        //左邊初始位置為0
        int childLeft = 0 + paddingLeft;
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View childView = getChildAt(i);
            if (childView.getVisibility() != View.GONE) {
                final int childWidth = childView.getMeasuredWidth();
                childView.layout(childLeft, 0 + paddingTop, childLeft + childWidth, paddingTop + childView.getMeasuredHeight());
                childLeft += childWidth;
            }
        }
    }
}

相關文章