面对日常开发中多样化的需求,自定义View,是个常见、且实用的方式。
1. 引言
等到需要自定义View的时候,其他的就不必多言,Just do it。
2. View的生命周期
一个View从创建,到展示在屏幕上,再到最终的销毁,大致要流经这几个方法:
- constructor构造方法
- (ViewRootImpl.setView)
- onAttachedToWindow
- onWindowVisibilityChanged
- onVisibilityChanged
- onMeasure
- onSizeChanged
- onLayout
- onDraw
- onWindowFocusChanged
- (成功展示)
- (onActivityPause)
- onWindowFocusChanged
- onWindowVisilibityChanged
- (onActivityStop)
- onVisibilityChanged
- (onActivityDestroy)
- onDetachedFromWindow
以上,是个完整的流程,乍一看有些杂乱。概括的来讲,View的绘制只有3个步骤,第一步,确定自身的尺寸大小。第二步,确定在屏幕中的位置;最后一步,在目标位置绘制自身。
3. ViewRootImpl.setView
为什么调用了这个方法之后才会走View的流程呢?这个方法又是在哪调用的呢?先来简单回想下老知识。
Activity中有个Window变量,看代码可知是在attach时new出来的,而attach则是在ActivityThread.performLaunchActivity中调用的,紧接着调用了onCreate,总结地说便是,Window变量在onCreate调用之前创建的。
然后,便是onResume,这个方法是在ActivityThread中的handleResumeActivity中调用的,此外,还将Activity的decorView添加到了WindowManager中,
1 | // ActivityThread.java |
最后调用了WindowManager.addView,WindowManager有个实现类叫做WindowManagerImpl,WindowManagerImpl的addView又调用了WindowManagerGlobal的addView,所以,最终来看WindowManagerGlobal的addView,
1 | // WindowManagerGlobal.java |
至此,就很清晰了,在ViewRootImpl在讲DecorView添加到WindowManager时创建,在ViewRootImpl的doTraversals中,开始走View的流程。
4. View.draw
View的绘制主要依靠draw方法,它的方法介绍里,说明了最多可能会有7个步骤,
1 | // View.java |
可以看到,除了前景和背景,就剩下两个主要的步骤了,一个是绘制内容,一个是绘制子View,而在View中,这两个方法都是空实现,也就是说,一个View对象,即没有内容也没有子View。
5. 关于方法重写
我们在自定义View时,主要依靠继承View并重写它的onDraw和dispatchDraw,来实现不同需求。
一般来说,只会重写其中一个方法。如果是个独立的View,便重写onDraw用来绘制自身内容,如果包含子View,那么就重写dispatchDraw来绘制子View。当然,你也可以剑走偏锋、棋行险着,坚持两个都重写,既要绘制自身,又有子View可绘制。
当包含子View时,一般不直接继承View,而是继承ViewGroup。ViewGroup是View的子类,它重写了dispatchDraw,在里面加了一些通用逻辑。简单来讲,ViewGroup包含多个View,那么我在dispatchDraw里循环调用每个字View的draw方法,等到每个子View绘制完成,那么ViewGroup自然也绘制完成。但是实际情况要比这稍微复杂一些,比如可以有一些动画、一些临时效果的View,以及还有页面滚动的情况,等等。而ViewGroup的dispatchDraw里就是加了这些情况的处理逻辑,继承ViewGroup能减少一些重复的代码量。
6. View.onMeasure
上面提到,View绘制三步走,第一步就是测量尺寸大小。View里的尺寸测量步骤是这样的,首先明确的是View一定是存在于一个ViewGroup中,那么这个ViewGroup就会对View的尺寸有一定的限定。测量View时,回调用它的measure方法,
1 | public final void measure(int widthMeasureSpec, int heightMeasureSpec) { } |
方法的两个参数,便是ViewGroup传过来的,名字可以叫做测量规格,里面包含了模式和尺寸,宽度规格使用方式如下,
1 | int widthMode = MeasureSpec.getMode(widthMeasureSpec); |
高度规格同理,不赘述。mode是模式,size是尺寸。其中,mode有三种类型
MeasureSpec.EXACTLY
经过ViewGroup的测量,得出了View的精确大小,即为size的值。如指定了layout_width为具体的值,或是MATCH_PARENT,此时ViewGroup都可以测量出View的精确尺寸。MeasureSpec.AT_MOST
经过ViewGroup的测量,没有得出View的精准大小,但是得出来最大的可能尺寸。比如,View设置layout_width为WRAP_CONTENT,同时ViewGroup自身的宽度是100dp,那么View宽度便是0到100dp中的一个值。MeasureSpec.UNSPECIFIED
未指定,不限制,不常见。
measure方法是final的,不可重写,那么ViewGroup传来什么尺寸我View就只能用什么尺寸了吗?当然不是,measure中调用了onMeasure,我们可以重写这个方法,对ViewGroup传来的尺寸规格进行调整,以此来体现自定义的意义,比如,当某个参数设置为true时,不管ViewGroup传来什么规格,我都需要将宽度设置为高度的2倍,等形形色色的需求。
修改完尺寸规格,要记得调用setMeasureDimension将最新的尺寸传入
1 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
7. View.onLayout
绘制三步走的第二步。摆放,形象一点说,就是排版,View的layout方法里接收的就是ViewGroup指定的位置,
1 | public void layout(int l, int t, int r, int b) { } |
一般是继承ViewGroup时会重写这个方法,在这个方法里,根据这4个参数,计算出每个子View的参数。
8. View.onDraw
最后一步走。这个方法传来了一个画布Canvas,将所有需要绘制的内容,都绘制在这块画布上即可。
9. 插播:自定义ViewGroup
当我们需要自定义ViewGroup而不是View时,多数情况是需要封装一个组合View。比如一个多层半透明背景叠加、上层有水波动画,外加中心还有数字和说明的环形进度条,进度和动画还有着不同状态下的不同逻辑,这时,便可以把这个多个View组合的需求封装成一个独立的ViewGroup。
不用想着创建个类继承ViewGroup,然后在自己手动计算每个View的尺寸和位置。直接写个xml的布局文件,把这些View组合好,然后创建个类继承自xml中根布局所用的ViewGroup类型,在构造方法里,将这个xml布局加载到自身之中即可
1 | LayoutInflater.from(context).inflate(R.layout.view_pro, this, true); |