oynix

于无声处听惊雷,于无色处见繁花

Android自定义View

面对日常开发中多样化的需求,自定义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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ActivityThread.java

public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
// 分发 onResume 事件
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
final Activity a = r.activity;
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
if (!a.mWindowAdded) {
a.mWindowAdded = true;
// 将 DecorView 添加到 WindowManager 中
wm.addView(decor, l);
}
}
}

最后调用了WindowManager.addView,WindowManager有个实现类叫做WindowManagerImpl,WindowManagerImpl的addView又调用了WindowManagerGlobal的addView,所以,最终来看WindowManagerGlobal的addView,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// WindowManagerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow, int userId) {
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
ViewRootImpl root;
root = new ViewRootImpl(view.getContext(), display);
try {
root.setView(view, wparams, panelParentView, userId);
} catch(RuntimeException e) {}
}

// ViewRootImple.java
public void setView(View view, WindowMnager.LayoutParams attrs, View panelParentView, int userId) {
if (mView == null) {
mView = view;
requestLayout();
}
}

public void requestLayout() {
...
checkThread();
scheduleTraversals();
}

void scheduleTraversals() {
...
mChoregrapher.postCallback(Choregrapher.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}

// mTraversalRunnable的run中调用了doTraversal
void doTraversal() {
...
performTraversals();
}

private void performTraversals() {
final View host = mView;
...
host.dispatchAttachedToWindow();
...
host.dispatchWindowVisibilityChanged();
...
performMeasure();
...
performLayout();
...
performDraw();
}

至此,就很清晰了,在ViewRootImpl在讲DecorView添加到WindowManager时创建,在ViewRootImpl的doTraversals中,开始走View的流程。

4. View.draw

View的绘制主要依靠draw方法,它的方法介绍里,说明了最多可能会有7个步骤,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// View.java
public void draw(Canvas canvas) {
...
// Step 1, draw the background, if needed
drawBackground(canvas);
// Step 2, If necessary, save the canvas' layers to prepare for fading
// Step 3, draw the content
onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
}

可以看到,除了前景和背景,就剩下两个主要的步骤了,一个是绘制内容,一个是绘制子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
2
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(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
2
3
4
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...modify...
setMeasureDimension()
}

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);
------------- (完) -------------
  • 本文作者: oynix
  • 本文链接: https://oynix.com/2022/08/ea9fa0ecc1ab/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

欢迎关注我的其它发布渠道