【读书笔记】【Android 开发艺术探索】第4章 View 的工作原理

来源:互联网 发布:未来影响过去知乎 编辑:程序博客网 时间:2024/04/28 08:55

转载请注明出处 http://blog.csdn.net/yxhuang2008/article/details/51126616

一、基础知识

1、ViewRoot 和 DecorView

ViewRoot 对应 ViewRootImpl 类,它是连接 WindowManager 和 DecorView 的纽带,View 的三大流程都是通过 ViewRoot 来完成的。在ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRoot 对象。

DecorView 添加到窗口 Window 的过程。


图片来自 https://yq.aliyun.com/articles/3005


View 的绘制流程从 ViewRootImpl 的 preformTraversals 开始,下面是它的伪码

 // ViewRootImple#performTraverals 的伪代码    private void preformTraverals(){        preformMeasure(...)     --------- View.measure(...);                performLayout(...)        ---------  View.layout(...);                performDraw(...)           --------   View.draw(...);    }


2、 MeasureSpec

MeasureSpec 是一个32位的值,高2位是 SpecMode, 测量模式,低30位是 SpecSize,是在某种测量模式下的规格大小。
(1).SpecMode 的分类:
UNSPECIFIED: 表示开发人员可以将试图按照自己的意愿设置任意大小,没有任何限制。这种情况少见,不太会用到。比如 listView 一般自定义 View 用不到。

EXACTLY: 表示父视图希望子视图的大小应该由 SpeceSize 的值决定的。子元素将被限定在给定的边界里而忽略它本身的大小。对应的属性为 match_parent 或者指定大小。

WRAP_PARENT : 表示子视图最多只能是 SpecSize 中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不超过 SpeceSize.对应的属性是 wrap_content.

(2).MeasureSpece 和 LayoutParams 的对应关系
DecorView 的 MeasureSpec 由窗口尺寸和自身 LayoutParams 决定;
普通 View 的 MeasureSpec 由父容器的 MeasureSpce 和自身的 LayoutParams 决定。
由 ViewGroup 的 getChildMeasureSpece(...) 方法,可以得出下图中的结论。


二、View 的工作流程

1、measure 过程

(1). measure 的核心方法
      .mensure(int widthMeasureSpec, int heightMeasureSpec)
 该方法在 View 中定义为 final ,不能重写该方法。但是 measure 最终会调用 onMeasure (...)方法, 因此在自定义的View 的时候,只要重写onMeasure(...) 方法即可。

.onMeasure(int widthMeasureSpec, int heightMeasureSpec)

该方法就是我们只自定义 View 的时候需要实现测量绘制逻辑的方法,该方法的参数是父视图对子视图的 widht 和height 测量方法的要求。在自定义 View 时,需要做的就是更加 widthMeasureSpec 和 heightMeasureSpec 计算View 的width 和 height ,不同的处理模式不同。

.setMeasuredDimension(int measuredWidth, int measureHeith)

测量阶段的终极方法,在 onMeasure(int widthMeasureSpec, int heightMeasureSpece) 方法中调用,将计算的得到尺寸传递给该方法,测量阶段结束。该方法必须调用,否则会报异常。在自定义 View 的时候,不需要关系系统复杂的 Measure 过程,只需调用setMeasuredDimension(int measuredWith, int measuredHeith) 设置根据 MeasureSpec计算得到的尺寸即可。


(2)measure 过程

Measure 过程传递尺寸的两个参数

ViewGroup.LayoutParams View 自身的布局参数;

MeasureSpec 类, 父视图对子视图的测量要求。


View 的 measure 过程


View 的 getDefaultSize 方法

    // 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;    }
从 getDefaultSize 方法可以看出, View 的宽高是由 specSize 决定的。

直接继承 View 的自定义控件需要重写 onMeasure(...) 方法并设置 wrap_content 时的自身大小,否则在布局中使用 wrap_content 相当于使用 match_parent. 从 MeasureSpece 和 LayoutParams 关系表格中可看出。

解决方法,给 View 指定一个默认的内部宽高(mWith 和 mHeight),并在 wrap_content 时设置此宽高即可。

 protected  void onMeasure(int widthMeasureSpec, int heightMeasureSpec){        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);        int heightSpeceMode = MeasureSpec.getMode(heightMeasureSpec);        int widthSpeceSize = View.MeasureSpec.getSize(widthMeasureSpec);        int heightSepceSize = View.MeasureSpec.getSize(heightMeasureSpec);        if (widthMeasureSpec == MeasureSpec.AT_MOST                && heightMeasureSpec == MeasureSpec.AT_MOST){            // 设置一个默认的宽高             setMeasuredDimension(mWidth, mHeight);        } else if (widthSpecMode == View.MeasureSpec.AT_MOST){            setMeasuredDimension(mWidth, heightSepceSize);        } else if (heightSpeceMode == View.MeasureSpec.AT_MOST){            setMeasuredDimension(widthSpeceSize, mHeight);        }    }


ViewGroup  的 measure 过程.

ViewGroup 除了完成自己的 measure 过程以为,还会遍历去调用所有子元素的 measure 方法。 ViewGroup 是一个抽象类,没有重写 View 的 onMeasure 方法。ViewGroup 也没有定义其测量的具体过程,其测量过程的 onMeasure 方法需要各个之类去实现。


measure 完成以后,可以通过 getMeasureWidth / getMeasureHeight 获取 View 的测量宽高, 要在 onLayout 方法中去获取 View 的测量宽高或者最终宽高。

因为 View 的 measure 过程和 Activity 的生命周期方法不是同步的,因此无法保证 Activity 在 onCreate, onStart, onResume 方法中获取 View 的宽高信息。

解决办法:

1. 在 Activity/View # onWindowFoucsChanged 方法中

   @Override    public void onWindowFocusChanged(boolean hasFocus) {        super.onWindowFocusChanged(hasFocus);        if (hasFocus){            int width = view.getMeasuredWidth();            int height = view.getMeasuredHeight();        }    }

2. 使用 view.post(Runnable)

   @Override    protected void onStart() {        super.onStart();        mTextView.post(new Runnable() {            @Override            public void run() {                int width = view.getMeasuredWidth();                int height = view.getMeasuredHeight();            }        });    }

3.使用 ViewTreeObserver

@Override    protected void onStart() {        super.onStart();        ViewTreeObserver observer = view.getViewTreeObserver();        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {            @SuppressWarnings("deprecation")            @Override            public void onGlobalLayout() {                view.getViewTreeObserver().removeGlobalOnLayoutListener(this);                int widht = view.getMeasuredWidth();                int height = view.getMeasuredHeight();            }        });    }

4. 使用 View.measure(int widthMeasureSpec, int heightMeasureSpec)

通过手动对 View 进行 measure 得到 View 的宽高。

 View 的 LayoutParams 分:

match_parent: 

无法测出;


具体数值(dp/px):

     例如宽高都是 100px 时

int widthMesureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);        int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);        view.measure(widthMesureSpec, heightMeasureSpec);


wrap_parent 时

        int widthMesureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) -1, View.MeasureSpec.AT_MOST);        int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) -1, View.MeasureSpec.AT_MOST);        view.measure(widthMesureSpec, heightMeasureSpec);

2. layout 过程

子视图的具体位置是相对于父视图而言的。View 的 onLayout 方法时空方法,ViewGrop 的 onLayout 方法时 abstract .

如果自定义的 View 继承 ViewGroup ,需要实现 onLayout 方法。

 // View#layout    public void layout(int l, int t, int r, int b) {        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;        }        int oldL = mLeft;        int oldT = mTop;        int oldB = mBottom;        int oldR = mRight;        // setOpticalFrame / setFrame 设定 View 的四个顶点        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);            ...        }        ...    }


getMeasureWidth 和 getWidth 之间的区别:

getMeasureWidth 是 measure() 过程之后获取后,getWidth 是在 layout() 过程之后得到的。getMeasureWidth() 方法中的值是通过 setMeasureDimension() 方法类进行设置的,而 getWidth() 方法中的值是通过视图右边的坐标减去左边的坐标计算出来的。


3.draw 过程

.View.draw(Canvas canvas)  
ViewGroup 没有重写该方法,所以所有的视图最终都会调用 View 的 draw(...) 方法进行绘制。在自定义视图时,不应该重写该方法,而是应该重写 onDraw(Canvas cavas) 方法,进行绘制。如果自定义视图确实要重写该方法,先调用super.draw(canvas) 完成系统的绘制,然后再进行自定义的绘制。 
.View.onDraw(..) 
View 的 onDraw(...) 方法默认是空方法,自定义视图时,需要重写该方法,绘制自身的内容。
.View.dispatchDraw(...)
View 中默认是空方法,ViewGroup 重写了该方法对子元素进行了绘制。在自定义 ViewGroup 是不应该对该方法进行重写。
   // View#draw    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)            // 第六步, 绘制 foreground, scrollbars            onDrawForeground(canvas);            // we're done...            return;        }    }
不需要绘制 layer 的时候会跳过第二步和第五步。因此在绘制的时候,能不绘制 layer 就尽量不绘制 layer, 以提高绘制效率。

setWillNotDraw

   /**     * 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 不需要绘制任何内容,则将这个标志位设置为 true, 系统会进行相应的优化。当我们在自定义控件继承 ViewGroup 并且本身不具备绘制功能时,可以开启这个标志位从而便于系统进行后续的优化。


invalidate()

请求重绘 View 树,即 draw 过程。假如视图没有发生大小变化就不会调用 layout() 过程,并且只调用那些调用了 invalidate()方法的 View.


requestLayout()

当布局发生变化时,会调用该方法。在自定义视图中,如果在某些情况下希望重新测量尺寸大小,应该手动去调用该方法,它会触发 measure() 和 layout() 过程,但不会进行 draw。


三、自定义 View

1、分类

(1). 继承 View 重写 onDraw() 方法
用来实现一些不规则的视图,需要自己支持 wrap_content, 并且也需要自己处理 padding.

  (2).继承 ViewGroup 派生特殊的 Layout
用于自定义布局,需要合适地处理 ViewGroup 的测量、布局的过程,并同时处理处理子元素的测量和布局过程。

  (3). 继承特定的 View 
用户扩展某种已有的 View 的特性。

  (4).继承特定的 ViewGroup (例如 LinearLayout)

 2、注意事项

(1).让 View 支持 wrap_content
(2).如有必要,让 View 支持 padding, 在 draw 方法中支持处理 padding.
(3).尽量不要在 View 中使用 Handler, 可使用其内部的 post 方法.
(4).View 中有线程或者动画时
在 onAttachToWindow 中启动线程和动画;
在 onDetachFromWindow 方法中要停止线程和动画.
(5). View 带有滑动嵌套情况时,需要处理好滑动冲突。



本文除了是《Android 开发艺术探索》 书中的知识,还有部分内容摘自这两个博客
https://github.com/android-cn/android-open-project-analysis/tree/master/tech/viewdrawflow

View 绘制流程













1 0