《Android 开发艺术与探索》笔记——(4)View 的工作原理

ViewRoot 和 DecorView

ViewRoot 对应于 ViewRootImpl,用来对 view 进行操作,它和最顶层 view 一起组成了显示上的 Window。

DecorView 是 Activity、Dialog 中最顶层的 View。

View 的绘制流程总述:

View 的绘制流程是通过 ViewRoot 完成的。在 ActivityThread 中,当 Actiivity 被创建后,会通过 PhoneWindow 生成 DecorView,通过 WindowManager 创建 ViewRootImpl,ViewRootImpl 会对 DecorView 进行操作,和 DecorView 一起组成用来显示的 Window。

从 ViewRootImpl 的 performTraversals 方法开始,它经过 measure、layout 和 draw 三个过程才能将一个 View 绘制出来。

measure 确定了 View 的测量宽高。完成后,可以通过 getMeasuredWidth 和 getMeasuredHeight 来获取测量后的宽高,除特殊情况外,这个宽高都等于 View 最终的宽高。

layout 确定了 View 四个顶点的坐标和最终宽高。完成后,可以通过 getTop、getBottom、getLeft、getRight 获取四个顶点的位置,并可以通过 getWidth、getHeight 获取 View 的最终宽高。

draw 将 View 绘制到屏幕上。

MeasureSpec

MeasureSpec 包含了 View 的测量信息。

在测量过程中,系统会将 View 的 LayoutParams 根据父容器的 MeasureSpec 转换成相应的 MeasureSpec,然后根据这个 measureSpec 来测量出 View 的宽高。这个宽高不一定是最终的宽高。

MeasureSpec 的表示

MeasureSpec 代表一个 32 位 int 值,高 2 位代表 SpecMode,低 30 位代表 SpecSize,SpecMode 指测量模式,SpecSize 指规格大小。将它们组合成一个 int 值是为了避免过多的对象内存分配,为了方便操作,MeasureSpec 也提供了打包和解包的方法。

SpecMode 有三类,每一类都表示特殊的含义:

  • UNSPECIFIED

父容器不对 View 有限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。

  • EXACTLY

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

  • AT_MOST

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

MeasureSpec 和 LayoutParams 的对应关系

对于顶级 View:

其 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 共同决定。

对于普通 View:

其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 共同决定。

自身的 LayoutParams 说明了自己希望多大,父容器的 MeasureSpec 说明了能给多大。

对于普通 View,View 的 measure 会由 ViewGroup 的 measureChild 或 measureChildWithMargins 调用:

ViewGroup.measureChildWithMargins()、ViewGroup.measureChild() --->
ViewGroup.getChildMeasureSpec()

getChildMeasureSpec 的表格化:

EXACTLY/parentSize AT_MOST/parentSize UNSPECIFIED/0
dp/px EXACTLY/childSize EXACTLY/childSize
match_parent EXACTLY/parentSize AT_MOST/parentSize
wrap_content AT_MOST/parentSize AT_MOST/parentSize

表格的解释:

parent 的 SpecMode/SpecSize parent 的 SpecMode/SpecSize parent 的 SpecMode/SpecSize
child 的 LayoutParams child 的 SpecMode/SpecSize child 的 SpecMode/SpecSize

View 的工作流程

即 measure、layout、draw 三大流程

measure 过程

View 的 measure

public final void measure(int widthMeasureSpec, int heightMeasureSpec){
    ...
    onMeasure(widthMeasureSpec, heightMeasureSpec)
    ...
}

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

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

// UNSPECIFIED 的情况下:
protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth :
        max(mMinWidth, mBackground.getMinimumWidth());
}

// Drawable.class
public int getMinimumWidth() {
    // 有原始宽高,就返回原始宽高,无原始宽高返回 0
    // BitmapDrawable 有原始宽高,ShapeDrawable 无原始宽高
    final int intrinsicWidth = getIntrinsicWidth();
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

UNSPECIFIED 情况下,size 会取 mMinWidth 或 mBackground.getMinimmWidth()。

AT_MOST、EXACTLY 情况下,size 会取 MeasureSpec 的 SpecSize。

所以,直接继承 View 的自定义控件需要重新 onMeasure 方法并设置 wrap_content 时的自身大小,否则在布局中使用 wrap_content 就相当于使用 match_parcent。

因为如果不重写,那 View 的 MeasureSpec 就是 AT_MOST/parcentSize,取到的 size 就是 parcentSize,这与设置为 match_parcent 取到的 size 是一样的。

重写 onMeasure:

protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec,heightMeasureSpec);
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
    if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode ==                           MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWidth,mHeight);
    } else if (widthSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWidth,heightSpecSize);
    } else if (heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(widthSpecSize,mHeight);
    }
}

这个处理方法不止处理了 View 设置为 wrap_content 的情况,也处理了 View 设置为 match_parcent 且其父容器 SpecMode 为 AT_MOST 时的情况(也就是其父容器设置为 wrap_content)。

wrap_content 默认对应的 MeasureSpec 只有 AT_MOST/parentSize。

ViewGroup 的 measure

对于 ViewGroup 来说,除了自己的 measure,还要调用其子元素的 measure。和 View 不同,ViewGroup 是一个 抽象类,它没有重写 View 的 onMeasure 方法,而是提供了一个 measureChildren 方法。

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

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

measureChildren 方法只是 ViewGroup 提供的一个measure 方法,在部分 ViewGroup 里使用到。

ViewGroup 可以实现自己的 onMeasure 方法,如 LinearLayout 的 onMeasure 里没有用 measureChildren,而是使用了自定义的 measureVertical、measureHorizontal,里面会循环调用 measureChildWithMargins;RelativeLayout 则直接在 onMeasure 里循环调用了 measureChild。

以 LinearLayout 为例,展示流程:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}

measureVertical 的部分代码:

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    for (int i = 0; i < count; ++i) {
        ...
        // Determine how big this child would like to be. If this or
        // previous children have given a weight, then we allow it to
        // use all available space (and we will shrink things later
        // if needed).
        measureChildBeforeLayout(
           child, i, widthMeasureSpec, 0, heightMeasureSpec,
           totalWeight == 0 ? mTotalLength : 0);
           if (oldHeight != Integer.MIN_VALUE) {
           lp.height = oldHeight;
        }

        final int childHeight = child.getMeasuredHeight();
        final int totalLength = mTotalLength;
        mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
               lp.bottomMargin + getNextLocationOffset(child));

        ...
    }
    ...
}

void measureChildBeforeLayout(View child, int childIndex,
        int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
        int totalHeight) {
    measureChildWithMargins(child, widthMeasureSpec, totalWidth,
            heightMeasureSpec, totalHeight);
}

measureChildBeforeLayout 会调用 child View 的 measure 方法,之后会通过 totalLength 记录各 child 的总高度。当 child 的测量完成后,LinearLayout 会测量自己的大小。

// 还是在 measureVertical 内部
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;

int heightSize = mTotalLength;

// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;

...

setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
    heightSizeAndState);
...

View 的 measure 过程是三大流程里最复杂的一个,measure 完成后,通过 getMeasuredWidth/Height 就可以获取到 View 的测量宽/高。需要注意的是,在某些极端情况下,系统可能需要多次 measure 才能确定最终的测量宽/高(如 FrameLayout、RelativeLayout measure 两次,待验证),这种情形下,在 onMeasure 中拿到的测量宽/高很可能是不准确的。一个比较好的习惯是在 onLayout 中获取 View 的测量宽/高或者最终宽/高。

View的三次measure,两次layout和一次draw

onCreate 中获取 View 宽高

在 Activity 启动时希望获得 View 的宽高,不能在 onCreate、onResume 中去做。因为 View 的 measure 过程和 Activity 的生命周期方法不是同步的,所以无法保证在 onCreate、onResume 执行时 View 的 measure 已经执行完毕,这时候就会得到 0。

下面介绍四种 onCreate 中获取 View 宽高的方法

(1)Activitity/View#onWindowFocusChanged

onWindowFocusChanged 这个方法的调用时机是,该 view 的 window 的获取焦点、失去焦点时。

window 获取焦点,说明已经在展示在用户面前,这时是可以获取到 view 的宽高的。

onWindowFocusChanged 会被调用多次,每次获取、失去焦点都会调用,所以,如果频繁地 onResume、onPause,那么 onWindowFocusChanged 也会被频繁调用。需要注意。

(2)view.post(runnable)

通过 post 可以将一个 runnable 投递到消息队列尾部,等 Looper 调用到此 runnable 时,View 已经初始化好了。

public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {//在 activity onCreate 中attachInfo 当前还没有赋值
        return attachInfo.mHandler.post(action);
    }
    // Assume that post will succeed later
    ViewRootImpl.getRunQueue().post(action);
    return true;
}

runable 会在 ViewRootImpl 的 performTraversals 中被 handler post,当 performTraversals 执行完后,runable 才会执行,也就是 view 绘制完毕后 runable 才会执行,这时就可以拿到宽高了。

View.post(Runnable)在onCreate获取控件宽高分析

(3)ViewTreeObserver

使用 ViewTreeObserver 的众多回调可以完成这个功能,比如使用 onGlobalLayoutListener 这个接口,当 View 树的状态发生改变或者 View 树内部的 View 的可见性发生改变时,onGlobalLayout 方法将被回调,因此这时获取 View 宽高的一个时机。需要注意,随着 View 树状态变化,onGlobalLayout 会被调用多次。典型代码如下:

ViewTreeObserver observer = mView.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        mView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
        int width = mView.getMeasuredWidth();
        int height = mView.getMeasuredHeight();
    }
});

(4)view.measure(int widthMeasureSpec, int heightMeasureSpec)

通过手动 measure 得到 View 的宽高,根据 LayoutParams 分三种情况:

match_parcent:

当其 parcent 的 SpecMode 为 EXACTLY 时,没有办法。因为这时 View 的 measure 必须知道 parcentSize,而这我们得不到。

当其 parcent 的 SpecMode 为 AT_MOST 时,可以通过下面 wrap_content 的方法来获取,因为这时 match_parcent 的效果与 wrap_content 的效果是一样的,获取到的 MeasureSpec 都是 AT_MOST/parcentSize。

(这里我认为书中表述有误,我按我的理解写了。)

具体的数值(dp/px):

// 宽高都为 100 px
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int heighMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
mView.measure(widthMeasureSpec, heighMeasureSpec);
int width = mView.getMeasuredWidth();
int heigh = mView.getMeasuredHeight();

其实这种情况下,如果是 dp 的话可以直接换算成 px,如果是 px 的话根本不用算。

wrap_content:

int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
int heighMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
mView.measure(widthMeasureSpec, heighMeasureSpec);
int width = mView.getMeasuredWidth();
int heigh = mView.getMeasuredHeight();

如上所说,这时获取到的 MeasureSpec 是 AT_MOST/parcentSize。因为 parcentSize 不确定,这时可以按 parcentSize 理论上的最大值(1<<30-1)来 measure。这需要 View 对 AT_MOST 的情况做处理,就像上面所说的重写 onMeasure,否则获取到的宽高就是 (1<<30-1) 了。

layout 过程

Layout 的作用是 ViewGroup 用来确定子元素的位置,当 ViewGroup 的位置被确定后,它在 onLayout 中会遍历所有的子元素并调用其 layout 方法,layout 又会调用 onLayout。即 layout 方法确定 View 本身位置,onLayout 确定所有子元素位置。

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

首先通过 setFrame 来设定 View 的四个顶点的位置,即初始化 mLeft、mRight、mTop 和 mBottom 四个值,View 的四个顶点确定了,其在父容器中的位置也就确定了;接着会调用 onLayout 方法,这个方法的用途是确定子元素的位置,和 onMeasure 类似,onLayout 的具体实现和布局有关,View 和 ViewGroup 均没有实现。

LinearLayout 的 onLayout 举例。

getWidth 和 getMeasuredWidth

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

一般情况下,它们都是相等的。

特殊情况包括:

人为修改 mRight、mLeft 的值,这会导致 getWidth 数值变化(当然这会导致 View 显示不正常且没什么意义)。如:

public void layout(int l, int t, int r, int b) {
    super.layout(l+10, t+10, r+10, b+10);
}

View 需要多次测量的时候,最后的测量值会等于最终值,但前几次的测量值可能与最终宽高不一致。

draw 过程

draw 过程包括:

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

setWillNotDraw

这个 View 的一个特殊方法。

如果一个 View 不需要绘制任何内容,那么设置这个标记位为 true 后,系统会进行相应的优化。ViewGroup 会默认开启这个标记位(View 没有),当我们的自定义控件继承于 ViewGroup 时,可以根据需不需要绘制来进行相应的处理。

自定义 View

自定义 View 的分类

继承 View 重写 onDraw 方法

需要自己支持 wrap_content,padding 也要自己处理。

继承 ViewGroup 派生特殊的 Layout

略复杂,需要自己处理 ViewGroup 的 measure、layout 及其子元素的 measure、layout。

继承特定的View

比较常见,用于扩展已有 View 的功能,如 TextView、ImageView 等。不需要自己支持 wrap_content、padding。

继承特定的 ViewGroup

如 LinearLayout,不需要自己处理 ViewGroup 的测量和布局过程。

自定义 View 须知

1. 让 View 支持 wrap_content

上面说了很多次,需要重写 onMeasure。

2. 如有必要,支持 padding

直接继承于 View,需要在 draw 方法中处理 padding。直接继承于 ViewGroup,需要在 onMeasure 和 onLayout 中考虑 padding 和子元素的 margin 对其造成的影响。

3. 尽量不要在 View 中使用 Handler,没必要

View 本身就提供了 post 系列方法。

4. View 中如果有线程或动画,需要及时停止,参考 View#onDetachedFromWindow

如果有线程或动画需要停止,onDetachedFromWindow 是一个很好的时机。当包含此 View 的 Activity 退出或当前 View 被 remove 时,View 的 onDetachedFromWindow 方法会被调用,和它对应的是 onAttachedToWindow,当包含此 View 的 Activity 启动时,View 的 onAttachToWindow 会被调用(在 Activity#onResume 后,View#onDraw 前)。同时,当 View 变得不可见时也要及时停止,避免内存泄漏。

5. View 带有滑动嵌套情形时,需要处理好滑动冲突

猜你喜欢

转载自blog.csdn.net/gdeer/article/details/80114984