android: View的生命周期
今天看到了一篇不错的文章,是一位外国小哥写的,个人觉得不错,遂翻译之,英文好的同学可以直接移步 ——> 生肉: https://proandroiddev.com/the-life-cycle-of-a-view-in-android-6a2c4665b95e
概述
当我们查看一款App的时候,首先引起我们注意的就是屏幕上显示的内容,而屏幕上显示的内容就是 View 。
View是UI界面的基本构建块,它占据了一块矩形区域,负责绘图和事件处理。View同时也是android上其它UI控件的基类,可以用来创建其它交互式的UI组件(比如Button, TextView, 等等)。View的子类 ViewGroup 则是布局的基类,ViewGroup是一个不可见的容器,用于容纳其它的View(或其它的ViewGroup),而且还定义了相关的布局属性。
下图描述了android上的View与其它UI组件之间的关系:
View的生命周期
每个Activity 都有自己的生命周期,同样,View也有自己的生命周期。在屏幕上渲染的View必须经历这些生命周期方法才能正确地在屏幕上绘制。这些生命周期方法的每一种都有其重要性。下面我们来深入了解下View的生命周期:
构造函数
通常,我们对于View为什么有四种类型的构造函数感到困惑:
View(Context context) View(Context context, @Nullable AttributeSet attrs) View(Context context, @Nullable AttributeSet attrs, int defStyleAttr) View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)
View(Context context)
当从代码动态的创建View时就会用到这个简易的构造方法。这里的参数 context 是运行视图的上下文,通过它可以访问到当前的主题(theme), 资源(resources)等东西。
View(Context context, @Nullable AttributeSet attrs)
从XML布局里加载View时调用的构造方法。当从XML文件里创建View同时也为这个View指定了一些相应的属性时,就会调用此方法。这个版本的构造函数使用的默认样式是0,因此唯一应用的属性值是上下文主题和给定AttrubiteSet中的属性值。
View(Context context, @Nullable AttributeSet attrs, int defStyleAttr)
从XML文件执行加载并从主题属性(defStyleAttr) 中应用基本样式。View的这个构造方法允许View在加载时使用它自己定义的基本样式。比如,系统Button类的构造函数就调用了这个构造函数,并且为 defStyleAttr 这个参数提供 R.attr.buttonStyle 的样式属性;这样可以使得系统提供的按钮主题样式可以修改所有的View(特别是View的背景),以及Button类的样式属性。
参数 defStyleAttr 是当前主题的一个属性,其中包含了对样式资源的引用,这个样式资源为View的属性提供了一个默认值,不查找默认值可以将其设置为0。
View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)
从XML文件执行加载,并从主题属性或样式资源中应用特定于类的基本样式。View的这个构造方法允许View在加载时使用它自己的基本样式,与上个构造方法类似。
参数 defStyleRes 是样式资源的资源标识符,为View提供默认值,仅当 defStyleAttr 为0或在主题中找不到时使用。如果不查找默认值,则可以为0。
View的生命周期主要由三部分组成:
Attachment / Detachment (关联/分离)
Traversals(遍历)
State Save / Restore (状态保存/ 恢复)
一. Attachment / Detachment
这是将View关联到窗口(Window)或从窗口分离时的阶段。在此阶段,我们有一些方法可以接收回调来执行适当的操作。
● onAttachedToWindow()
当View关联到窗口时调用。在这个阶段,View知道它处于活动的状态并且具有可绘制的表面。因此,我们在此阶段就可以开始分配任何资源或设置listeners。
● onDetachedFromWindow()
当View与窗口分离时将调用此方法。此时,View不再具有可绘制的表面。在此阶段,你需要停止任何已执行的任务或清理任何已分配的资源。当我们在ViewGroup类上调用 removeView() 或者Activity被销毁时将调用此方法。
● onFinishInflate()
当布局加载器(LayoutInflater)从XML文件里完成了所有子View的加载时,将会调用此方法。
二. Traversals
之所以把此阶段称为“遍历”,是因为View的视图层次就像是从父节点(ViewGroup)到子节点的树状结构。因此,每个方法都是从父节点开始,一直遍历执行到最后一个节点:
测量(Measure)阶段和布局(Layout)阶段始终是一起发生。如上图所示,这是一个顺序的过程。
● onMeasure()
这一步用于测量View到底应该有多大。如果是ViewGroup,它会对它的每一个子View都调用测量,测量的结果可以帮助ViewGroup确定其自身的大小。
onMeasure(int widthMeasureSpec, int heightMeasureSpec) @param widthMeasureSpec Horizontal space requirements as imposed by the parent @param heightMeasureSpec Vertical space requirements as imposed by the parent
onMeasure() 没有返回值,由系统调用。
setMeasuredDimension() 用于显式地设置宽度(width)和高度(height)值。
● MeasureSpec
MeasureSpec 类封装了从父View传递到子View的布局要求。每个 MeasureSpec 代表对宽度或高度的要求 。 MeasureSpec 由大小(size)和模式(mode)组成,有三种可能的模式:
MeasureSpec.EXACTLY : 父View已经为View确定了一个确切的尺寸。不管子View有多大,都会给子View一个最大限制。这个属性给View设置了一个固定的宽度(比如为LinearLayout指定weight, 或者是为View指定match_parent属性)。
MeasureSpec.AT_MOST: 子View可以根据需要变化到它想要的大小。
MeasureSpec.UNSPECIFIED: 父View没有对子View添加任何约束,子View可以是任意大小。
● onLayout()
View在调用 onMeasure() 完成测量后即会调用此方法,目的是在于确定View在屏幕上的位置。
● onDraw()
尺寸(通过onMeasure)和位置(通过onLayout())已经由前面的步骤计算出来了,因此View在这个时候已经可以根据上述的尺寸和位置绘制自身。在 onDraw(Canvas canvas) 中,生成或更新的Canvas对象具有要发送到GPU的 OpenGL- ES命令列表(displayList)。
注意:切勿在onDraw()方法中创建对象,因为这个方法会被多次调用。
当特定的View的属性发生变化时,还有另外两个方法可以被使用,那就是:
● invalidate()
invalidate()方法用于强制重绘我们希望显示更改的特定View。 简单的说,如果View的外观发生了变化,而我们又想看到这些变化,就可以调用invalite()。
● requestLayout()
在某些情况下,View的状态会发生变化, requestLayout() 可以向android的视图系统发出信号,告诉系统,View需要重新执行自身的测量和布局阶段(onMeasure()->onLayout()->onDraw())。简单的说,当View的范围发生变化时,我们可以调用此函数。
注意:在View上调用任何方法时,必须在UI线程上,如果你正在其它线程上工作,并且想要从该线程更新View的状态,则应使用Handler。
三. State Save / Restore
● onSaveInstanceState()
要保存视图状态,首先需要为其提供一个ID。如果你的视图层级结构中有多个具有相同ID的View, 则将保存所有的状态,为了避免这种情况,最好为View指定一个唯一的ID。
其次,你需要一个类来继承 View.BaseSavedState , 然后保存其属性。为了更好的理解,下面举例说明:
● onRestoreInstanceState(Parcelable state)
在这里我们需要重写此方法,并从Parcelable 读取数据,然后根据Parcelable可用的数据编写逻辑。
@Override public Parcelable onSaveInstanceState() { Bundle bundle = new Bundle(); // The vars you want to save - in this instance a string and a boolean String someString = "something"; boolean someBoolean = true; State state = new State(super.onSaveInstanceState(), someString, someBoolean); bundle.putParcelable(State.STATE, state); return bundle; } @Override public void onRestoreInstanceState(Parcelable state) { if (state instanceof Bundle) { Bundle bundle = (Bundle) state; State customViewState = (State) bundle.getParcelable(State.STATE); // The vars you saved - do whatever you want with them String someString = customViewState.getText(); boolean someBoolean = customViewState.isSomethingShowing()); super.onRestoreInstanceState(customViewState.getSuperState()); return; } // Stops a bug with the wrong state being passed to the super super.onRestoreInstanceState(BaseSavedState.EMPTY_STATE); } protected static class State extends BaseSavedState { protected static final String STATE = "YourCustomView.STATE"; private final String someText; private final boolean somethingShowing; public State(Parcelable superState, String someText, boolean somethingShowing) { super(superState); this.someText = someText; this.somethingShowing = somethingShowing; } public String getText(){ return this.someText; } public boolean isSomethingShowing(){ return this.somethingShowing; } }
<END>