Android View 的工做流程和原理

前言

在平常开发中,咱们天天都在和各类 View 打交道,好比TextView,Button等,咱们直接拿过来就可使用,那么 Android 是怎么把 View 绘制到屏幕上呢,接下来咱们结合源码来具体分析。android

在具体结合源码分析前,先了解一个比较重要的概念 ViewRoot面试

ViewRoot

ViewRoot 对应于 ViewRootImpl 类,它是链接 WindowManager 和 根布局 DecorView(看上图) 的纽带, View 的三大流程均是经过 ViewRoot 来完成的。在 ActivityThread 中,当 Activity 对象被建立完毕后,会将 DecorView 添加到 Window 中,同时会建立 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 创建关联。canvas

View 的绘制流程是从 ViewRoot 的 performTraversals 方法开始的,它通过 measure、layout 和 draw 三个过程才能最终将一个 View 绘制出来,其中 measure 用来测量 View 的宽和高,layout 用来肯定 View 在父容器中的放置位置,而 draw 则负责将 View 绘制在屏幕上。针对 performTraversals的大体流程以下:
小程序

performTraversals 会依次调用 performMeasure、performLayout 和 performDraw 三个方法,这三个方法分别完成顶级 View 的 measure、layout 和 draw 这三大流程,其中在 performMeasure 中会调用 measure 方法,在 measure 方法中又会调用 onMeasure 方法,在 onMeasure 方法中则会对全部的子元素进行 measure 过程,这个时候 measure 流程就从父容器传递到子元素中了,这样就完成了一次 measure 过程。接着子元素会重复父容器的 measure 过程,如此反复就完成了整个 View 树的遍历。同理,performLayout 和 performDraw 的传递流程和 performMeasure 是相似的,惟一不一样的是,performDraw 的传递过程是在 draw 方法中经过 dispatchDraw 来实现的,不过这并无本质区别。性能优化

接下来结合源码来分析这三个过程。架构

Measure 测量过程

这里分两种状况,View 的测量过程和 ViewGroup 的测量过程。app

View 的测量过程

View 的 测量过程由其 measure 方法来完成,源码以下:ide

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
            //省略代码...
            
            if (forceLayout || needsLayout) {
            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

            resolveRtlPropertiesIfNeeded();

            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }

            // flag not set, setMeasuredDimension() was not invoked, we raise
            // an exception to warn the developer
            if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                throw new IllegalStateException("View with id " + getId() + ": "
                        + getClass().getName() + "#onMeasure() did not set the"
                        + " measured dimension by calling"
                        + " setMeasuredDimension()");
            }

            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }

        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;

        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }

 

能够看到 measure 方法是一个 final 类型的方法,这意味着子类不能重写此方法。源码分析

在 13 行 measure 中会调用 onMeasure 方法,这个方法是测量的主要方法,继续看 onMeasure 的实现布局

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

setMeasuredDimension 方法的做用是设置 View 宽和高的测量值,咱们主要看 getDefaultSize 方法
是如何生成测量的尺寸。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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,MeasureSpec 是什么鬼呢,敲黑板了,重点来了。
MeasureSpec 决定了 View 的测量过程。确切来讲,MeasureSpec 在很大程度上决定了一个 View 的尺寸规格。
来看 MeasureSpec 类的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /** @hide */
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}

        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        public static final int EXACTLY     = 1 << MODE_SHIFT;

        public static final int AT_MOST     = 2 << MODE_SHIFT;

        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
        
        public static int makeSafeMeasureSpec(int size, int mode) {
            if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
                return 0;
            }
            return makeMeasureSpec(size, mode);
        }

        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

        static int adjust(int measureSpec, int delta) {
            final int mode = getMode(measureSpec);
            int size = getSize(measureSpec);
            if (mode == UNSPECIFIED) {
                // No need to adjust size for UNSPECIFIED mode.
                return makeMeasureSpec(size, UNSPECIFIED);
            }
            size += delta;
            if (size < 0) {
                Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
                        ") spec: " + toString(measureSpec) + " delta: " + delta);
                size = 0;
            }
            return makeMeasureSpec(size, mode);
        }

        public static String toString(int measureSpec) {
            //省略...
        }
    }

能够看出 MeasureSpec 中有两个主要的值,SpecMode 和 SpecSize, SpecMode 是指测量模式,而 SpecSize 是指在某种测量模式下的规格大小。

SpecMode 有三种模式:

  1. UNSPECIFIED
    不限制:父容器不对 View 有任何限制,要多大给多大,这种状况比较少见,通常不会用到。

  2. EXACTLY
    限制固定值:父容器已经检测出 View 所须要的精确大小,这个时候 View 的最终大小就是 SpecSize 所指定的值。它对应于 LayoutParams 中的 match_parent 和具体的数值这两种模式。

  3. AT_MOST
    限制上限:父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值,具体是什么值要看不一样 View 的具体实现。它对应于 LayoutParams 中的 wrap_content。

MeasureSpec 中三个主要的方法来处理 SpecMode 和 SpecSize

  1. makeMeasureSpec 打包 SpecMode 和 SpecSize
  2. getMode 解析出 SpecMode
  3. getSize 解析出 SpecSize

不知道童鞋们以前有没有注意到 onMeasure 有两个参数 widthMeasureSpec 和 heightMeasureSpec,那这两个值从哪来的呢,这两个值都是由父视图通过计算后传递给子视图的,说明父视图会在必定程度上决定子视图的大小,可是最外层的根视图 也就是 DecorView ,它的 widthMeasureSpec 和 heightMeasureSpec 又是从哪里获得的呢?这就须要去分析 ViewRoot 中的源码了,在 performTraversals 方法中调了 measureHierarchy 方法来建立 MeasureSpec 源码以下:

1
2
3
4
5
6
7
8
9
10
11
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
        int childWidthMeasureSpec;
        int childHeightMeasureSpec;
        
        childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
        childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                
      //省略代码...          
}

里面调用了 getRootMeasureSpec 方法生成 MeasureSpec,继续查看 getRootMeasureSpec 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
       int measureSpec;
       switch (rootDimension) {

       case ViewGroup.LayoutParams.MATCH_PARENT:
           // Window can't resize. Force root view to be windowSize.
           measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
           break;
       case ViewGroup.LayoutParams.WRAP_CONTENT:
           // Window can resize. Set max size for root view.
           measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
           break;
       default:
           // Window wants to be an exact size. Force root view to be that size.
           measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
           break;
       }
       return measureSpec;
   }

经过上述代码,DecorView 的 MeasureSpec 的产生过程就很明确了,具体来讲其遵照以下规则,根据它的 LayoutParams 中的宽和高的参数来划分。

  • LayoutParams.MATCH_PARENT:限制固定值,大小就是窗口的大小 windowSize
  • LayoutParams.WRAP_CONTENT:限制上限,大小不定,可是不能超过窗口的大小 windowSize
  • 固定大小:限制固定值,大小为 LayoutParams 中指定的大小 rootDimension

对于 DecorView 而言, rootDimension 的值为 lp.width 和 lp.height 也就是屏幕的宽和高,因此说 根视图 DecorView 的大小默认老是会充满全屏的。那么咱们使用的 View 也就是 ViewGroup 中 View 的 MeasureSpec 产生过程又是怎么样的呢,在 ViewGroup 的测量过程当中会具体介绍。

先回头看 getDefaultSize 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
   }

如今理解起来是否是很简单呢,若是 specMode 是 AT_MOST 或 EXACTLY 就返回 specSize,这也是系统默认的行为。以后会在 onMeasure 方法中调用 setMeasuredDimension 方法来设定测量出的大小,这样 View 的 measure 过程就结束了,接下来看 ViewGroup 的 measure 过程。

ViewGroup 的测量过程

ViewGroup中定义了一个 measureChildren 方法来去测量子视图的大小,以下所示

1
2
3
4
5
6
7
8
9
10
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);
           }
       }
   }

从上述代码来看,除了完成本身的 measure 过程之外,还会遍历去全部在页面显示的子元素,
而后逐个调用 measureChild 方法来测量相应子视图的大小

measureChild 的实现以下

1
2
3
4
5
6
7
8
9
10
11
protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

measureChild 的思想就是取出子元素的 LayoutParams,而后再经过 getChildMeasureSpec 来建立子元素的 MeasureSpec,接着将 MeasureSpec 直接传递给 View 的 measure 方法来进行测量。

那么 ViewGroup 是如何建立来建立子元素的 MeasureSpec 呢,咱们继续看 getChildMeasureSpec 方法源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

上面的代码理解起来很简单,为了更清晰地理解 getChildMeasureSpec 的逻辑,这里提供一个表,表中对 getChildMeasureSpec 的工做原理进行了梳理,表中的 parentSize 是指父容器中目前可以使用的大小,childSize 是子 View 的 LayoutParams 获取的值,从 measureChild 方法中可看出

1
2
3
4
5
6
7
8
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);

表以下:

普通 View 的 MeasureSpec 的建立规则普通 View 的 MeasureSpec 的建立规则

经过上表能够看出,只要提供父容器的 MeasureSpec 和子元素的 LayoutParams,就能够快速地肯定出子元素的 MeasureSpec 了,有了 MeasureSpec 就能够进一步肯定出子元素测量后的大小了。

至此,View 和 ViewGroup 的测量过程就告一段落了。来个小结。

MeasureSpec 的模式和生成规则
MeasureSpec 中 specMode 有三种模式:

  1. UNSPECIFIED
    不限制:父容器不对 View 有任何限制,要多大给多大,这种状况比较少见,通常不会用到。

  2. EXACTLY
    限制固定值:父容器已经检测出 View 所须要的精确大小,这个时候 View 的最终大小就是 SpecSize 所指定的值。它对应于 LayoutParams 中的 match_parent 和具体的数值这两种模式。

  3. AT_MOST
    限制上限:父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值,具体是什么值要看不一样 View 的具体实现。它对应于 LayoutParams 中的 wrap_content。

生成规则:

  1. 对于普通 View,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定。
  2. 对于不一样 ViewGroup 中的不一样 View 生成规则参照上表。

MeasureSpec 测量过程:
measure 过程主要就是从顶层父 View 向子 View 递归调用 view.measure 方法,measure 中调 onMeasure 方法的过程。

说人话呢就是,视图大小的控制是由父视图、布局文件、以及视图自己共同完成的,父视图会提供给子视图参考的大小,而开发人员能够在 XML 文件中指定视图的大小,而后视图自己会对最终的大小进行拍板。

那么测量事后,怎么获取 View 的测量结果呢
通常状况下 View 测量大小和最终大小是同样的,咱们可使用 getMeasuredWidth 方法和 getMeasuredHeight 方法来获取视图测量出的宽高,可是必须在 setMeasuredDimension 以后调用,不然调用这两个方法获得的值都会是0。为何要说是通常状况下是同样的呢,在下文介绍 Layout 中会具体介绍。

Layout 布局过程

测量结束后,视图的大小就已经测量好了,接下来就是 Layout 布局的过程。上文说过 ViewRoot 的 performTraversals 方法会在 measure 结束后,执行 performLayout 方法,performLayout 方法则会调用 layout 方法开始布局,代码以下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
           int desiredWindowHeight) {
       mLayoutRequested = false;
       mScrollMayChange = true;
       mInLayout = true;

       final View host = mView;
       if (host == null) {
           return;
       }
       if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
           Log.v(mTag, "Laying out " + host + " to (" +
                   host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
       }
try {
   host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
   //...省略代码
   } finally {
           Trace.traceEnd(Trace.TRACE_TAG_VIEW);
   }
   mInLayout = false;

View 类中 layout 方法实现以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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;

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

            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }

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

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

        if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
            mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
            notifyEnterOrExitForAutoFillIfNeeded(true);
        }
    }

layout 方法接收四个参数,分别表明着左、上、右、下的坐标,固然这个坐标是相对于当前视图的父视图而言的,而后会调用 setFrame 方法来设定 View 的四个顶点的位置,即初始化 mLeft、mRight、mTop、mBottom 这四个值,View 的四个顶点一旦肯定,那么 View 在父容器中的位置也就肯定了,接着会调用 onLayout 方法,这个方法的用途是父容器肯定子元素的位置,和 onMeasure 方法相似

onLayout 源码以下:

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

纳尼,怎么是个空方法,没错,就是一个空方法,由于 onLayout 过程是为了肯定视图在布局中所在的位置,而这个操做应该是由布局来完成的,即父视图决定子视图的显示位置,咱们继续看 ViewGroup 中的 onLayout 方法

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

能够看到,ViewGroup 中的 onLayout 方法居然是一个抽象方法,这就意味着全部 ViewGroup 的子类都必须重写这个方法。像 LinearLayout、RelativeLayout 等布局,都是重写了这个方法,而后在内部按照各自的规则对子视图进行布局的。因此呢咱们若是要自定义 ViewGroup 那么就要重写 onLayout 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class TestViewGroup extends ViewGroup {

    public TestViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (getChildCount() > 0) {
            View childView = getChildAt(0);
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (getChildCount() > 0) {
            View childView = getChildAt(0);
            childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
        }
    }
}

xml 中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

    <com.will.testdemo.customview.TestViewGroup
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_blue_bright"
        >

        <TextView
            android:id="@+id/tv_hello"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@color/colorPrimaryDark"
            android:text="@string/hello_world"
            android:textColor="@android:color/white"
            android:textSize="15sp"
            />

    </com.will.testdemo.customview.TestViewGroup>

  </LinearLayout>

显示效果以下:

不知道童鞋们发现了没,我给自定义的 ViewGroup 设置了背景色,看效果貌似占满全屏了,但是我在 xml 中设置的 wrap_content 啊,这是什么状况,咱们回头看看 ViewGroup 中 View 的 MeasureSpec 的建立规则
普通 View 的 MeasureSpec 的建立规则普通 View 的 MeasureSpec 的建立规则

从表中可看出由于 ViewGroup 的父布局设置的 match_parent 也就是限制固定值模式,而 ViewGroup 设置的 wrap_content,那么最后 ViewGroup 使用的是 父布局的大小,也就是窗口大小 parentSize,那么若是咱们给 ViewGroup 设置固定值就会使用 咱们设置的值,来改下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

    <com.will.testdemo.customview.TestViewGroup
        android:layout_width="200dp"
        android:layout_height="100dp"
        android:background="@android:color/holo_blue_bright"
        >

        <TextView
            android:id="@+id/tv_hello"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@color/colorPrimaryDark"
            android:text="@string/hello_world"
            android:textColor="@android:color/white"
            android:textSize="15sp"
            />

    </com.will.testdemo.customview.TestViewGroup>

</LinearLayout>

效果以下:

表中的其余状况,建议童鞋们本身写下代码,会理解的更好。

以前说过,通常状况下 View 测量大小和最终大小是同样的,为何呢,由于最终大小在 onLayout 中肯定,咱们来改下代码:

1
2
3
4
5
6
7
@Override
   protected void onLayout(boolean changed, int l, int t, int r, int b) {
       if (getChildCount() > 0) {
           View childView = getChildAt(0);
           childView.layout(0, 0, childView.getMeasuredWidth()+100, childView.getMeasuredHeight()+200);
       }
   }

显示效果

没错,onLayout 就是这么任性,因此要获取 View 的真实大小最好在 onLayout 以后获取。那么如何来获取 view 的真实大小呢,能够经过下面的代码来获取

1
2
3
4
tv_hello.post(Runnable {
       log(" getMeasuredWidth() = ${tv_hello.measuredWidth}")
       log(" getWidth() = ${tv_hello.width}")
}

打印以下:

1
2
01-18 23:33:20.947 2836-2836/com.will.testdemo I/debugLog:  getMeasuredWidth() = 239
01-18 23:33:20.947 2836-2836/com.will.testdemo I/debugLog:  getWidth() = 339

能够看到实际高度和测试的高度是不同的,由于咱们在 onLayout 中作了修改。

由于 View 的绘制过程和 Activity 的生命周期是不一样步的,因此咱们可能在 onCreate 中获取不到值。这里提供几种方法来获取

1.Activity 的 onWindowFocusChanged 方法

1
2
3
4
5
6
override fun onWindowFocusChanged(hasFocus: Boolean) {
       super.onWindowFocusChanged(hasFocus)
       if (hasFocus){
           //获取 view 的大小
       }
   }

2.view.post(runnable) 也就是我上面使用的方法
3.ViewTreeObserver 这里童鞋们搜索下就能够找到使用方法,篇幅较长就不举例子了

Draw 绘制过程

肯定了 View 的大小和位置后,那就要开始绘制了,Draw 过程就比较简单,它的做用是将 View 绘制到屏幕上面。View 的绘制过程遵循以下几步:

  1. 绘制背景 background.draw (canvas)
  2. 绘制本身(onDraw)
  3. 绘制 children(dispatchDraw)
  4. 绘制装饰(onDrawScrollBars)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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);

            drawAutofilledHighlight(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);

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

            if (debugDraw()) {
                debugDrawFocus(canvas);
            }

            // we're done...
            return;
        }
        
        //省略代码..
}

View 的绘制过程的传递是经过 dispatchDraw 实现的,dispatchdraw 会遍历调用全部子元素的 draw 方法,如此 draw 事件就一层一层的传递下去。和 Layout 同样 View 是不会帮咱们绘制内容部分的,所以须要每一个视图根据想要展现的内容来自行绘制,重写 onDraw 方法。具体可参考 TextView 或者 ImageView 的源码。

最后

View 的工做流程和原理到这就分析完了,难点主要是 MeasureSpec 测量过程,须要童鞋们认真揣摩。

最后给你们分享一份很是系统和全面的Android进阶技术大纲及进阶资料,及面试题集

想学习更多Android知识,请加入Android技术开发交流 7520 16839

进群与大牛们一块儿讨论,还可获取Android高级架构资料、源码、笔记、视频

包括 高级UI、Gradle、RxJava、小程序、Hybrid、移动架构、React Native、性能优化等全面的Android高级实践技术讲解性能优化架构思惟导图,和BATJ面试题及答案!

群里免费分享给有须要的朋友,但愿可以帮助一些在这个行业发展迷茫的,或者想系统深刻提高以及困于瓶颈的

朋友,在网上博客论坛等地方少花些时间找资料,把有限的时间,真正花在学习上,因此我在这免费分享一些架构资料及给你们。但愿在这些资料中都有你须要的内容。

相关文章
相关标签/搜索