美文网首页Android专题
Android基础(17)View绘制

Android基础(17)View绘制

作者: perry_Fan | 来源:发表于2019-02-23 21:22 被阅读174次

    1)View绘制流程
    2)计算一个view的嵌套层级
    3)View刷新机制。invalidate和 postInvalidate、requestLayout的区别及使用

    4)自定义控件原理,如何优化自定义View
    (1. 降低刷新频率,减少不必要的invalidate 2. 不要在OnDraw当中创建绘制对象 3.硬件加速)
    5)自定义View的事件 (onTouchEvent / 新建接口在onTouch中触发)
    6)自定义View如何提供获取View属性的接口?(TypedArray)
    7)为什么不能在子线程更新UI?(若可以的话,那么在多线程中并发访问可能会导致UI控件处于不可预期的状态。)

    一. View绘制流程

    每一个View的绘制过程都必须经历三个最主要的过程,也就是measure、layout和draw。
    整个View树的绘图流程是在ViewRootImpl类的performTraversals()方法开始的,该函数做的执行过程主要是根据之前设置的状态,判断是否重新计算视图大小(measure)、是否重新放置视图的位置(layout)、以及是否重绘 (draw),其核心也就是通过判断来选择顺序执行这三个方法中的哪个,如下:

    private void performTraversals() {
            ......
            //最外层的根视图的widthMeasureSpec和heightMeasureSpec由来
            //lp.width和lp.height在创建ViewGroup实例时等于MATCH_PARENT
            int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
            int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
            ......
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            ......
            mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
            ......
            mView.draw(canvas);
            ......
    }
    

    1. Measure

        /**
         * <p>
         * This is called to find out how big a view should be. The parent
         * supplies constraint information in the width and height parameters.
         * </p>
         *
         * <p>
         * The actual measurement work of a view is performed in
         * {@link #onMeasure(int, int)}, called by this method. Therefore, only
         * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
         * </p>
         *
         *
         * @param widthMeasureSpec Horizontal space requirements as imposed by the
         *        parent
         * @param heightMeasureSpec Vertical space requirements as imposed by the
         *        parent
         *
         * @see #onMeasure(int, int)
         */
         //final方法,子类不可重写
        public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
            ......
            //回调onMeasure()方法
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            ......
        }
    

    为整个View树计算实际的大小,然后设置实际的高和宽,每个View控件的实际宽高都是由父视图和自身决定的。实际的测量是在onMeasure方法进行,所以在View的子类需要重写onMeasure方法,这是因为measrue方法是final的,不允许重载,所以子view只能通过重载onMeasure来实现自己的测量逻辑。

    这个方法的两个参数都是从父view传递过来的,也就是代表了父view的规格。它由两部分组成,高2位表示MODE,定义在MeasureSpec类中(View的内部类),三种类型MeasureSpec.EXACTLY表示确定大小,MeasureSpec.AT_MOST表示最大大小,MeasureSpec.UNSPECIFIED表示不确定。低30位表示size,也就是View的大小。对于系统Window类的DecorView对象Mode一般都为MeasureSpec.EXACTLY,而size分别对应屏幕宽高。对于子view来说大小由父view和子view共同决定。
    看出measure方法最终回调了View的onMeasure方法,我们来看下View的onMeasure源码,如下:

      /**
         * <p>
         * 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 overriden by subclasses to provide accurate and efficient
         * measurement of their contents.
         * </p>
         *
         * <p>
         * <strong>CONTRACT:</strong> When overriding this method, you
         * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
         * measured width and height of this view. Failure to do so will trigger an
         * <code>IllegalStateException</code>, thrown by
         * {@link #measure(int, int)}. Calling the superclass'
         * {@link #onMeasure(int, int)} is a valid use.
         * </p>
         *
         * <p>
         * The base class implementation of measure defaults to the background size,
         * unless a larger size is allowed by the MeasureSpec. Subclasses should
         * override {@link #onMeasure(int, int)} to provide better measurements of
         * their content.
         * </p>
         *
         * <p>
         * If this method is overridden, it is the subclass's responsibility to make
         * sure the measured height and width are at least the view's minimum height
         * and width ({@link #getSuggestedMinimumHeight()} and
         * {@link #getSuggestedMinimumWidth()}).
         * </p>
         *
         * @param widthMeasureSpec horizontal space requirements as imposed by the parent.
         *                         The requirements are encoded with
         *                         {@link android.view.View.MeasureSpec}.
         * @param heightMeasureSpec vertical space requirements as imposed by the parent.
         *                         The requirements are encoded with
         *                         {@link android.view.View.MeasureSpec}.
         *
         * @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)
         */
         //View的onMeasure默认实现方法
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
        }
    

    setMeasuredDimension传入的参数都是通过getDefaultSize返回的,所以再来看下getDefaultSize方法源码,如下:

      public static int getDefaultSize(int size, int measureSpec) {
            int result = size;
            //通过MeasureSpec解析获取mode与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;
        }
    

    如果specMode等于AT_MOST或EXACTLY就返回specSize,这就是系统默认的规格。
    回过头继续看上面onMeasure方法,其中getDefaultSize参数的widthMeasureSpec和heightMeasureSpec都是由父View传递进来的。getSuggestedMinimumWidth与getSuggestedMinimumHeight都是View的方法,具体如下:

    protected int getSuggestedMinimumWidth() {
            return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
        }
    
        protected int getSuggestedMinimumHeight() {
            return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
        }
    

    建议的最小宽度和高度都是由View的Background尺寸与通过设置View的miniXXX属性共同决定的。
    到此一次最基础的元素View的measure过程就完成了。上面说了View实际是嵌套的,而且measure是递归传递的,所以每个View都需要measure。实际能够嵌套的View一般都是ViewGroup的子类,所以在ViewGroup中定义了measureChildren, measureChild, measureChildWithMargins方法来对子视图进行测量,measureChildren内部实质只是循环调用measureChild,measureChild和measureChildWithMargins的区别就是是否把margin和padding也作为子视图的大小。如下我们以ViewGroup中稍微复杂的measureChildWithMargins方法来分析:

     protected void measureChildWithMargins(View child,
                int parentWidthMeasureSpec, int widthUsed,
                int parentHeightMeasureSpec, int heightUsed) {
            //获取子视图的LayoutParams
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            //调整MeasureSpec
            //通过这两个参数以及子视图本身的LayoutParams来共同决定子视图的测量规格
            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);
            //调运子View的measure方法,子View的measure中会回调子View的onMeasure方法
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    

    该方法就是对父视图提供的 measureSpec 参数结合自身的 LayoutParams 参数进行了调整,然后再来调用child.measure()方法,具体通过方法 getChildMeasureSpec 来进行参数调整。所以我们继续看下 getChildMeasureSpec 方法代码,如下:

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
            // 获取当前Parent View的Mode和Size
            int specMode = MeasureSpec.getMode(spec);
            int specSize = MeasureSpec.getSize(spec);
            // 获取Parent size与padding差值(也就是Parent剩余大小),若差值小于0直接返回0
            int size = Math.max(0, specSize - padding);
            // 定义返回值存储变量
            int resultSize = 0;
            int resultMode = 0;
            // 依据当前Parent的Mode进行switch分支逻辑
            switch (specMode) {
            // Parent has imposed an exact size on us
            // 默认Root View的Mode就是EXACTLY
              case MeasureSpec.EXACTLY:
                if (childDimension >= 0) {
                    // 如果child的layout_wOrh属性在xml或者java中给予具体大于等于0的数值
                    // 设置child的size为真实layout_wOrh属性值,mode为EXACTLY
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // 如果child的layout_wOrh属性在xml或者java中给予MATCH_PARENT
                    // Child wants to be our size. So be it.
                    // 设置child的size为size,mode为EXACTLY
                    resultSize = size;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // 如果child的layout_wOrh属性在xml或者java中给予WRAP_CONTENT
                    // 设置child的size为size,mode为AT_MOST
                    // Child wants to determine its own size. It can't be
                    // bigger than us.
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;
            ......
            // 其他Mode分支类似
            }
            // 将mode与size通过MeasureSpec方法整合为32位整数返回
            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
    

    getChildMeasureSpec的逻辑是通过其父View提供的MeasureSpec参数得到specMode和specSize,然后根据计算出来的specMode以及子View的childDimension(layout_width或layout_height)来计算自身的measureSpec,如果其本身包含子视图,则计算出来的measureSpec将作为调用其子视图measure函数的参数,同时也作为自身调用setMeasuredDimension的参数,如果其不包含子视图则默认情况下最终会调用onMeasure的默认实现,并最终调用到setMeasuredDimension。

    所以可以看见onMeasure的参数其实就是这么计算出来的。同时从上面的分析可以看出来,最终决定View的measure大小是View的setMeasuredDimension方法,所以我们可以通过setMeasuredDimension设定死值来设置View的mMeasuredWidth和mMeasuredHeight的大小,但是一个好的自定义View应该会根据子视图的measureSpec来设置mMeasuredWidth和mMeasuredHeight的大小,这样的灵活性更大,所以这也就是上面分析onMeasure时说View的onMeasure最好不要重写死值的原因。

    2. layout

      public void layout(int l, int t, int r, int b) {
            ......
            //实质都是调用setFrame方法把参数分别赋值给mLeft、mTop、mRight和mBottom这几个变量
            //判断View的位置是否发生过变化,以确定有没有必要对当前的View进行重新layout
            boolean changed = isLayoutModeOptical(mParent) ?
                    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
            //需要重新layout
            if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
                //回调onLayout
                onLayout(changed, l, t, r, b);
                ......
            }
            ......
     }
    
    • View.layout方法可被重载,ViewGroup.layout为final的不可重载,ViewGroup.onLayout为abstract的,子类必须重载实现自己的位置逻辑。
    • measure操作完成后得到的是对每个View经测量过的 measuredWidth 和 measuredHeight,layout操作完成之后得到的是对每个View进行位置分配后的 mLeft、mTop、mRight、mBottom,这些值都是相对于父View来说的。
    • 凡是layout_XXX的布局属性基本都针对的是包含子 View 的 ViewGroup 的,当对一个没有父容器的View设置相关layout_XXX属性是没有任何意义的。
    • 使用View的 getWidth() 和 getHeight() 方法来获取View测量的宽高,必须保证这两个方法在onLayout流程之后被调用才能返回有效值。

    3.draw

    • 如果该View是一个ViewGroup,则需要递归绘制其所包含的所有子View。
    • View默认不会绘制任何内容,真正的绘制都需要自己在子类中实现。
    • View的绘制是借助onDraw方法传入的Canvas类来进行的。
    • 区分View动画和ViewGroup布局动画,前者指的是View自身的动画,可以通过setAnimation添加,后者是专门针对ViewGroup显示内部子视图时设置的动画,可以在xml布局文件中对ViewGroup设置layoutAnimation属性(譬如对LinearLayout设置子View在显示时出现逐行、随机显示等不同动画效果)。
    • 在获取画布剪切区(每个View的draw中传入的Canvas)时会自动处理掉padding,子View获取Canvas不用关注这些逻辑,只用关心如何绘制即可。
    • 默认情况下子View的ViewGroup.drawChild绘制顺序和子View被添加的顺序一致,但是你也可以重载ViewGroup.getChildDrawingOrder()方法提供不同顺序。
    二. 计算一个view的嵌套层级
       int i = 0;
        private void getParents(ViewParent view){
            if (view.getParent() == null) {
                Log.v("tag", "最终==="+i);
                return;
            }
            i++;
            ViewParent parent = view.getParent();
            Log.v("tag", "i===="+i);
            Log.v("tag", "parent===="+parent.toString());
            getParents(parent);
        }
    

    因为 public abstract class ViewGroup extends View implements ViewParent

    三. View的invalidate和postInvalidate方法源码分析

    invalidate追源码最后发现均回调用到 invalidateInternal 方法。

    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
                boolean fullInvalidate) {
            ......
                // Propagate the damage rectangle to the parent view.
                final AttachInfo ai = mAttachInfo;
                final ViewParent p = mParent;
                if (p != null && ai != null && l < r && t < b) {
                    final Rect damage = ai.mTmpInvalRect;
                    //设置刷新区域
                    damage.set(l, t, r, b);
                    //传递调运Parent ViewGroup的invalidateChild方法
                    p.invalidateChild(this, damage);
                }
                ......
    }
    

    View的invalidate(invalidateInternal)方法实质是将要刷新区域直接传递给了父ViewGroup的invalidateChild方法,在invalidate中,调用父View的invalidateChild,这是一个从当前向上级父View回溯的过程,每一层的父View都将自己的显示区域与传入的刷新Rect做交集 。所以我们看下ViewGroup的invalidateChild方法,源码如下:

    public final void invalidateChild(View child, final Rect dirty) {
            ViewParent parent = this;
            final AttachInfo attachInfo = mAttachInfo;
            ......
            do {
                ......
                //循环层层上级调运,直到ViewRootImpl会返回null
                parent = parent.invalidateChildInParent(location, dirty);
                ......
            } while (parent != null);
    }
    

    这个过程最后传递到ViewRootImpl的invalidateChildInParent方法结束,所以我们看下ViewRootImpl的invalidateChildInParent方法,如下:

     @Override
        public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
            ......
            //View调运invalidate最终层层上传到ViewRootImpl后最终触发了该方法
            scheduleTraversals();
            ......
            return null;
      }
    

    2. postInvalidate 方法源码分析

     public void postInvalidate() {
            postInvalidateDelayed(0);
     }
    
     public void postInvalidateDelayed(long delayMilliseconds) {
            // We try only with the AttachInfo because there's no point in invalidating
            // if we are not attached to our window
            final AttachInfo attachInfo = mAttachInfo;
            //核心,实质就是调运了 ViewRootImpl.dispatchInvalidateDelayed方法
            if (attachInfo != null) {
                attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
            }
    }
    

    继续看他调运的ViewRootImpl类的dispatchInvalidateDelayed方法,如下源码:

        public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
            Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
            mHandler.sendMessageDelayed(msg, delayMilliseconds);
        }
    

    通过ViewRootImpl类的Handler发送了一条MSG_INVALIDATE消息,继续追踪这条消息的处理可以发现:

    public void handleMessage(Message msg) {
        ......
        switch (msg.what) {
        case MSG_INVALIDATE:
            ((View) msg.obj).invalidate();
            break;
        ......
        }
        ......
    }
    

    实质就是又在UI Thread中调运了View的invalidate();方法,那接下来View的invalidate();

    3. requestLayout方法源码分析

    public void requestLayout() {
            ......
            if (mParent != null && !mParent.isLayoutRequested()) {
                //由此向ViewParent请求布局
                //从这个View开始向上一直requestLayout,最终到达ViewRootImpl的requestLayout
                mParent.requestLayout();
            }
            ......
    }
    

    当我们触发View的requestLayout时其实质就是层层向上传递,直到ViewRootImpl为止,然后触发ViewRootImpl的requestLayout方法,如下就是ViewRootImpl的requestLayout方法:

     @Override
        public void requestLayout() {
            if (!mHandlingLayoutInLayoutRequest) {
                checkThread();
                mLayoutRequested = true;
                //View调运requestLayout最终层层上传到ViewRootImpl后最终触发了该方法
                scheduleTraversals();
            }
        }
    

    类似于上面分析的invalidate过程,只是设置的标记不同,导致对于View的绘制流程中触发的方法不同而已。
    requestLayout()方法会调用measure过程和layout过程,不会调用draw过程,也不会重新绘制任何View包括该调用者本身。

    相关文章

      网友评论

        本文标题:Android基础(17)View绘制

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