美文网首页
View测量、布局及绘制原理

View测量、布局及绘制原理

作者: 有没有口罩给我一个 | 来源:发表于2019-11-14 23:15 被阅读0次
    ActivityThread.handleResumeActivity

    1 、View绘制的三大过程

    //View绘制的三大过程开始位置
    @Override
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
                                     String reason) {
    // 将DecorView添加到Window上,紧接着进入绘制三大过程,实际上是调用WindowManagerImpl的addView方法,然后调用WindowManagerGlobal的addView方法。
                    // 出发绘制的单打过程的条件是:当DecorView被添加到Window中时。
                    wm.addView(decor, l);
    
    }
    
    WindowManagerImpl.addView
     // 参数view是顶层视图(DecorView)
    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }
    
    WindowManagerGlobal
     // 参数view是顶层视图(DecorView)
     public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
    
    ViewRootImpl root;
    View panelParentView = null;
    
    root = new ViewRootImpl(view.getContext(), display);
    // view就是顶层视图(DecorView)
    view.setLayoutParams(wparams);
    // 将View和和ViewRooot已将View的 params收集到容器中管理
    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);
    
    try {
    
     root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
            }
    }
    
    小结

    我省略了一些不重要的代码,触发View的绘制过程的条件是ActivityTherad.handleResumeActivity方法开始将DecorView添加到Window上时,紧接着在WindowManagerGlobal的addView方法中创建ViewRootImpl 对象并调用ViewRootImpl的setView方法,将顶层视图(DecorView)做参数传入,进入绘制三大过程,实际上是调用WindowManagerImpl的addView方法,然后调用WindowManagerGlobal的addView方法。触发View绘制三大过程的条件是:当DecorView被添加到Window中时,上面最后一步提到调用ViewRootimpl的setView并传入DecorView,那么我们看一下ViewRootImpl.setView方法做了什么?

    ViewRootImpl
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
                requestLayout();
    }
    

    我们看到setView方法里面调用了requestLayout方法,代码如下:

     @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }
    

    在requestLayout方法中又调用 checkThread(),看看就知道检查当前线程,看一下代码:

     void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }
    

    你现在应该知道为什么不能再工作线程中更新UI了吗?当然你要是Activity的onResume之前是可以在工作线程更新UI的,我们看到在requestLayout方法中还调用checkThread结束还调用scheduleTraversals方法,点击去看看代码:

    void scheduleTraversals() {
    
         mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    
    }
    

    在scheduleTraversals方法中其他的不要看,你就mTraversalRunnable代码。

    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    

    在TraversalRunnable 中又调用doTraversal方法。

    void doTraversal() {
    
      performTraversals();
    
    }
    

    可以看到在doTraversal方法中调用performTraversals方法。

    private void performTraversals() {
    
    final View host = mView;
    
    // 因为顶层View,这里需要根据窗口的宽高以及View自身的LayoutParams计算MeasureSpec
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
                    
    // performMeasure  测量
    // Ask host how big it wants to be
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    int width = host.getMeasuredWidth();
    int height = host.getMeasuredHeight();
    //再次测量的标志
    boolean measureAgain = false;
    //有权重,就会被测量两次,为了性能,想线性布局中使用Weight
    if (lp.horizontalWeight > 0.0f) {
        width += (int) ((mWidth - width) * lp.horizontalWeight);
        childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
        measureAgain = true;
     }
    if (lp.verticalWeight > 0.0f) {
       height += (int) ((mHeight - height) * lp.verticalWeight);
       childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY);
       measureAgain = true;
    }
    if (measureAgain) {
        //有权重,就会被测量两次,为了性能,想线性布局中使用Weight
       // performMeasure
       performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    
    // performLayout
     performLayout(lp, mWidth, mHeight);
    
    
    // performDraw
     performDraw();
    
    }
    

    可以看到在performTraversals方法中又分别调用:performMeasure(测量)、performLayout(布局)和performDraw(绘制),执行了View的绘制三大过程,那么接下来我们分别介绍这些过程。

    2、 测量(performMeasure)

    首先看看performTraversals方法在执行performMeasure之前做的测量准备,代码如下;

    // 因为顶层View,这里需要根据窗口的宽高以及View自身的LayoutParams计算MeasureSpec
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
                    
    // performMeasure  测量
    // Ask host how big it wants to be
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    int width = host.getMeasuredWidth();
    int height = host.getMeasuredHeight();
    //再次测量的标志
    boolean measureAgain = false;
    //有权重,就会被测量两次,为了性能,想线性布局中使用Weight
    if (lp.horizontalWeight > 0.0f) {
        width += (int) ((mWidth - width) * lp.horizontalWeight);
        childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
        measureAgain = true;
     }
    if (lp.verticalWeight > 0.0f) {
       height += (int) ((mHeight - height) * lp.verticalWeight);
       childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY);
       measureAgain = true;
    }
    if (measureAgain) {
        //有权重,就会被测量两次,为了性能,想线性布局中使用Weight
       // performMeasure
       performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    

    首先我知道测量时需要MeasureSpec,所以在performTraversals方法中执行performMeasure方法之前,即在测量之前需要准备MeasureSpec,具体代码:

    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    

    我们看看那getRootMeasureSpec方法:

    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;
    }
    

    可以看到在创建DecorView的MeasureSpec时,需要根据自己的LayoutParams和Parent的测量模式(model)最终决定DecorView的MeasureSpec。

    2.1 、MeasureSpec的介绍

    模式(Mode) + 尺寸(Size)->MeasureSpec  32位int值
    00000000 00000000 00000000 00000000
    SpecMode(前2位)   +  SpecSize(后30)
    mode + size --> MeasureSpec
    
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK = 0x3 << MODE_SHIFT;
    
    父容器不对View做任何限制,系统内部使用
    public static final int UNSPECIFIED = 0 << MODE_SHIFT; 00000000 00000000 00000000 00000000
    
    父容器检测出View的大小,Vew的大小就是SpecSize LayoutPamras match_parent 固定大小
    public static final int EXACTLY     = 1 << MODE_SHIFT; 01000000 00000000 00000000 00000000
    
    父容器指定一个可用大小,View的大小不能超过这个值,LayoutPamras wrap_content
    public static final int AT_MOST     = 2 << MODE_SHIFT; 10000000 00000000 00000000 00000000
    

    由上述可知MeasureSpe封装了测量model和size,而前2位表示SpecMode,后30位表示(SpecSize),MeasureSpe中还定义了三种MeasureSpe,分别是:

    • UNSPECIFIED 父容器不对View做任何限制,系统内部使用,当然你在ScrollView中也是可以看到的。
    • EXACTLY 父容器检测出View的大小,Vew的大小就是SpecSize LayoutPamras.match_parent 固定大小
    • AT_MOST 父容器指定一个可用大小,View的大小不能超过这个值,LayoutPamras.wrap_conten

    在MeasureSpe中定义了从MeasureSpe获取model和size的方法,我们就不看了。

    2.2、performMeasure方法
    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        try {
            //调用DecorView的measure方法进行调度测量,那么DecorView是继承了FrameLayout
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }
    

    可以看到performMeasure方法中直接调用了View.measure,而这个measure方法是View的测量调度方法:

    2.2.1 默认的情况
    //View的measure方法
    
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    
        if (forceLayout || needsLayout) {
            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
    
            resolveRtlPropertiesIfNeeded();
    
            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            /**
             * 没有缓存直接调用onMeasure进行测量操作
             *  onMeasure  -> setMeasuredDimension -> setMeasuredDimensionRaw(保存测量的结果)
             */
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                // 测量自己,这应该将mPrivateFlags设置为PFLAG_MEASURED_DIMENSION_SET标志回传
                // 不复写使用默认大小,因为目前是针对的DecorView,所以我们要看DecorView的OnMeasure的实现,即Framelayout
                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
            /**
             * 在setMeasuredDimensionRaw中设置 将mPrivateFlags为了PFLAG_MEASURED_DIMENSION_SET标志
             */
            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;
        }
    
    }
    
    
    
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //不复写使用默认大小
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
    
    
    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }
    
    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;
    
        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }
    

    为了简洁我把一些优化的算法给删掉,在measure方法中又调用onMeasure测量自己,然后调用setMeasuredDimension和setMeasuredDimensionRaw 方法保存测量结果,并将mPrivateFlags设置为PFLAG_MEASURED_DIMENSION_SET标志回传,如果你不复写View.onMeasure方法,就使用默认大小,如果你自定义View,不复写onMeasure方法,最后你会发现:wrap_content 和 match_parent的效果是一样的,可以进去看看onMeasure源码,因为目前是针对的DecorView,所以我们要看DecorView的OnMeasure的实现,即Framelayout,当你需要自定义Viewroup时你需要根据child的布局属性来测量自己的宽和高,而当你自定义View时,你仅仅只需要测量自己就可以了,最后总结:

    ViewGroup : measure --> onMeasure(测量子控件的宽高measureChild)--> setMeasuredDimension -->setMeasuredDimensionRaw(保存自己的宽高)
    View measure --> onMeasure(测量自己) --> setMeasuredDimension -->setMeasuredDimensionRaw(保存自己的宽高)
    

    最后我们来看看FrameLayout的onMeasure是怎么实现的:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
    
        final boolean measureMatchParentChildren = MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();
    
        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;
    
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            // GONE 是不进行测量的
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                /**
                 * 调用measureChildWithMargins测量Child
                 */
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
    
    
                /**
                 * 根据FrameLayout的布局特点,为了计算自己的大小,它只需要计算最大的宽度和高度
                 */
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
    
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
        //设置测量结果,进行回传
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
    }
    
     // 测量child  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);
    }
    

    可以看到在FrameLayout.onMeasure方法中是通过循环遍历所有的child,来决定自己的最终宽度和高,那么DecorView是继承FrameLayout的,所以这也就是DecorView的测量规范,由measureChildWithMargins方法得知Child的ModeSpec是由parentWidthMeasureSpec 和 Child的LayoutParam决定,然后在顶用child.measure方法将计算出来的ModeSpec传给Child,最终会回调到child的onMeasure方法中。

    3、布局 performLayout

    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,  int desiredWindowHeight) {
    
          //View和ViewGroup 也是会调用的onLayout,layout方法是摆放自己,而onLayout是根据自己的要求摆放child的
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    }
    

    顶层视图(DecorView)host调用layout方法摆放自己

    public void layout(int l, int t, int r, int b) {
    
       // onLayout
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
           }
    }
    

    在layouyt方法中有会调用onLayout,对child进行摆放,不管是View还是ViewGroup都会回调onLayout方法,但是onLayout方法是对child进行摆放的,在View中是空实现,你自定义实现了也没有实际的意义,所以layout的过程是比较简单的,接下来我们看看performDraw方法,
    ViewGroup.layout(来确定自己的位置,4个点的位置) -->onLayout(进行子View的布局)
    View.layout(来确定自己的位置,4个点的位置)。

    4、绘制performDraw

     private void performDraw() {
           boolean canUseAsync = draw(fullRedrawNeeded);
      }
    

    performDraw方法很简单,直接调用了draw方法:

     private boolean draw(boolean fullRedrawNeeded) {
               if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                        scalingRequired, dirty, surfaceInsets)) {
                    return false;
                }
     }
    

    draw方法代码很多,我就挑重点讲,在draw方法中调用了drawSoftware方法:

    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
        mView.draw(canvas);
    }
    

    drawSoftware方法代码非常多,主要就是初始化Canvas相关的操作,只挑重点讲,在drawSoftware方法中经过Canvas的初始化,直接调用了View的mView.draw(canvas);并将canvas作为参数传进去:

    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
    
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }
          //ViewGroup不会回调onDraw这个方法,如果你设置背景onDraw是会被回调的,具体请看ViewGroup的构造方法initViewGroup
            if (!dirtyOpaque) onDraw(canvas);
    
    
         //在ViewGroup实现了这个方法,在View中是空实,意思就是:如果是ViewGroup那么它会分发绘制的给child,而绘制有child自己完成。
            dispatchDraw(canvas);
       
    
           // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);
     }
    

    在draw方法中,你可能会看到privateFlags 这个标志,实际上这个表示是用来表示是否需要绘制,即回调onDraw方法,在ViewGroup的构造函数中将此标志设置为 WILL_NOT_DRAW,所以为什么ViewGroup默认不会回调onDraw方法的原因,当然如果你设置背景onDraw是会被回调的。

    最后关于自定义View相关的:

    • ViewGroup
      1、绘制背景 drawBackground(canvas);

      2、绘制自己onDraw(canvas),实际上ViewGroup 并不需要去关心这个方法;

      3、绘制子View dispatchDraw(canvas) ViewGroup 会在dispatchDraw方法中循环遍历所有的child并调用ViewGroup .drawChild方法紧接着调用child.draw方法让child自己绘制,dispatchDraw方法是在ViewGroup中才会有的,所以View并不存在此方法;

      4、绘制前景,滚动条等装饰onDrawForeground(canvas)

    • View
      1、绘制背景 drawBackground(canvas)

    2、绘制自己onDraw(canvas)

    3、绘制前景,滚动条等装饰onDrawForeground(canvas)

    相关文章

      网友评论

          本文标题:View测量、布局及绘制原理

          本文链接:https://www.haomeiwen.com/subject/dbfobqtx.html