美文网首页Fucking source codeAndroid自定义View
View绘制流程及源码解析(三)——Layout与Draw流程

View绘制流程及源码解析(三)——Layout与Draw流程

作者: Geeks_Liu | 来源:发表于2017-03-27 15:03 被阅读110次
    提纲(三).png

    在上一篇View绘制流程及源码解析(二)——onMeasure()流程分析这篇文章中,我们详细的分析了很多测量过程的细节(好吧,真是有点过于细节了,感觉寒假的时候这时间花的,简直心疼),这篇我们来分析三大流程最后两个流程,鉴于上一篇文章中事无巨细的分析细节造成的惨痛教训,这篇文章会避免对于细节的死扣,而更加注重整体流程的把握。

    一.Layout流程

    1.Layout的基本过程

    Layout的过程就是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定之后,它在onLayout中会遍历所有的子元素并调用其Layout方法,在Layout方法中onLayout方法又会被调用。这段话摘自《Android开发艺术探索》,既然Layout的过程是ViewGroup用来确定子元素的位置的,那么ViewGroup的位置又是怎么确定的呢?实际上,类似于Measure()过程,整个布局的过程也是一个递归调用的过程——首先从最顶层的DecorView开始,我们说过,DecorView是一个FrameLayout,也就是一个ViewGroup,调用他的Layout方法,那么他会遍历循环他的子元素,并调用子元素的onLayout方法,如果子元素依然是个ViewGroup,那么调用这个子ViewGroup(如LinearLayout)重写过的onLayout方法;然后在该方法中又会接着往下遍历,调用子View的layout方法,一直扒到最后一层View,假设这个子View是TextView,那么就会调用TextView的onLayout的方法,具体的控件,他们的onLayout的方法都是重写过的,每个有每个自己的布局规矩。

    screen_simple.xml中的关系.png

    这里我们再贴一下前两篇中都提到的图,大家可以对着这个图心理想一遍这个递归调用的过程。

    2.DecorView的Layout过程

    首先我们要回到三大流程开始的地方,也就是ViewRootImpl类中的performTraversals()方法中:
    (framewoks/base/core/java/android/view/ViewRootImpl):

    if (didLayout) {
        performLayout(lp, mWidth, mHeight);
    

    View绘制流程及源码解析(一)——performTraversals()源码分析这篇文章的“第二段代码”前面的一段中,我们说了这个mWidth与mHeight,这两个值实际上就表示的是当前窗口(Window)的大小,或者说当前DecorView的宽高,lp就是DecorView的布局参数。
    我们接着看这个方法(framewoks/base/core/java/android/view/ViewRootImpl):

    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        ......
        final View host = mView;
    
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
        try {
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    

    这个方法中,可以看到host调用了layout()方法,这个host是mView赋值过来的,这个mView就是DecorView,我们去到View类中看下这个layout()方法(framewoks/base/core/java/android/view/View):

    /*
     * @param l Left position, relative to parent
     * @param t Top position, relative to parent
     * @param r Right position, relative to parent
     * @param b Bottom position, relative to parent
     */
    public void layout(int l, int t, int r, int b) {
        ......
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
    
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
    
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }
        ......
    }
    

    可以看到,调用了onLayout(changed, l, t, r, b);方法,这个方法的四个参数,看注释可以知道,依次是View/ViewGroup的左、上、右、下四个坐标,而他得调用方法是host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());,这里的host变量就是DecorView,那么传入的这四个参数是什么意思呢?首先我们知道,DecorView是Android屏幕的的顶级View,他上面已经没有父View了,而我们知道,Android当中的原点坐标系是从左上角开始的:

    图解Android Window区域划分.png

    我们贴一张之前贴过的图,注意左上角的那个红点,我们已经标出了他是Android坐标系的原点,以及DecorView的区域示意图(忽略过扫描区域,那玩意是在屏幕外面的)。因此,我们也不难理解,host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());中的参数了,前两个0,0表示的是Android的坐标系原点(DecorView的左上角),后两个表示是DecorView的测量宽高。

    回到上面的源码中,我们可以看到在layout()方法中,调用了onLayout()方法,我们来看看这个方法:

    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }
    

    如你所见,这个方法是空的——上面我们已经提到,onLayout的实现方法,是整个下发到子View/ViewGroup中的,具体的View具体实现,我们对着上面的screen_simple.xml的结构层次图,试着分析一下LinearLayout的onLayout()方法:

    3.LinearLayout的Layout过程

    (framewroks/base/core/java/android/widget/linearlayout):

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }
    
    void layoutVertical(int left, int top, int right, int bottom) {
        final int paddingLeft = mPaddingLeft;
    
        int childTop;
        int childLeft;
    
        // Where right end of child should go
        final int width = right - left;
        int childRight = width - mPaddingRight; //确定子元素的右界限
    
        // Space available for child
        int childSpace = width - paddingLeft - mPaddingRight;   //子元素的生存空间为,LinearLayout的宽度减左右Padding
    
        final int count = getVirtualChildCount();   //获取子元素的个数
    
        final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
        final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
    
        switch (majorGravity) { //根据BOTTOM,CENTER_VERTICAL,TOP三种Gravity来确定childTop,即子元素的顶部位置
           case Gravity.BOTTOM:
               childTop = mPaddingTop + bottom - top - mTotalLength;
               break;
               ......
           default:
               childTop = mPaddingTop;
               break;
        }
    
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {     //可见性不是GONE的元素都参与布局
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();
    
                final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
                ......
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {    //根据三种Gravity确定子元素左位置
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                + lp.leftMargin - lp.rightMargin;
                     ......
                    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);
            }
        }
    }
    

    上面的流程比较清楚,我们加了一些必要的注释,就不死扣实现的细节了,可以看到调用了setChildFrame(child, childLeft, childTop + getLocationOffset(child),childWidth, childHeight);这个方法:

    private void setChildFrame(View child, int left, int top, int width, int height) {
        child.layout(left, top, left + width, top + height);
    }
    

    可以看到,这个方法中又调用了子View的layout()方法,如果此时子View也是个ViewGroup,那么回到上面的流程中接着调用;如果此时子View是一个View,那么调用子View(如TextView)的onLayout方法。

    二.Draw流程

    1.Canvas与Surface

    上面performLayout()方法之之心完之后,就该执行performDraw()方法了,我们直接来看这个方法(framewoks/base/core/java/android/view/ViewRootImpl):

    private void performDraw() {
        ......
        final boolean fullRedrawNeeded = mFullRedrawNeeded;
        mFullRedrawNeeded = false;
    
        mIsDrawing = true;
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
        try {
            draw(fullRedrawNeeded);
        } finally {
            mIsDrawing = false;
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        ......
    }
    

    调用了draw(fullRedrawNeeded);,这个fullRedrawNeeded意思是,需要完全重绘的意思,笔者看了下这个变量,整个ViewRootImpl类中,只有上面一段中的这句:

     final boolean fullRedrawNeeded = mFullRedrawNeeded;
     mFullRedrawNeeded = false;
    

    mFullRedrawNeeded的值变为了false,其他地方均为true,也就是说,你可以先不管这个boolean值代表什么,只要知道他是一个true值就行了。
    然后我们接着看draw(fullRedrawNeeded);方法:

    private void draw(boolean fullRedrawNeeded) {
        Surface surface = mSurface;
        if (!surface.isValid()) {
            return;
        }
        ......
    
            if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
                return;
            }
        ......
    
    1).什么是surface?

    这个surface是什么呢?Surface是原始图像缓冲区(raw buffer)的一个句柄,而原始图像缓冲区是由屏幕图像合成器(screen compositor)管理的。SDK的中对该类的注释为:Handle onto a raw buffer that is being managed by the screen compositor——处理由屏幕合成器管理的原始缓冲区。

    • 句柄,英文:HANDLE,数据对象进入内存之后获取到内存地址,但是所在的内存地址并不是固定的,需要用句柄来存储内容所在的内存地址。从数据类型上来看它只是一个32位(或64位)的无符号整数。
    • Surface 充当句柄的角色,用来获取原始图像缓冲区以及其中的内容
    • 原始图像缓冲区(raw buffer)用来保存当前窗口的像素数据。

    看了这段话,我们大概已经知道了这个Surface是用来干什么的了,他就是一个用来获取原始图像缓冲区中图像数据的句柄~~我们接着看if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty))这个巨长的方法:

    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {
    
        // Draw with software renderer.
        final Canvas canvas;
        try {
            ......
            canvas = mSurface.lockCanvas(dirty);
            ......
        } catch (IllegalArgumentException e) {
            ......
        }
        ......
        try {
            ......
    
            mView.draw(canvas);
            .......
    
    
    2).什么是Canvas ?

    上面这段代码中出现了一个Canvas对象,我们省略了除了关键代码之外的所有代码。首先说一下Canvas是什么?
      先看下这个类的注释:

    /**
     * The Canvas class holds the "draw" calls. To draw something, you need
     * 4 basic components: A Bitmap to hold the pixels, a Canvas to host
     * the draw calls (writing into the bitmap), a drawing primitive (e.g. Rect,
     * Path, text, Bitmap), and a paint (to describe the colors and styles for the
     * drawing).
     * /
    

    为了绘制一个东西,我们需要4个元素来协同完成:

    • 位图:Bitmap 来保持(hold)那些像素
    • 画布:Canvas 来响应画画(draw)的调用(并将其写入 bitmap)
    • 画笔:paint 描述画画的颜色和样式等
    • “颜料“:drawing primitive,比如矩形、路径、文字、位图等其他元素

    关于这四个元素,我们如果了解自定义View的话应该很熟悉,这个Canvas实际上就是一个画布.

    3).Surface与Canvas

    可以看到,每一个Surface中保存了一个Canvas对象,因为Canvas是“画布”嘛,它可以暂时保存我们绘画的数据(当然最终是要存到BitMap中);而Surface是操纵图像数据的句柄,因此他保存一个Canvas,通过这个Canvas我们就可以进行各种绘制的工作。
      不论是普通的自定义View,还是SurfaceView自定义Veiw,我们都需要首先获取Surface中的Canvas,通过canvas = mSurface.lockCanvas(dirty);方法,然后才能进行一系列操作,并在操作完成后unlockCanvasAndPost(canvas)方法释放canvas——只不过在SurfaceView中需要手动调用这两个方法,但是在普通的View或者自定义View中,这两个方法是在ViewRootImpl类中由系统自行调用。

    我们看看这句:canvas = mSurface.lockCanvas(dirty);,这句代码调用了Surface类中的lockCanvas()方法(framewoks/base/core/java/android/view/Surface):

    public Canvas lockCanvas(Rect inOutDirty)
            throws Surface.OutOfResourcesException, IllegalArgumentException {
        synchronized (mLock) {
            checkNotReleasedLocked();
            if (mLockedObject != 0) {
                throw new IllegalArgumentException("Surface was already locked");
            }
            mLockedObject = nativeLockCanvas(mNativeObject, mCanvas, inOutDirty);
            return mCanvas;
        }
    }
    

    可以看到,这个方法中,我们返回了一个Canvas对象,并调用native层的方法nativeLockCanvas()锁定这个画布。

    2.绘制流程

    之后我们就可以看上面那段代码中真正的开始绘制的地方了:mView.draw(canvas);,我们之前说过,mView表示的就是DecorView:

    @CallSuper
        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;
    
            /*
             * Draw traversal performs several drawing steps which must be executed
             * in the appropriate order:
             *
             *      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)
             */
    
            // Step 1, draw the background, if needed
            int saveCount;
    
            if (!dirtyOpaque) {
                drawBackground(canvas);
            }
    
            // skip step 2 & 5 if possible (common case)
            final int viewFlags = mViewFlags;
            boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
            boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
            if (!verticalEdges && !horizontalEdges) {
                // Step 3, draw the content
                if (!dirtyOpaque) onDraw(canvas);
    
                // Step 4, draw the children
                dispatchDraw(canvas);
    
                // Overlay is part of the content and draws beneath Foreground
                if (mOverlay != null && !mOverlay.isEmpty()) {
                    mOverlay.getOverlayView().dispatchDraw(canvas);
                }
    
                // Step 6, draw decorations (foreground, scrollbars)
                onDrawForeground(canvas);
    
                // we're done...
                return;
            }
            ......
        }
    

    可以看到,上面的注释中说的很清楚了,绘制的过程主要分为4个步骤(本来6个步骤,一般情况下跳过2和5):

    ①绘制背景 drawBackground(canvas);
    ②绘制自己onDraw(canvas)
    /**
     * Implement this to do your drawing.
     *
     * @param canvas the canvas on which the background will be drawn
     */
    protected void onDraw(Canvas canvas) {
    }
    

    这个方法是一个空方法,不同的子View会重写这个方法来实现自己的绘制;ViewGroup里边没有实现这个方法,笔者看了一下,貌似只有LinearLayout实现了这个方法,但也仅仅是为了在其中调用分割线(drawable)的draw()方法,

    ③绘制childrendispatchDraw(canvas)
    /**
     * Called by draw to draw the child views. This may be overridden
     * by derived classes to gain control just before its children are drawn
     * (but after its own view has been drawn).
     * @param canvas the canvas on which to draw the view
     */
    protected void dispatchDraw(Canvas canvas) {
    
    }
    

    View的绘制机制通过dispatchDraw()方法来传递的,该方法在View类中是一个空方法,但是在ViewGroup类中确实实现的(framewoks/base/core/java/android/view/ViewGroup):

    @Override
    protected void dispatchDraw(Canvas canvas) {
        boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
        final int childrenCount = mChildrenCount;
        final View[] children = mChildren;
        int flags = mGroupFlags;
    
        if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
            final boolean buildCache = !isHardwareAccelerated();
            for (int i = 0; i < childrenCount; i++) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
                    final LayoutParams params = child.getLayoutParams();
                    attachLayoutAnimationParameters(child, params, i, childrenCount);
                    bindLayoutAnimation(child);
                }
            }
        ......
            // Draw any disappearing views that have animations
            if (mDisappearingChildren != null) {
                final ArrayList<View> disappearingChildren = mDisappearingChildren;
                final int disappearingCount = disappearingChildren.size() - 1;
                // Go backwards -- we may delete as animations finish
                for (int i = disappearingCount; i >= 0; i--) {
                    final View child = disappearingChildren.get(i);
                    more |= drawChild(canvas, child, drawingTime);
                }
            }
            ......
    

    可以看到,在ViewGroup的这个方法中,我们循环遍历了子View,并调用他们的draw方法,如此draw事件就这样一层层传递下去了。

    ④绘制装饰(foreground, scrollbars) onDrawForeground(canvas)

    相关文章

      网友评论

        本文标题: View绘制流程及源码解析(三)——Layout与Draw流程

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