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

1 初识 ViewRoot 和 DecorView

1.1 ViewRootImpl 类的作用是什么?

  • ViewRoot 对应于 ViewRootImpl 类,是连接 WindowManagerDecorView 的纽带,更广一点可以说是 WindowView 之间的纽带。换句话说,Window 需要通过 ViewRootImpl 才能与 View 建立联系。

    ViewRoot 这个类目前已经没有了,是老版本中的一个类,在 Android2.2 以后用 ViewRootImpl 代替 ViewRoot

  • ViewRootImpl 完成 View 的三大流程,包括 measurelayoutdraw 过程。
  • ViewRootImplDecorView 分发收到的由用户发起的输入事件,包括按键事件(KeyEvent)和点击事件(MotionEvent)。

1.2 ViewRootImpl 对象是如何和 DecorView 关联起来的?

在调用 startActivity 方法启动Activity后,最终会调用到 ActivityThreadhandleLaunchActivity() 方法中:

private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    // 初始化 WindowManagerService
    WindowManagerGlobal.initialize();
    // 完成 Activity对象 的创建,调用 Activity 对象的 attach 方法和onCreate 方法
    Activity a = performLaunchActivity(r, customIntent);
    if (a != null) {
        r.createdConfig = new Configuration(mConfiguration);
        Bundle oldState = r.state;
        // 调用 Activity 对象的 onResume 方法,把 DecorView 对象添加到 Window 中。
        handleResumeActivity(r.token, false, r.isForward,
                !r.activity.mFinished && !r.startsNotResumed);
    }
}

需要说明的是,handleLaunchActivity 方法中的注释中是一个总的说明,下面进行详细地说明:

在调用 Activityattach 方法中,创建了 mWindow 对象:

mWindow = new PhoneWindow(this);

ActivityonCreate 方法中,通过 setContentView 方法,会创建一个 DecorView 对象:

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

PhoneWindow 中会创建 DecorView 对象:

private void installDecor() {
    if (mDecor == null) {
        mDecor = generateDecor();

handleResumeActivity 中,在调用完 Activity 对象的 onResume 方法之后,会把 DecorView 对象添加到 Window 里面:

View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (a.mVisibleFromClient) {
a.mWindowAdded = true;
// 把 DecorView 对象添加到 Window 中,真正调用的是 WindowManagerImpl 的 addView 方法。
wm.addView(decor, l);

WindowManagerImpl 中调用 addView 方法,内部 WindowManagerGlobaladdView 方法:

private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mDisplay, mParentWindow);
}

WindowManagerGlobaladdView 方法中,会创建一个 ViewRootImpl 对象,并调用ViewRootImpl 对象的 setView 方法,就把 DecorView 对象(view 对象就是 DecorView 对象)和 ViewRootImpl 对象关联起来了:

public void addView(View view, ViewGroup.LayoutParams params,
                    Display display, Window parentWindow) {
    root = new ViewRootImpl(view.getContext(), display);
    root.setView(view, wparams, panelParentView);
}

1.3 顶级 View 的整体绘制流程是什么?

顶级 View 的绘制流程是从 ViewRootImplperformTraversals() 方法开始的,performTraversals() 方法经过 measure、layout 和 draw 三个过程才能最终把一个 View 绘制出来。

  • measure 过程用来测量 View 的宽和高;
  • layout 过程用来确定 View 在父容器中的放置位置,即确定 View 的四个顶点的坐标和实际的 View 的宽/高;
  • draw 过程负责将 View 绘制在屏幕上。

performTraversals() 的工作流程如下:

2 理解 MeasureSpec

2.1 说一说对于 MeasureSpec 的理解?

  • 从类上看,MeasureSpec 类是 View 类的内部类,它封装了由父容器传递给子元素的布局要求,这个值在很大程度上决定了一个 View 的尺寸规格,之所以说很大程度上是因为这个过程还受父容器的影响,因为父容器影响 ViewMeasureSpec 的创建过程。具体来说,在测量过程中,系统会将 ViewLayoutParams 根据父容器所施加的规则转换成对应的 MeasureSpec,然后再根据这个 MeasureSpec 来测量出 View 的宽和高;
  • 从值上看,MeasureSpec 代表一个 int 值(32位),高 2 位代表 SpecMode(指测量模式),低 30 位代表 SpecSize(指在某种测量模式下的规格大小,表示尺寸大小的参考值或者建议值)。
    public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        // 对应的二进制是 11000000 00000000 00000000 00000000,作用是打包和解包 MeasureSpec 值。
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
        // 对应的二进制是 00000000 00000000 00000000 00000000,
        // 未指定模式:父容器不对 View 有任何限制,要多大给多大,父容器对 View 说,我不管你,你使劲造吧,比如 ScrollView 作为父容器,在高度方向会给子 View 这个模式。
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
        // 对应的二进制是 01000000 00000000 00000000 00000000,
        // 精确模式:父容器已经检测出 View 所需要的精确大小
        public static final int EXACTLY     = 1 << MODE_SHIFT;
        // 对应的二进制是 10000000 00000000 00000000 00000000,
        // 最大模式:父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值,具体是什么值要看不同 View 的具体实现。
        public static final int AT_MOST     = 2 << MODE_SHIFT;
       
        public static int makeMeasureSpec(int size, int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                // (size & ~MODE_MASK) 表示取出 size 这个 int 值的低 30 位
                // (mode & MODE_MASK) 表示取出 mode 这个 int 值的高 2 位
                // (size & ~MODE_MASK) | (mode & MODE_MASK) 表示打包一个 MeasureSpec 值,也是 int 型。
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
      
        public static int getMode(int measureSpec) {
            // 从 MeasureSpec 值中解包出 SpecMode 这个 int 值
            return (measureSpec & MODE_MASK);
        }
     
        public static int getSize(int measureSpec) {
        	// 从 MeasureSpec 值中解包出 SpecSize 这个 int 值
            return (measureSpec & ~MODE_MASK);
        }
        static int adjust(int measureSpec, int delta) {
            final int mode = getMode(measureSpec);
            if (mode == UNSPECIFIED) {
                // No need to adjust size for UNSPECIFIED mode.
                return makeMeasureSpec(0, UNSPECIFIED);
            }
            int size = getSize(measureSpec) + 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);
        }
    	// 打印一个 MeasureSpec 里的 specMode 和 specSize 信息。这个方法好用。
        public static String toString(int measureSpec) {
            int mode = getMode(measureSpec);
            int size = getSize(measureSpec);
            StringBuilder sb = new StringBuilder("MeasureSpec: ");
            if (mode == UNSPECIFIED)
                sb.append("UNSPECIFIED ");
            else if (mode == EXACTLY)
                sb.append("EXACTLY ");
            else if (mode == AT_MOST)
                sb.append("AT_MOST ");
            else
                sb.append(mode).append(" ");
            sb.append(size);
            return sb.toString();
        }
    }
    

2.2 对于顶级 View(如 DecorView)和普通 View 来说,获取传入它们的 measure 方法的 MeasureSpec 的方式有什么不同?

  • 对于顶级 View(如 DecorView),其 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 来共同确定,可以查看 ViewRootImpl 中的 measureHierarchy 方法(这是一个测量协商方法),有如下代码:

    // getRootMeasureSpec 传入期望的窗口宽度和窗口自身的LayoutParams的宽度,获取宽度的MeasureSpec
    childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
    // getRootMeasureSpec 传入期望的窗口高度和窗口自身的LayoutParams的高度,获取高度的MeasureSpec
    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
    // 执行测量,内部就是调用了 View 的 measure 方法。
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    

    我们主要去看看 getRootMeasureSpec 方法的内部实现:

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

    绘制出表格如下:

    在这里插入图片描述

  • 对于普通 ViewView 的 measure 过程由其父容器 ViewGroup 传递而来,可以查看 ViewGroupmeasureChild 方法:

    protected void measureChildWithMargins(View child,
              int parentWidthMeasureSpec, int widthUsed,
              int parentHeightMeasureSpec, int heightUsed) {
          final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
          // getChildMeasureSpec 传入父容器的宽度MeasureSpec,父容器已用的空间(这里是父容器的左右padding,子 View 的左右 margin,父容器宽度方向已用尺寸),子元素的LayoutParams宽度
          final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                  mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                          + widthUsed, lp.width);
          // getChildMeasureSpec 传入父容器的高度MeasureSpec,父容器已用的空间(这里是父容器的上下padding,子 View 的上下 margin,父容器高度方向已用尺寸),子元素的LayoutParams高度  
          final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                  mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                          + heightUsed, lp.height);
    
          child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
      }
    

    可以看到,子 View 需要的 MeasureSpec 由父容器的 MeasureSpec 和子 View自身的 LayoutParams 来共同确定,还和父容器的 padding,子 View 的 margin,父容器已用空间 有关。

    getChildMeasureSpec 方法的过程如下面表格所示:

    在这里插入图片描述
    其中,parentSize 是指父容器中目前可以使用的大小。需要说明的是,当子 View 的布局参数为 match_parent 或者 wrap_content,并且父容器的测量模式是 UNSPECIFIED,这时给子 View 的测量尺寸是 0 或者 parentSize,这是由 View.sUseZeroUnspecifiedMeasureSpec 来决定的。

怎么看上面的表格呢?

艺术探索里面是一行一行地看。按照代码流程来看的话,是一列一列地看。但是,千万不要斜着看啊。

这里也是采用一行一行地看。

当子 View 采用具体的宽/高时,不管父容器的 MeasureSpec 是什么,ViewMeasureSpec 都是精确模式+子 ViewLayoutParams 中指定的具体的大小。

当子 View 的宽/高采用 match_parent 时,这时候父容器的 MeasureSpec 会发挥作用,子 View 的模式总是跟父容器的模式一样:

  • 如果父容器的模式是精确模式(EXACTLY),那么子 ViewMeasureSpec 就是精确模式+父容器的剩余空间(或者说父容器的可用空间);
  • 如果父容器的模式是最大模式(AT_MOST),那么子 ViewMeasureSpec 就是最大模式+父容器的剩余空间(或者说父容器的可用空间);
  • 如果父容器的模式是未指定模式(UNSPECIFIED),那么子 ViewMeasureSpec 就是未指定模式+父容器的剩余空间或者0。

当子 View 的宽高采用 wrap_content 时,这时候父容器的 MeasureSpec 同样会发挥作用:

  • 如果父容器的模式是精确模式(EXACTLY),那么子 ViewMeasureSpec 就是最大模式+父容器的剩余空间(或者说父容器的可用空间);
  • 如果父容器的模式是最大模式(AT_MOST),那么子 ViewMeasureSpec 就是最大模式+父容器的剩余空间(或者说父容器的可用空间);
  • 如果父容器的模式是未指定模式(UNSPECIFIED),那么子 ViewMeasureSpec 就是未指定模式+父容器的剩余空间或者0。

需要特别总结说明的是,当子 View 的宽高采用 wrap_content 时,不管父容器的模式是精确模式还是最大模式,子 View 的模式总是最大模式+父容器的剩余空间。

2.3 当 View 声明了 match_parent,那么 mode 一定是 EXACTLY,当 View 声明了 wrap_content,那么 mode 一定是 AT_MOST,这种说法正确吗?

对于顶层 View 来说,是正确的;对于普通 View 来说,是错误的。
参考:每日一问 | 自定义控件测量模式真的和 match_parent,wrap_content 一一对应吗

3 View的工作流程

3.1 View 类和 ViewGroup 类的区别和联系是什么?

View 类是一个具体类,也就是说它没有任何抽象方法;
ViewGroup 类继承于 View 类,是一个抽象类,包含一个抽象方法:

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

3.2 简要说一下 View 的工作流程

View 的工作流程主要是指 measure、layout、draw 这三大流程,即测量、布局和绘制。measure是确定 View 的测量宽/高,layout 是确定 View 的最终宽/高和四个顶点的位置,draw 是将 View 绘制到屏幕上。

3.3 简要说一下 measure 过程

measure 过程分为 View 的 measure 过程和 ViewGroup 的 measure 过程。

View 的 measure 过程

如果是一个原始的 View,那么通过 measure 方法就完成了测量过程,在 measure 方法中会去调用 ViewonMeasure 方法,View 类里面定义了 onMeasure 方法的默认实现(我们需要理解默认实现,以便自定义 View 的时候清楚默认实现的作用或者在默认实现的基础上作进一步的修改):

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

源码中的写法很紧凑,我们把这个方法稍微改动一下,便于分步骤查看:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	// 先获取建议的最小宽度和高度
	int suggestedMinimumWidth = getSuggestedMinimumWidth();
	int suggestedMinimumHeight = getSuggestedMinimumHeight();
    // 再通过 getDefaultSize 方法获取宽度和高度的测量值
    int measuredWidth = getDefaultSize(suggestedMinimumWidth, widthMeasureSpec);
    int measuredHeight = getDefaultSize(suggestedMinimumHeight, heightMeasureSpec);
    // 最后调用 setMeasuredDimension 方法设置 View 宽度和高度的测量值。
    setMeasuredDimension(measuredWidth, measuredHeight);
}

先看一下 getSuggestedMinimumWidthgetSuggestedMinimumHeight 方法的源码:

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

我们只看 getSuggestedMinimumWidth 方法:如果 View 的背景 mBackground == null 成立,即 View 没有设置背景,那么 View 的建议最小宽度就是 mMinWidthmMinWidth 对应于 android:minWidth 这个属性所指定的值);如果 View 设置了背景,那么 View 的建议最小宽度为 mWidthmBackground.getMinimumWidth() 中较大的那个。mBackground 是一个 Drawable 对象,所以去看Drawable 类下的 getMinimumWidth 方法:

public int getMinimumWidth() {
    final int intrinsicWidth = getIntrinsicWidth();
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

可以看到,getMinimumWidth 方法获取的是 Drawable 的原始宽度。如果存在原始宽度(即满足 intrinsicWidth > 0),那么直接返回原始宽度即可;如果不存在原始宽度(即不满足 intrinsicWidth > 0),那么就返回 0。

ShapeDrawable 没有原始宽度和高度,而 BitmapDrawable 有原始宽度和高度。

接着看最重要的 getDefaultSize 方法:

// 这是一个 static 修饰的方法,所以是一个工具方法
/**
 * Utility to return a default size. Uses the supplied size if the
 * MeasureSpec imposed no constraints. Will get larger if allowed
 * by the MeasureSpec.
 *
 * @param size Default size for this view
 * @param measureSpec Constraints imposed by the parent
 * @return The size this view should be.
 */
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;
}

如果 specModeMeasureSpec.UNSPECIFIED 即未指定模式,那么返回由方法参数传递过来的尺寸(即默认尺寸,或者说指定的尺寸,内部提供的尺寸)作为 View 的测量宽度和高度;
如果 specMode 不是 MeasureSpec.UNSPECIFIED 即是最大模式或者精确模式,那么返回从 measureSpec 中取出的 specSize 作为 View 测量后的宽度和高度。

注意到注释里的一句话:

Utility to return a default size. Uses the supplied size if the
MeasureSpec imposed no constraints. Will get larger if allowed
by the MeasureSpec.

返回一个默认尺寸的工具方法。如果 MeasureSpec 没有施加任何限制,就使用提供的尺寸,否则将获取 MeasureSpec 所允许的更大的尺寸。

为什么注释里会说 specMode 不是 MeasureSpec.UNSPECIFIED 即是最大模式或者精确模式时,获取到的测量宽高会较大呢?

还是看一下这个表格:在这里插入图片描述
specModeEXACTLY 或者 AT_MOST 时,View 的布局参数为 wrap_content 或者 match_parent 时,给 View 的 specSize 都是 parentSize。这会比建议的最小宽高要大。

但是,注意到当 specModeEXACTLY 或者 AT_MOST, 而View 的布局参数为 wrap_contentView 的布局参数为 match_parent 的效果是一样的,最后都会给 View 的 specSize 为 parentSize。这是不符合我们的预期的。

直接继承 View 的自定义控件需要重写 onMeasure 方法并设置 wrap_content 时的自身大小(给 View 指定一个默认的内部宽高)。具体处理方式没有固定的依据,根据需要灵活指定。可以查看 TextViewImageViewProgressBarSpace 等源码针对 wrap_content 情形的处理方式。

ViewGroup 的 measure 过程

如果是一个 ViewGroup,除了完成自己的 measure 过程以外,还会遍历去调用所有子元素的 measure 方法,各个子元素再递归去执行 measure 过程。

自定义ViewGroup 完成自己的 measure 过程是指在 onMeasure 方法里调用了 setMeasuredDimension(int measuredWidth, int measuredHeight) 方法,参数分别是 ViewGroup 的测量宽度和测量高度。

有的自定义 ViewGroup 是先完成自己的 measure 过程,再去完成子元素的 measure 过程,如 ViewPager,伪代码如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(自定义ViewGroup的测量宽度,自定义ViewGroup的测量高度);
    // 测量每一个子View
    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);
	    }
	}
}

有的自定义 ViewGroup 是先完成子元素的 measure 过程,再完成自己的 measure 过程,大多数的自定义 ViewGroup 都是这样的,伪代码如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 测量每一个子View
    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);
	    }
	}
	setMeasuredDimension(自定义ViewGroup的测量宽度,自定义ViewGroup的测量高度);
}

ViewGroup 并没有重写 ViewonMeasure 方法,但是它提供了 measureChildrenmeasureChildmeasureChildWithMargins 这几个方法专门用于测量子元素。

// 遍历所有的子元素,并使用 measureChild 方法来对每一个子元素进行 measure。
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];
        // 只有当子元素的可见性不是 GONE 时,才对它进行测量。这一点是个细节。
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}
// 测量每一个子元素:最重要的逻辑是通过 getChildMeasureSpec 方法来获取测量子元素所需要的宽度 MeasureSpec 和 高度 MeasureSpec。
// getChildMeasureSpec(int spec, int padding, int childDimension) 方法需要的参数必须搞清楚:
// spec 是 ViewGroup 从 onMeasure 方法接收到的 MeasureSpec,
// padding 是 ViewGroup 已使用的空间,childDimension 是子元素的布局参数的宽和高。
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);
}

protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    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);
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

为什么ViewGroup 类没有像 View 一样对其 onMeasure 方法做统一的实现,实际上 ViewGroup 继承了 View 类对 onMeasure 方法的实现?这是因为不同的 ViewGroup 子类有不同的布局特性,这导致它们的测量细节各不相同,比如 LinearLayoutRelativeLayout 这两者的布局特性显然不同,因此 ViewGroup 无法统一实现。

3.4 View 的子类可以重写 View 类的 measure 方法吗?

不可以,这是因为 View 类中的 measure 方法是一个 final 类型的方法。

public final void measure(int widthMeasureSpec, int heightMeasureSpec)

3.5 为什么说在 onLayout 方法中去获取 View 的测量宽/高是一个好的习惯?

正常情况下,Viewmeasure 过程完成以后,通过 getMeasuredWidth/getMeasuredHeight 方法就可以正确地获取到 View 的测量宽/高;但是,在某些极端情况下,系统可能需要多次 measure 才能确定最终的测量宽/高,在这种情形下,在 onMeasure 方法中拿到的测量宽/高很可能是不准确的。

3.6 在 Activity 启动时,如何获取某个 View 的宽/高?

方式解释
Activity/ViewonWindowFocusChangedonWindowFocusChanged 回调时,表示 View 已经初始化完毕了,宽/高已经准备好了,所以这时去获取宽/高是没有问题的。但是,onWindowFocusChanged 会被多次调用,当 Activity 的窗口得到焦点和失去焦点时均会被调用一次。
View.post(Runnable runnable)通过 post 可以将一个 Runnable 对象放到消息队列的尾部,然后等到 Looper 调用此 Runnable 的时候,View 也已经初始化好了。注意:是 View.post 方法而不是 Handler.post 方法。参考:每日一问 在Activity 的 onResume 方法中 view.postRunnable 能获取到 View 宽高吗?每日一问 在Activity 的 onResume 方法中 handler.postRunnable 能获取到 View 宽高吗?
ViewTreeObserver 的多个回调,如 OnGlobalLayoutListener 接口。OnGlobalLayoutListener 接口:当 View 树的状态发生改变或者 View 树内部的 View 的可见性发生改变时,它的 onGlobalLayout 方法将被回调。需要注意的是,伴随着 View 树的状态改变等,onGlobalLayout 会被调用多次。使用完接口后,记得移除。
使用 View.measure(int widthMeasureSpec, int heightMeasureSpec) 方法进行手动测量这种方法比较复杂。当 ViewLayoutParamsmatch_parent 时,这种方式不能测量出具体的宽/高;当 ViewLayoutParams 为具体的数值(如 100 dp)时,这种方式可以测量出具体的宽/高;当 ViewLayoutParamswrap_content 时, 这种方式不能测量出具体的宽/高。

3.7 简要说一下 layout 过程

如果是 View 的话,那么在它的 layout 方法中就确定了自身的位置(具体来说是通过 setFrame 方法来设定 View 的四个顶点的位置,即初始化 mLeftmRightmTopmBottom 这四个值),layout 过程就结束了。

如果是 ViewGroup 的话,那么在它的 layout 方法中只是确定了 ViewGroup 自身的位置,要确定子元素的位置,就需要重写 onLayout 方法;在 onLayout 方法中,会调用子元素的 layout 方法,子元素在它的 layout 方法中确定自己的位置,这样一层一层地传递下去完成整个 View 树的 layout 过程。

3.8 layout 方法和 onLayout 方法的区别是什么?

layout 方法的作用是确定 View 本身的位置,即设定 View 的四个顶点的位置,这样就确定了 View 在父容器中的位置;
onLayout 方法的作用是父容器确定子元素的位置,这个方法在 View 中是空实现,因为 View 没有子元素了,在 ViewGroup 中则进行抽象化,它的子类必须实现这个方法。

3.9 View 的 getMeasuredWidth 和 getWidth 这两个方法有什么区别?

  • getMeasuredWidth 获取的是测量宽度,定义了一个 View 想要在父 View 里的尺寸,getWidth 获取的是宽度,有时也被称为绘制宽度,定义了绘制时或者布局后屏幕上的 View 的实际尺寸。

  • 两者的赋值时机不同,测量宽/高的赋值时机要早于最终宽/高。具体来说,View 的测量宽/高形成于 Viewmeasure 过程,可以看 measure 方法中的 setMeasuredDimensionRaw 方法:

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

    getMeasuredWidth 获取的就是 mMeasuredWidth 值:

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

    再看一下 getWidth 方法:

    public final int getWidth() {
       return mRight - mLeft;
    }
    

    mRightmLeft 的赋值是在 layout 过程的 setFrame 方法中,所以 View 的最终宽/高形成于 Viewlayout 过程。

  • 两者的值多数情况下是相等的,但在某些特殊情况下会不一致。例如有两种特殊情况:

    • 重写 Viewlayout 方法:手动修改传入 layout 方法的参数值;
    • View 需要多次 measure 才能确定自己的测量宽/高,在前几次的测量过程中,其得出的测量宽/高有可能和最终宽/高不一致,但最终来看,测量宽/高还是会和最终宽/高相同。

参考:每日一问 | getWidth, getMeasuredWidth 有什么区别?

3.10 View 的绘制过程的步骤是什么?

  1. 绘制背景(background.draw(canvas););
  2. 绘制自己(onDraw);
  3. 绘制 children(dispatchDraw(canvas));
  4. 绘制装饰(onDrawScrollBars)。

dispatchDraw 方法的调用是在 onDraw 方法之后,也就是说,总是先绘制自己再绘制子 View

对于 View 类来说,dispatchDraw 方法是空实现的,对于 ViewGroup 类来说,dispatchDraw 方法是有具体实现的。

3.11 ViewGroup 的 draw 过程是如何传递的?

通过 dispatchDraw 来传递的。dispatchDraw 会遍历调用子元素的 draw 方法,如此 draw 事件就一层一层传递了下去。dispatchDraw 在 View 类中是空实现的,在 ViewGroup 类中是真正实现的。

3.12 View 类中的 setWillNotDraw 方法的含义及其开发意义是什么?

含义:如果一个 View 不需要绘制任何内容,那么就设置这个标记为 true,系统会进行进一步的优化。默认地,View 没有启用这个标记,但是View 的子类例如 ViewGroup 可以启用这个标记。特别地,如果覆写了 onDraw 方法,就要清除这个标记。
开发意义:当创建的自定义控件继承于 ViewGroup 并且不具备绘制功能时,就可以开启这个标记,便于系统进行后续的优化;当明确知道一个 ViewGroup 需要通过 onDraw 绘制内容时,需要关闭这个标记。

查看 LinearLayout 对这个方法的调用:setWillNotDraw(divider == null);,在有 divider 时才会关闭这个标记,否则是打开的;ScrollView 直接使用 setWillNotDraw(false);ViewStub 类则使用 setWillNotDraw(true);

4 自定义 View

4.1 自定义 View 的分类

分类用途特点
1.继承 View 重写 onDraw 方法用于实现一些不规则的效果,不方便通过布局的组合方式来达到需要通过绘制的方式来实现,即重写 onDraw 方法;需要自己支持 wrap_content,处理 padding
2.继承 ViewGroup 派生特殊的 Layout用于实现自定义的布局稍微复杂一些,需要合适地处理 ViewGroup 的测量、布局这两个过程,并同时处理好子元素的测量和布局过程
3.继承特定的 View用于扩展已有的 View 的功能不需要自己支持 wrap_content 和 padding 等
4.继承特定的 ViewGroup用于实现几种 View 组合在一起的效果不要自己处理 ViewGroup 的测量和布局这两个过程,方法2能实现的效果一般方法4也可以实现,但方法2更接近 View 底层

4.2 自定义 View 的注意事项

  • 对于直接继承 View 或者 ViewGroup 的控件,要在 onMeasure 方法中对 wrap_content 做特殊处理;
  • 必要时,让自定义 View 支持 padding
  • 尽量不要在自定义 View 中使用 Handler,用 View 自己的 post 方法就行;
  • 及时关闭自定义 View 中的线程或者动画,比如在 onDetachedFromWindow 方法中;
  • 自定义 View 带有嵌套滑动时,要处理好滑动冲突;
  • 有自定义布局属性的,在构造方法中取得属性后应及时调用recycle方法回收资源;
  • onDrawonTouchEvent 方法中都应避免创建对象,过多操作会造成卡顿;
  • 自定义 ViewGroup 要重载关于 LayoutParams 的几个方法;
  • 必要时添加对 View 的状态存储与恢复的支持;
  • 对于自定义 ViewGroup,需要绘制内容但是又没有在布局中设置background的话,会画不出来,这时候需要调用setWillNotDraw方法,并设置为false

参考


版权声明:本文为willway_wang原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。