View的绘制流程

Quibbler 2021-3-27 1262

View的绘制流程


        要成为一个优秀的Android开发者就必须要了解View的绘制流程。在DecorView添加到Window中的流程一文我们了解了DecorView添加到Window的整个过程,同时在最后留下了伏笔:View添加到Window的同时,也开启了View的绘制流程。现在继续跟着源码熟悉View的绘制流程。

        


1、接上回--View添加到Window

        上回说到View添加到Window的流程,最后通过ViewRootImplsetView()方法将DecorView设置给ViewRootImpl管理。该方法两百余行,其它的我们选择性忽略,只关注和View绘制主线流程相关的:

    /**
     * We have one child
     */
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,int userId) {
        synchronized (this) {
            if (mView == null) {
                mView = view;
                ...
                // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.
                requestLayout();
                ...
            }
        }
    }


1.1、scheduleTraversals()

        每当新set一个View,就会调用requestLayout()方法重新布局:将View的测量、布局、绘制重新执行一遍,以更新界面。另见《View的requestLayout()流程》一文。

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

        内部调用scheduleTraversals(),借助Choreographer执行View遍历绘制流程,关于“编舞者”这里暂不展开讨论,以后有时间再安排上。

    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }


1.2、TraversalRunnable任务

        执行的mTraversalRunnable对象是一个TraversalRunnable类型,实现Runnable接口。不用想,传给Choreographer绕来绕去,最终肯定是被Handler执行。

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

         TraversalRunnable中的run()执行ViewRootImpldoTraversal()方法。

    void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
            ...
            performTraversals();
            ...
        }
    }

 

1.3、performTraversals()

        performTraversals()是最终执行的方法,在这里完成了View绘制的三大流程:measure、layout、draw。该方法一共七百余行,只需关注和View绘制的核心代码段,简化版performTraversals()如下:

    private void performTraversals() {
        ...
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
        ...
        // 执行测量
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        ...
        //执行布局
        performLayout(lp, mWidth, mHeight);
        ...
        //执行绘制
        performDraw();
        ...
    }

        框架中的类和方法很长,一千多行的方法都很正常,为了尽可能的提高一些性能。所以阅读源码一定要抓重点,切莫硬钻进一条逻辑走不出来。

        进入View绘制流程的三个关键方法:performMeasure()performLayout()performDraw()




2、measure

        先开始View的测量,看看测量相关的核心代码。View的测量从performMeasure()方法开始。


2.1、performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec)

        宽高两个参数从ViewRootImpl通过getRootMeasureSpec(int windowSize, int rootDimension)方法初始化好传过来。MeasureSpec用来将sizemode包装成一个int,主要是为了节省内存,关于MeasureSpec详见充分理解MeasureSpec

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

        mView就是添加到ViewRootImpl中的DecorViewDecorView继承自FrameLayoutDecorView并没有重写measure(int widthMeasureSpec, int heightMeasureSpec),还得看父类View中的measure(int widthMeasureSpec, int heightMeasureSpec)方法:

    /**
     * This is called to find out how big a view should be. The parent
     * supplies constraint information in the width and height parameters.
     *
     * @param widthMeasureSpec Horizontal space requirements as imposed by the
     *        parent
     * @param heightMeasureSpec Vertical space requirements as imposed by the
     *        parent
     *
     * @see #onMeasure(int, int)
     */
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        // measure ourselves, this should set the measured dimension flag back
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        ...
    }

        

2.2、View中的默认onMeasure()

        从上面可以看出在View的测量过程中,会调用onMeasure()回调。

    /**
     * Measure the view and its content to determine the measured width and the
     * measured height. This method is invoked by {@link #measure(int, int)} and
     * should be overridden by subclasses to provide accurate and efficient
     * measurement of their contents.
     * 
     * @see #getMeasuredWidth()
     * @see #getMeasuredHeight()
     * @see #setMeasuredDimension(int, int)
     * @see #getSuggestedMinimumHeight()
     * @see #getSuggestedMinimumWidth()
     * @see android.view.View.MeasureSpec#getMode(int)
     * @see android.view.View.MeasureSpec#getSize(int)
     */
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

        这个方法开发者在自定义View的时候可以重写,如果没有重写onMeasure()方法,则会默认调用getDefaultSize()来获得View的宽高,

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

        最后调用setMeasuredDimension(int measuredWidth, int measuredHeight)方法设置当前View的测量宽高值。

    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;
            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }


2.3、重写onMeasure():以LinearLayout和TextView为例

        ViewGroup并没有重写onMeasure(),不过提供了遍历子View的测量方法:measureChildren(int widthMeasureSpec, int heightMeasureSpec),该方法已经不再被普遍使用,了解一下就行,各个容器都有自己的测量子View的方法。

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

        以最常见的容器FrameLayoutonMeasure()方法为例,重写的方法挨个遍历测量每个子View,统计得出宽高。

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
        ...
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            ...
        }
        ...
        count = mMatchParentChildren.size();
        if (count > 1) {
            for (int i = 0; i < count; i++) {
                final View child = mMatchParentChildren.get(i);
                ...
                final int childWidthMeasureSpec;
                if (lp.width == LayoutParams.MATCH_PARENT) {...} else {
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                            lp.leftMargin + lp.rightMargin,
                            lp.width);
                }
                final int childHeightMeasureSpec;
                if (lp.height == LayoutParams.MATCH_PARENT) {...} else {
                    childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                            getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                            lp.topMargin + lp.bottomMargin,
                            lp.height);
                }
                ...
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

        再以TextView重写的onMeasure()方法为例,根据文字、RLT、字重等属性经过复杂的计算,最终测量得出TextView的宽高。

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int width;
        int height;
        ...
        if (widthMode == MeasureSpec.EXACTLY) {
            // Parent has told us how big to be. So be it.
            width = widthSize;
        }
        ...
        if (heightMode == MeasureSpec.EXACTLY) {
            // Parent has told us how big to be. So be it.
            height = heightSize;
            mDesiredHeightAtMeasure = -1;
        } 
        ...
        setMeasuredDimension(width, height);
    }


2.4、获取测量值

        测量后,mMeasuredWidthmMeasuredHeight这两个测量值会初始化,内部的setMeasuredDimensionRaw(int measuredWidth, int measuredHeight)方法设置:

    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;
        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

        可以通过下面两个get方法获取对应的宽高测量值mMeasuredWidthmMeasuredHeight

    public final int getMeasuredWidth() {
        return mMeasuredWidth & MEASURED_SIZE_MASK;
    }
    
    public final int getMeasuredHeight() {
        return mMeasuredHeight & MEASURED_SIZE_MASK;
    }


        有一个坑点,开发者应该遇到过:在Activity中如果想获取View的宽高,不管是在onCreate()还是onStart()onResume()生命周期获取到的值可能都是0。因为View的测量和Activity的生命周期不是同步的,大概率Activity起来了,View还没有测量完毕,值未被初始化,因此获取的是0。

        有没有方法在Activity中获取View的测量宽高吗?有以下三种方法:

        Activity焦点变化的onWindowFocusChanged(hasFocus: Boolean)回调中获取View宽高。

    override fun onWindowFocusChanged(hasFocus: Boolean) {
        super.onWindowFocusChanged(hasFocus)
        Log.d(TAG, "onWindowFocusChanged measuredHeight = " + imageViewEx.measuredHeight)
    }

        通过Viewpost()方法可以将一个Runnable任务放到消息队列的尾部。当UI线程完成View测量后,会执行最后的action。关于消息队列MessageHandler,详见Handler消息延迟原理

    imageViewEx = findViewById(R.id.image_exactly)
    imageViewEx.post {
        Log.d(TAG, "post measuredHeight = " + imageViewEx.measuredHeight)
    }

        借助ViewTreeObserver观察者,监听View变化。当View绘制完毕,回调中获取测量结果。ViewTreeObserver对绘制性能有一定影响,不建议频繁使用。

    override fun onWindowFocusChanged(hasFocus: Boolean) {
        super.onWindowFocusChanged(hasFocus)
        Log.d(TAG, "onWindowFocusChanged measuredHeight = " + imageViewEx.measuredHeight)
    }

        


3、layout

         measure阶段完成,进入View绘制流程第二步:layout,为了确定各View的位置。


3.1、performLayout()

        接着看1.3节 performTraversals()中和layout相关的方法performLayout()

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


3.2、layout(int l, int t, int r, int b)

        局部变量host被赋值为mViewmView我们知道是DecorView类型。调用的是DecorViewlayout()方法,该方法定义在View中,内部调用了setFrame()onLayout()方法。

    /**
     * 为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);
        ...
            onLayout(changed, l, t, r, b);
        ...
    }

        setFrame()方法传入的四个参数分别是左、上、右、下距离父View的距离,于是当前View的位置被布局好了。在两个月前写的Android坐标系一文中,有了解过这些坐标参数的含义。提前储备足够的知识,关键时候还是能串起来用的。

    /**
     * Assign a size and position to this view.
     * This is called from layout.
     *
     * @param left Left position, relative to parent
     * @param top Top position, relative to parent
     * @param right Right position, relative to parent
     * @param bottom Bottom position, relative to parent
     * @return true if the new size and position are different than the
     *         previous ones
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
    protected boolean setFrame(int left, int top, int right, int bottom) {
        ...
        // Remember our drawn bit
        int drawn = mPrivateFlags & PFLAG_DRAWN;
        int oldWidth = mRight - mLeft;
        int oldHeight = mBottom - mTop;
        int newWidth = right - left;
        int newHeight = bottom - top;
        boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
        // Invalidate our old position
        invalidate(sizeChanged);
        ...
        return changed;
    }

        layout()方法还在ViewGroup中被重写:

    @Override
    public final void layout(int l, int t, int r, int b) {
        if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
            if (mTransition != null) {
                mTransition.layoutChange(this);
            }
            super.layout(l, t, r, b);
        } else {
            // record the fact that we noop'd it; request layout when transition finishes
            mLayoutCalledWhileSuppressed = true;
        }
    }


3.3、onLayout(boolean changed, int left, int top, int right, int bottom)

        View中的onLayout()方法默认为空方法:

    /**
     * Called from layout when this view should
     * assign a size and position to each of its children.
     *
     * Derived classes with children should override
     * this method and call layout on each of
     * their children.
     * @param changed This is a new size or position for this view
     * @param left Left position, relative to parent
     * @param top Top position, relative to parent
     * @param right Right position, relative to parent
     * @param bottom Bottom position, relative to parent
     */
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

        对于ViewGroup的子类必须重写onLayout()方法,因为该方法在ViewGroup中被修改成抽象的,以便自定义的ViewGroup能够处理所有子View的布局。

    @Override
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);


3.4、举个例子

        找个ViewGroup看看典型的onLayout()方法如何实现,以最常见的线性容器LinearLayout为例。其中的onLayout()方法根据设置的orientation方向属性选择调用layoutVertical()或者layoutHorizontal()进行垂直布局水平布局。详细代码就不在这里展开解读了。

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

        


4、draw

        View的位置也已经放置好,到了View绘制流程最后一步。涉及到开发者自定义View中接触最多的一个方法:onDraw(Canvas )


4.1、performDraw()

        接着1.3节 performTraversals()中与draw相关的方法:performDraw()

    private void performDraw() {
        if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) {
            return;
        } else if (mView == null) {
            return;
        }
        final boolean fullRedrawNeeded = mFullRedrawNeeded || mReportNextDraw;
        ...
            boolean canUseAsync = draw(fullRedrawNeeded);
        ...
    }

        花了挺时间才找到View绘制的下一步(因为框架的代码量实在是太大,看起来有点费劲):调用ViewRootImpl中的draw(boolean )方法:

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

        真正绘制View的原来是ViewRootImpl中的drawSoftware(),里面调用DecorViewdraw()方法完成从ViewGroup到View的挨个遍历绘制。

    /**
     * @return true if drawing was successful, false if an error occurred
     */
    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
        // Draw with software renderer.
        final Canvas canvas;
        ...
        canvas = mSurface.lockCanvas(dirty);
        ...
        mView.draw(canvas);
        ...
        return true;
    }


4.2、View的绘制:draw(Canvas canvas)

        DecorViewdraw(Canvas )方法定义如下,比起View中的draw(Canvas )增加了绘制菜单背景一项。

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
        if (mMenuBackground != null) {
            mMenuBackground.draw(canvas);
        }
    }

        父类View中的draw(Canvas )方法将绘制一个View拆分为7个小步骤。其中第步和第步正常情况下都会跳过。AOSP中的源码给这段加了详细的注释,每一步骤都有说明。写代码就应该这样,注释也非常重要。

        drawBackground(Canvas canvas):绘制View的背景

        保存画布的图层,准备褪色

        onDraw(canvas):绘制View内容

        dispatchDraw(canvas):分发绘制子View

        绘制褪色边缘并恢复图层

        onDrawForeground(canvas):绘制装饰(例如滚动条)

        drawDefaultFocusHighlight(canvas):绘制默认的焦点突出显示

    public void draw(Canvas canvas) {
        /*
         * 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)
         *      7. If necessary, draw the default focus highlight
         */
        // Step 1, draw the background, if needed
        drawBackground(canvas);
        // Step 2, save the canvas' layers
        // Step 3, draw the content
        onDraw(canvas);
        // Step 4, draw the children
        dispatchDraw(canvas);
        // Step 5, draw the fade effect and restore layers
        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);
        // Step 7, draw the default focus highlight
        drawDefaultFocusHighlight(canvas);
        // we're done...
    }

        

4.3、自定义View关键:onDraw(Canvas canvas)

        自定义View其中一种方式是继承自View,通过重写onDraw(Canvas )实现个性化的界面绘制。该方法一般被子View重写,而不是ViewGroup容器子类。

    /**
     * Implement this to do your drawing.
     *
     * @param canvas the canvas on which the background will be drawn
     */
    protected void onDraw(Canvas canvas) {
    }

        举个例子,以ImageView重写的onDraw(Canvas )为例,绘制我们设置的Drawable资源。

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mDrawable == null) {
            return; // couldn't resolve the URI
        }
        if (mDrawableWidth == 0 || mDrawableHeight == 0) {
            return;     // nothing to draw (empty bounds)
        }
        ...
        mDrawable.draw(canvas);
        ...
    }

        

4.4、绘制子View:dispatchDraw(Canvas canvas)

        ViewGroup并没有实现draw()onDraw(),那么子类递归的绘制在哪里分发的?刚刚在4.3节才看到Viewdraw()步:dispatchDraw(Canvas canvas)分发绘制子View。

    /**
     * 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) {
    }

        dispatchDraw(Canvas canvas)分发才是各容器分发绘制子View的核心,看看ViewGroup中重写的方法实现:

    @Override
    protected void dispatchDraw(Canvas canvas) {
        ...
        for (int i = 0; i < childrenCount; i++) {
            drawChild(canvas, child, drawingTime);
        }
        ...
    }

        遍历ViewGroup中的所有子View,调用drawChild()方法绘制子View:如果是单个View则直接绘制;如果子View还是ViewGroup容器类型,那么继续分发下去,直到所有的子View都绘制完。

    /**
     * Draw one child of this View Group. This method is responsible for getting
     * the canvas in the right state. This includes clipping, translating so
     * that the child's scrolled origin is at 0, 0, and applying any animation
     * transformations.
     *
     * @param canvas The canvas on which to draw the child
     * @param child Who to draw
     * @param drawingTime The time at which draw is occurring
     * @return True if an invalidate() was issued
     */
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }



        了解过View的绘制流程,开发者应该对View的绘制有更深刻的印象,对开发自定义View有一定帮助,加深View的绘制理解。关于自定义View详见以下相关的笔记及View专栏

        自定义View属性declare-styleable

        画布Canvas进阶操作

        路径Path的使用

        视图动画:Animation

        动画插值器Interpolator

        逐帧动画:Frame Animation

        补间动画:Tween Animation

        属性动画集合:AnimatorSet

        属性动画关键帧:keyframe

        大有可为的着色器:Shader

        Drawable中的形状:Shape

        ...


不忘初心的阿甘
最新回复 (0)
    • 安卓笔记本
      2
        登录 注册 QQ
返回
仅供学习交流,切勿用于商业用途。如有错误欢迎指出:fluent0418@gmail.com