View 工作原理笔记整理

主要摘抄自:《Android 开发艺术探索》

参考文章:https://lrh1993.gitbooks.io/android_interview_guide/content/android/basis/custom_view.html


1、View 的工作流程

View 的工作流程主要指 measure(测量)、layout(布局)、draw(绘制)这三个流程。

DecorView 作为顶级 View,但是其会附属在 ViewRootImpl 上(即 ViewRoot)。

View 树的绘制流程从 ViewRootImpl#performTraversals() 开始,在该方法中,会依次调用 performMeasure()performLayout()performDraw() 来触发顶级 View 的测量、布局,以及绘制。

1.1 measure 过程

performMeasure() 方法中会直接调用 View#measure(),该方法为 final 类型的,在 measure() 中又会进一步调用 View#onMeasure()

onMeasure() 的作用了是为了测量自身的尺寸,在该方法中,在测量出 width、height 之后,在其中调用setMeasuredDimension() 设定 View 的测量宽/高信息,完成 View 的测量操作setMeasuredDimension() 方法必须在 onMeasure() 中主动调用,在 setMeasuredDimension() 方法调用之后,我们才能使用 getMeasuredWidth()getMeasuredHeight() 来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是 0)

其中,对于 View 和 ViewGroup 来说,onMeasure() 的实现是有区别的。

  1. View 的 onMeasure() 只需要测量自身的尺寸,然后设定宽高即可;

  2. 而 ViewGroup 的 onMeasure() 方法一般需要根据布局特性进行重写,在里面需要依次测量子 View 的尺寸(即触发子 View 的 measure() 方法),可以通过调用默认的 measureChildren() 方法,或者手动遍历子 View 调用其 measure() 方法来实现。然后再根据子 View 的尺寸来计算自己的 测量尺寸,并设置自身的宽高(即手动调用 setMeasuredDimension() 方法)。
    而且,需要注意,在 ViewGroup 中调用子 View 的 measure() 时,需要 ViewGroup 自己根据 child 的 LayoutParams,以及自身的宽高来计算出 child 对应的 WidthMeasureSpec/HeightMeasureSpec(MeasureSpec 就包含了子 View 的 SpecModeSpecSize),从而通过 child.measure() 传递进去。

1.2 layout 过程

performLayout() 方法中,会调用 View#layout() 方法(为 public 类型,可被重写),layout() 方法用于确定 View 本身的位置

View#layout() 的大致流程如下:
(1)先直接或者间接的调用 setFrame() 方法,来设定 View 的四个顶点的位置(即 mLeft、mRight、mTop、mBottom),View 的四个顶点一旦确定,则 View 在父容器中的位置也就确定了。

(2)然后会调用 View#onLayout()该方法用于确定子 View 的位置,在 View 中默认为空实现,而在 ViewGroup 中为抽象方法,需要 ViewGroup 的子类去实现。在实现的时候,需要根据布局的要求,结合子 View 的可见性进一步的调用子 View 的 layout() 方法。

需要注意的是,View 最终的大小是在 layout 阶段确定的,因此一个好的习惯是在 onLayout() 中去获取 View 的测量宽高或者最终宽高

1.3 draw 过程

performDraw() 方法中会进一步调用 draw() 方法,然后进一步调用 drawSoftware() 方法,在该方法中,会调用 View#draw(Canvas)

View#draw(Canvas) 中会依次绘制如下内容:

  1. 绘制背景 drawBackground(canvas)
  2. 绘制自身的内容 onDraw(),默认空实现。而 ViewGroup 需要根据实际要求来决定是否重写该方法,但是作为控件容器,本身一般是没有内容的,此时就无须重写。
  3. 绘制子 View,通过dispatchDraw(),View 的该方法默认为空实现,而 ViewGroup 的有实现逻辑,其内部会通过调用 ViewGroup#drawChild() 间接调用 子 Viewdraw(Canvas, ViewGroup, long) 方法,该方法是让子 View 来绘制自己的(该方法是 View 基于 layer type 以及硬件加速来专门处理渲染行为的代码段),内部会在对应的情况调用 dispatchDraw() 或者 draw(Canvas)
  4. 绘制装饰 onDrawForeground(),包括 ScrollIndicators、ScrollBars

还需要注意,View 有一个方法,setWillNotDraw(boolean willNotDraw),如果一个 View 不需要绘制任何内容(即无须用 onDraw() 绘制自身的内容),那么通过该方法将对应的标记位设置为 true 之后,系统会进行相应的优化。View 默认没有启用,ViewGroup 默认启用了(是在构造方法中,调用 initViewGroup() 方法时,在该方法中直接调用 setFlags(WILL_NOT_DRAW, DRAW_MASK) 设置的)。

该标记位的意义为:如果自定义的 ViewGroup 本身没有内容需要绘制的话,则可以启用该标记位。而当明确需要通过 onDraw() 来绘制内容的时候,则需要显示的关闭它。


2、自定义 View

2.1 四种实现途径:
  1. 继承 View 重写 onDraw()
  2. 继承 ViewGroup 派生特殊的 Layout
  3. 继承特定的 View(如 TextView)
  4. 继承特定的 ViewGroup
2.2 注意事项
  1. 让 View 支持 wrap_content
  2. 让 View 支持 padding,因为直接继承 View 时,如果不在 onDraw() 方法中处理 padding,那么该属性是无法起作用的。另外直接继承 ViewGroup,也需要在onMeasure()onLayout() 中考虑 padding 和子元素的 margin 的影响,否则会导致这两者失效。
  3. 尽量不要在 View 中使用 Handler,View 内部本身提供了 post 系列方法,除非有明确的需要
  4. View 中如果有线程或者动画,需要及时停止,参考 View#onDetachedFromWindow()。当包含此 View 的 Activity 退出或者当前 View 被 remove 的时候,View 的 onDetachedFromWindow() 方法会被调用,与此对应的就是 onAttachedToWindow(),当包含此 View 的 Activity 启动时,View 的 onAttachedToWindow() 会被调用。另外,当 View 变得不可见时,也需要及时停止线程和动画,否则可能造成内存泄漏。
  5. View 带有滑动嵌套情形时,需要处理好滑动冲突
2.3 大致的实现内容
2.3.1 根据实际需要,定义自定义属性

(1)在 res/values 目录下创建自定义属性的 xml,比如 attrs.xml,也可以根据实际需要命名。

<resources>
	<declare-styleable name="CustomView">
		<attr name="attr_name" format="format style" />
	</declare-styleable>
</resources>

(2)在 View 的构造方法中解析自定义属性并处理。

public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
	super(context, attrs, defStyleAttr);
	// 得到自定义属性对应的 TypedArray 对象
	TypedArray a = context.obtainStyledAttributes(attrs, R.styleable. CustomView);
	xxx = a.getXXX(R.styleable. CustomView_attr_name, defaultValue);
	// 将资源进行回收
	a.recycle()
}

(3)在布局文件中使用自定义属性,使用的时候,要先定义命名空间,即 schemas 声明:

xmlns:schemas_name="http://schemas.android.com/apk/res-auto"

// 或者
xmlns:schemas_name="http://schemas.android.com/apk/res/应用的包名"
2.3.2 onMeasure() 方法

单一 View,一般重写此方法,针对 wrap_content 情况,规定 View 默认的大小值,避免与 match_parent 情况一致(如果不重写,对于 wrap_content 的处理会与 match_parent 一致 )。

而处理 wrap_content 情况,更准确的说,是处理 SpecMode 为 AT_MOST 的情况,此时需要设置默认的大小,或者根据控件自身的内容,以及 padding 来设置一个对应的值。

而对于 ViewGroup,若不重写,就会执行和父类 View#onMeasure() 相同逻辑,导致无法测量子 View。因此一般会重写 onMeasure() 方法,根据布局控件的实际要求遍历测量所有子 View 并调用其 meaure() 方法。(如 FrameLayout)。

2.3.3 onLayout() 方法

单一 View,不需要实现该方法。ViewGroup 必须实现,该方法是个抽象方法,实现该方法,来对子 View 进行布局。

2.3.4 onDraw() 方法

一般单一的 View 需要实现该方法,绘制实际内容,ViewGroup 则视情况而定。


3.有关 MeasureSpec

在调用 onMeasure(int widthMeasureSpec, int heightMeasureSpec) 的时候,会传递两个参数,分别是 width 和 height 的 MeasureSpec。

MeasureSpec 是一个 32 位的 int 值,高 2 位为 SpecMode(测量模式),低 30 位为 SpecSize(在某种测量模式下的规格大小)。

private static final int MODE_SHIFT = 30;
private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

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 getMode(int measureSpec) {
    //noinspection ResourceType
    return (measureSpec & MODE_MASK);
}

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

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

3.1 SpecMode 的类别

(1)UNSPECIFIED,父容器不对 View 有任何限制,要多大给多大,该状态一般用于系统内部,表示一种测量状态;

(2)EXACTLY,父容器已经监测出 View 所需要的精确大小,此时 View 的最终大小即为 SpecSize 所指定的值。其对应于 LayoutParams 中的 match_parent 和具体的数值这两种模式;

(3)AT_MOST,父容器指定了一个可用大小的 SpecSize,View 最大不能大于该值,具体是什么值看 View 的具体实现。其对应于 LayoutParams 中的 wrap_content。

3.2 View 的 MeasureSpec 的确定

分为两种情况:

(1)对于顶级 View(即 DecorView),其 MeasureSpec 由 窗口的尺寸 和其自身的 LayoutParams 共同决定。

// rootDimension 即 DecorView 的 LayoutParams 中设置的 width/height
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;
    // 固定大小,精确模式,大小即 LayoutParams 中指定的大小
    default:
        // Window wants to be an exact size. Force root view to be that size.
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}

(2)对于普通的 View,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 共同决定,此外还受 View 的 margin 及 padding 有关。可以参阅

// spec 为 parent 的 widthMeasureSpec 或者 heightMeasureSpec
// padding 为父容器设置的 padding,PaddingLeft+PaddingRight 或者 PaddingTop+PaddingBottom
// childDimension 即子 View 的 LayoutParams 设置的 width 或者 height
// 静态方法
ViewGroup.getChildMeasureSpec(int spec, int padding, int childDimension) 

该方法对应的处理逻辑如下表,其中 parentSize 指父容器中目前可使用的大小,即父容器的 specSize - padding 的值(在代码中为 Math.max(0, specSize - padding))。
在这里插入图片描述


4. 补充内容

(1)View 的测量宽高和最终宽高有什么区别?

该问题可以具体为 View 的 getMeasureWidth() 和 getWidth() 这两个方法的区别(关于 height 本质上一样)。

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

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

public final int getMeasuredWidth() {
	// mMeasuredWidth 本身是包含 SpecMode 的
    return mMeasuredWidth & MEASURED_SIZE_MASK;
}

public final int getMeasuredHeight() {
	// mMeasuredHeight 本身是包含 SpecMode 的
    return mMeasuredHeight & MEASURED_SIZE_MASK;
}

在 View 的实现中,View 的测量宽高与最终宽高是相等的。只不过测量宽高形成于 measure 过程,而最终宽高形成于 View 的 layout 过程,即两者的赋值时机不同。

但是也不是绝对的,比如重写 View 的 layout() 方法:

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

上述情况就会导致 View 的最终宽高比测量宽高大 100px。

还有一种情况就是某些情况下,View 需要 measure 多次才能确定自己的测量宽高,在前几次可能不一致,但是正常情况下,最终的测量宽高和最终宽高是相同的。

(2)正确获取 View 的宽高的途径

在 Activity 的 onCreate、onStart、onResume 的首次执行中均无法正确得到某个 View 的宽高信息,这是因为这几个生命周期方法执行时,无法保证 View 测量完毕。

可以通过一下几种方法解决:

1、Activity/View#onWindowFocusChanged()

该方法调用时,View 已经初始化完毕了。但是该方法会被调用多次,在 Activity onResume() 和 onPause() 的时候均就被调用。

2、View#post(Runnable)

执行该 runnable 的时候,View 已经初始化好了。

3、使用 ViewTreeObserver

@Override
@Override
protected void onStart() {
    super.onStart();
    view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
        	// 移除该 OnGlobalLayoutListener
            view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    });
}

伴随 View 树的状态改变,onGlobalLayout() 会被调用多次,因此需要在适当的时机移除该监听。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值