View绘制笔记

前言

大家都知道View是经过三大步骤(测量,摆放,绘制)后才显示出来的。但是这些操作是什么时候开始?大概经过怎样的处理呢?

为了弄清楚这些问题,必须查看源码,所以今天通过源码9.0来进一步了解这个过程。

这些步骤是什么时候开始?

从我们打开一个界面开始,ActivityThread中handleResumeActivity方法就会被调用,具体细节,以后有时间再进行讨论,此时我们先认为这个为绘制的入口,接下来进一步源码分析。

#ActivityThread  --->handleResumeActivity

ViewManager wm = a.getWindowManager();
wm.addView(decor, l);
将decorView添加到Window

WindowManager实现类WindowManagerImpl实现了addView方法

#WindowManager ---> addView

mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);

接着交给了WindowManagerGlobal处理

#WindowManagerGlobal---> addView

private final ArrayList<View> mViews = new ArrayList<View>();
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
private final ArrayList<WindowManager.LayoutParams> mParams  = newArrayList<WindowManager.LayoutParams>();

root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);

root.setView(view, wparams, panelParentView);
 创建ViewRootImpl,并且将ViewRootImpl与DecorView建立关联

#ViewRootImpl --->setView()

setView方法会调用requestLayout(),scheduleTraversals();

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

        void scheduleTraversals() {
1429        if (!mTraversalScheduled) {
1430            mTraversalScheduled = true;
1431            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
1432            mChoreographer.postCallback(
1433                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
1434            if (!mUnbufferedInputDispatch) {
1435                scheduleConsumeBatchedInput();
1436            }
1437            notifyRendererOfFramePending();
1438            pokeDrawLockIfNeeded();
1439        }
1440    }

重点在mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);该方法中,mChoreographer相当于维护着Looper,Handler(这里不深入),经过处理后mTraversalRunnable中的run方法将会被调用,接着就会调用到我们熟悉的performTraversals()方法了。

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

            performTraversals();

            。。。
    }

紧接着就会进行调用三大步骤了,performMeasure(),performLayout (),performDraw()

...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, mWidth, mHeight);
...
performDraw()
...

performMeasure()

接着单独分析 performMeasure(),主要调用了view 的mView.measure(),mView为setView()的时候传进来的DecorView,所以这里调用的是DecorView里面的measure。

    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec)                     {   
         mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);        
}


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

DecorView继承了FrameLayout ,所以就到了ViewGroup的测量过程了。

这里先补充一下关于MeasureSpec知识

MeasureSpec可以理解为测量的一种规格说明,“测量规格”。系统会根据View的layoutParams和父容器的限制转化为对应的MeasureSpec。MeasureSpec是一个32位的数值,高2位代表的是SpecMode,低30位代表的是SpecSize,可以通过 MeasureSpec.getSize(),MeasureSpec.getMode()获取对应的值。下面简单说一下三种模式。

UNSPECIFIED

父容器不对子View做限制。

EXACTLY

精确的,意思就是子view的值是确定的,就是指定的SpecSize。对应LayoutParams中的match_parent和具体的数值(例如在xml中敲下的10dp)。

AT_MOST

顾名思义,就是最大是多少,最大不能超过SpecSize。对应LayoutParams中的wrap_parent

接下来看一下DecorView MeasureSpec的创建过程

         /**
2930     * Figures out the measure spec for the root view in a window based on it's
2931     * layout params.
2932     *
2933     * @param windowSize
2934     *            The available width or height of the window
2935     *
2936     * @param rootDimension
2937     *            The layout params for one dimension (width or height) of the
2938     *            window.
2939     *
2940     * @return The measure spec to use to measure the root view.
2941     */

2942    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
2943        int measureSpec;
2944        switch (rootDimension) {
2945
2946        case ViewGroup.LayoutParams.MATCH_PARENT:
2947            // Window can't resize. Force root view to be windowSize.
2948            measureSpec = MeasureSpec.makeMeasureSpec(windowSize,              MeasureSpec.EXACTLY);
2949            break;
2950        case ViewGroup.LayoutParams.WRAP_CONTENT:
2951            // Window can resize. Set max size for root view.
2952            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
2953            break;
2954        default:
2955            // Window wants to be an exact size. Force root view to be that size.
2956            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
2957            break;
2958        }
2959        return measureSpec;
2960    }

该方法主要是根据不同类型的rootDimension组合成不同的MeasureSpec。主要有三种,即MATCH_PARENT填充父控件,SpecSize是窗口的大小,模式SpecMode就是精确的模式了(EXACTLY)。对于WRAP_CONTENT,SpecSize就是窗口的大小了,但是模式的话就是AT_MOST,意思是最大不能超过窗口大小。最后一种就是具体数值了,模式就是精确的,SpecSize为rootDimension。

普通View MeasureSpec的创建过程

6771    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
6772        int specMode = MeasureSpec.getMode(spec);
6773        int specSize = MeasureSpec.getSize(spec);
6774
6775        int size = Math.max(0, specSize - padding);
6776
6777        int resultSize = 0;
6778        int resultMode = 0;
6779
6780        switch (specMode) {
6781        // Parent has imposed an exact size on us
6782        case MeasureSpec.EXACTLY:
6783            if (childDimension >= 0) {
6784                resultSize = childDimension;
6785                resultMode = MeasureSpec.EXACTLY;
6786            } else if (childDimension == LayoutParams.MATCH_PARENT) {
6787                // Child wants to be our size. So be it.
6788                resultSize = size;
6789                resultMode = MeasureSpec.EXACTLY;
6790            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
6791                // Child wants to determine its own size. It can't be
6792                // bigger than us.
6793                resultSize = size;
6794                resultMode = MeasureSpec.AT_MOST;
6795            }
6796            break;
6797
6798        // Parent has imposed a maximum size on us
6799        case MeasureSpec.AT_MOST:
6800            if (childDimension >= 0) {
6801                // Child wants a specific size... so be it
6802                resultSize = childDimension;
6803                resultMode = MeasureSpec.EXACTLY;
6804            } else if (childDimension == LayoutParams.MATCH_PARENT) {
6805                // Child wants to be our size, but our size is not fixed.
6806                // Constrain child to not be bigger than us.
6807                resultSize = size;
6808                resultMode = MeasureSpec.AT_MOST;
6809            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
6810                // Child wants to determine its own size. It can't be
6811                // bigger than us.
6812                resultSize = size;
6813                resultMode = MeasureSpec.AT_MOST;
6814            }
6815            break;
6816
6817        // Parent asked to see how big we want to be
6818        case MeasureSpec.UNSPECIFIED:
6819            if (childDimension >= 0) {
6820                // Child wants a specific size... let him have it
6821                resultSize = childDimension;
6822                resultMode = MeasureSpec.EXACTLY;
6823            } else if (childDimension == LayoutParams.MATCH_PARENT) {
6824                // Child wants to be our size... find out how big it should
6825                // be
6826                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
6827                resultMode = MeasureSpec.UNSPECIFIED;
6828            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
6829                // Child wants to determine its own size.... find out how
6830                // big it should be
6831                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
6832                resultMode = MeasureSpec.UNSPECIFIED;
6833            }
6834            break;
6835        }
6836        //noinspection ResourceType
6837        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
6838    }

此方法传进来的参数: spec是父容器的MeasureSpec,pad。ing为父容器已使用的空间大小,childDimension为子View的lp.width.

子view可用的大小就是父容器的大小减去padding,即size(6775行)。

该方法主要就是根据父容器的MeasureSpec再结合子View的大小childDimension进行创建子View的MeasureSpec。childDimension >= 0意思是子View自己设置了具体的值。

了解完MeasureSpec的创建,我们继续了解measure的过程。

ViewGroup的measure过程

LinearLayout有自己的measure方式,FrameLayout也有自己的measure过程.。所以如果你想实现自己的ViewGroup,想要有自己的测量规则,那么你就应该实现onMeasure方法。下面我将已LinearLayout进行简单分析。

大概就是先遍历所有的子View,逐个测量孩子的大小,(调用孩子的measure()方法),如果孩子是ViewGroup,则又进入了onMeasure进行循环测量, 如果孩子是View,则进行View的测量过程,这个过程下面再单独进行分析。 测量完成后调用setMeasuredDimension()方法。

LinearLayout measure 过程

LinearLayout 测量分为水平和垂直,跟我们设置的方向有关系。下面以measureVertical为例简单阅读一下源码。

//遍历孩子,测量孩子
for (int i = 0; i < count; ++i) {
    //测量子View
    measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);

    //累加高度,加上孩子的上下Margin
     final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + childHeight +         lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));

    //测量子view完成后,根据子View测量自己的大小
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);
    
}

measureChildBeforeLayout调用了measureChildWithMargins。measureChildWithMargins里面主要就是获取view的MeasureSpec,上面有提到过。然后又回到view的measure方法中。

    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的measure过程。

View的measure过程

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

主要就是去设置View的宽高了,够直接的。但是需要了解一下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.AT_MOST,MeasureSpec.EXACTLY,返回的大小都是specSize。需要注意,如果你的控件是直接继承View,那么你应该重写onMeasure方法设置wrap_content的时候的大小,不设置的话就相当于match_parent。

performLayout

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,            int desiredWindowHeight) {
       ....
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
       ....

    
}
调用了View中的layout方法,此方法作用是确定自身和子View的位置。setFrame方法中确定了自己的位置,mLeft 可以通过getLeft()获取,是不是似曾相识呢。然后调用onLayout方法确定子View的位置。
protected boolean setFrame(int left, int top, int right, int bottom) {
    ...
    mLeft = left;
    mTop = top;
    mRight = right;
    mBottom = bottom;
    ...
}
onLayout跟onMeasure方法一样,不同的layout有不同的layout方式。这里继续以LinearLayout进行分析。还是选择垂直方向来体验这个过程。
void layoutVertical(int left, int top, int right, int bottom) {
    ....
    for (int i = 0; i < count; i++) {
            ....
            final View child = getVirtualChildAt(i);
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();
            final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();
            int gravity = lp.gravity;
            if (gravity < 0) {
                    gravity = minorGravity;
                }
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                + lp.leftMargin - lp.rightMargin;
                        break;

                    case Gravity.RIGHT:
                        childLeft = childRight - childWidth - lp.rightMargin;
                        break;

                    case Gravity.LEFT:
                    default:
                        childLeft = paddingLeft + lp.leftMargin;
                        break;
                }

            ...
             childTop += lp.topMargin;
             setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
             childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
             i += getChildrenSkipCount(child, i);

    }
    


}

遍历获得所有的子View,根据不同的Gravity,计算出对应的childLeft,childTop。然后调用了setChildFrame(child, childLeft, childTop + getLocationOffset(child),childWidth, childHeight),setChildFrame方法调用的是child.layout(left, top, left + width, top + height),所以回到前面说的setFrame方法确定了子View的位置了。

performDraw()

这里简单过一下流程,performDraw()会调用draw(fullRedrawNeeded),接着drawSoftware()最终调用 mView.draw(canvas),所以接下来看一下View的Draw方法查看View的Draw方法,很明了,注释写了几个步骤, 1. Draw the background 2. If necessary, save the canvas' layers to prepare for fading 3. Draw view's content 4. Draw children
5. If necessary, draw the fading edges and restore layers 6. Draw decorations (scrollbars for instance)。主要的是绘制背景,绘制自己,绘制孩子,还有绘制装饰。

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

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

    // Step 4, draw the children
            dispatchDraw(canvas);

    // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

    // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);
    ...

}
dispatchDraw方法是去遍历孩子,然后调用孩子的draw方法,这样就完成了绘制的传递了。                                                                                                               

补充:如果继承ViewGroup如果需要重写onDraw来绘制内容时,你需要设置setWillNotDraw(false)关闭WILL_NOT_DRAW标志位。默认情况下,View默认不开启这个标志位,但是ViewGroup默认开启。

    /**
     * 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从测量到显示的大概流程就讲完了,以此记录学习。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值