自定义View总结

csdn
个人博客

一.学习自定义 View

自定义 View 是 Android 中的一个基础技能,但是也相对一个较难的知识点,关于如何学习自定义 View 个人认为可以参考如下文章:

在学习了相关的文章后,笔者也通过几天的时间实现了一个牛顿摆的自定义 View NewtonCradle,并在这个过程中将相关知识学习总结如下

二.多个构造器

1
2
3
4
5
6
7
8
9
10
11
public NewtonCradle(Context context) {
this(context, null);
}

public NewtonCradle(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public NewtonCradle(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

在自定义 View 的时候我们通常会 实现这三个构造器,实际上有四个构造器,因为第四个不常用所以一般只实现三个就行了

1.1个参数的构造器
1
2
3
4
5
6
7
/**
* Simple constructor to use when creating a view from code.
*
* @param context The Context the view is running in, through which it can
* access the current theme, resources, etc.
*/
public View(Context context) {}

从注释中可以看到:一个参数的构造器会在代码中直接 new 的时候调用。

2.多个参数的构造器
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
/**
* Constructor that is called when inflating a view from XML. This is called
* when a view is being constructed from an XML file, supplying attributes
* that were specified in the XML file. This version uses a default style of
* 0, so the only attribute values applied are those in the Context's Theme
* and the given AttributeSet.
*
...

//两个参数的构造器会调用 三参数的构造器,指定 defStyleAttr 为 0
public View(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

/**
* Perform inflation from XML and apply a class-specific base style from a
* theme attribute. This constructor of View allows subclasses to use their
* own base style when they are inflating. For example, a Button class's
* constructor would call this version of the super class constructor and
* supply <code>R.attr.buttonStyle</code> for <var>defStyleAttr</var>; this
* allows the theme's button style to modify all of the base view attributes
* (in particular its background) as well as the Button class's attributes.
*
//三个参数的构造器会去调用 四个 参数的构造器,并指定
//defStyleRes 为 0
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}

/**
* Perform inflation from XML and apply a class-specific base style from a
* theme attribute or style resource. This constructor of View allows
* subclasses to use their own base style when they are inflating.
//最后调用的是四个参数的构造器
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {

从两个参数的构造器的注释中可以知道:当在 xml 文件中使用的 View 的时候,调用的是两个参数的构造器。而三个参数/四个参数的构造器就是通过两个参数的构造器去调用的。

这个四个参数的含义分别是:

  • Context ,上下文,一般为 Activity
  • AttributeSet,属性集合,自定义/非自定义的一些属性,包括 style 中的

    1
    style="@style/NewtonCradle"
  • defStyleAttr,默认的 style 的 属性,也就是 Theme 中的 style 的 id.当传入为 0 的时候表示不使用这个属性。通常使用形式如

    1
    2
    3
    4
    5
    <activity
    ...
    android:theme="@style/AppTheme"
    android:label="@string/app_name" >
    ...

这样做的原因是当切换不同的主题时,View 的样式也能随之进行改变.

  • defStyleRes 默认的 res 中的 style 的属性,当传入为 0 的时候表示不使用这个 属性。 这个属性的只能在代码中实现加载,因此不常用。

实际上多个构造器的作用都是为属性赋值,只不过针对不同的使用方式使用场景有所区别,因此也形成一种优先级赋值的关系。
优先级高的会覆盖优先级低的。

  • 1.在布局xml中直接定义
  • 2.在布局xml中通过style定义
  • 3.自定义View所在的 Activity 的 Theme 中指定 style 引用
  • 4.构造函数中 defStyleRes 指定的默认值

三.invalidate

1
2
3
4
5
invalidate();
invalidate(Rect dirty);
invalidate(l,t,r,b);
invalidateDrawable(Drawable drawable);
invalidateOutline();

invalidate 是 View 中用来对 View 进行重绘的方法,它有几个类似的方法用于指定重绘的区域,但是最终都会调用到同一个方法。

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
49
50
51
52
53
54
55
56
57
public void invalidate() {
invalidate(true);
}


public void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}

//invalidate 的几个类似方法最终都会调用到 invalidateInternal 这个方法,不同的是指定重绘的区域
//四个参数就是用于指定需要重绘的区域
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
...

//如果是 不可见或者处于动画中则不进行重绘
if (skipInvalidate()) {
return;
}

//判断一些标志位看是否需要重绘,如果 View 没有发生改变则不需要重绘。
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
|| (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
|| (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
|| (fullInvalidate && isOpaque() != mLastIsOpaque)) {
if (fullInvalidate) {
mLastIsOpaque = isOpaque();
mPrivateFlags &= ~PFLAG_DRAWN;
}

mPrivateFlags |= PFLAG_DIRTY;


//将绘制的区域传递给 父View
// Propagate the damage rectangle to the parent view.
final AttachInfo ai = mAttachInfo;
//ViewGroup 都实现 ViewParent 接口
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
//传递给 ViewGroup
p.invalidateChild(this, damage);
}

...
}
}
}


//判断是否需要可见或者处于动画中
private boolean skipInvalidate() {
return (mViewFlags & VISIBILITY_MASK) != VISIBLE && mCurrentAnimation == null &&
(!(mParent instanceof ViewGroup) ||
!((ViewGroup) mParent).isViewTransitioning(this));
}

下面就看 ViewGroup 中的

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
49
50
51
52
53
54
public final void invalidateChild(View child, final Rect dirty) {
... 先将 parent 指向自己

ViewParent parent = this;
...
//进入一个 do...while 循环

do {
View view = null;
if (parent instanceof View) {
view = (View) parent;
}

if (drawAnimation) {
if (view != null) {
view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
} else if (parent instanceof ViewRootImpl) {
((ViewRootImpl) parent).mIsAnimating = true;
}
}

// If the parent is dirty opaque or not dirty, mark it dirty with the opaque
// flag coming from the child that initiated the invalidate
if (view != null) {
if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&
view.getSolidColor() == 0) {
opaqueFlag = PFLAG_DIRTY;
}
if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {
view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag;
}
}
//首先调用自己的 invalidateChildInParent,
//这个方法返回 父容器的 ViewGroup
//重新赋值给 parent
//通过 循环不断传递给 父 ViewGroup
//这里通过类型 ViewParent 而不是 ViewGroup
parent = parent.invalidateChildInParent(location, dirty);
if (view != null) {
// Account for transform on current parent
Matrix m = view.getMatrix();
if (!m.isIdentity()) {
RectF boundingRect = attachInfo.mTmpTransformRect;
boundingRect.set(dirty);
m.mapRect(boundingRect);
dirty.set((int) Math.floor(boundingRect.left),
(int) Math.floor(boundingRect.top),
(int) Math.ceil(boundingRect.right),
(int) Math.ceil(boundingRect.bottom));
}
}
} while (parent != null);
}
}

1
2
3
4
5
6
7
8
9
10
11
  // 返回父 ViewParent 一般为 ViewGroup
@Deprecated
@Override
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
...
return mParent;
...
}

//ViewGroup 实现 ViewParent 接口
public abstract class ViewGroup extends View implements ViewParent, ViewManager {}

通过这种方式,就将绘制的请求回溯到上一层的 ViewGroup ,这里的使用的是 ViewParent 而不是 ViewGroup ,因为还有一个类也是实现了 ViewParent 接口。

1
2
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {}

看到这里,熟悉 View 的绘制流程的都知道一个 View 的绘制最先就是从这里开始的,ViewRootImpl 实现了 ViewParent 这个接口,那么在上述说到的 do/while 循环中最终就会回溯到这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
// 检查调用这个方法的 线程 和 创建 ViewRootImpl 是否在同一个线程。
checkThread();
....

invalidateRectOnScreen(dirty);

return null;
}

private void invalidateRectOnScreen(Rect dirty) {
final Rect localDirty = mDirty;
...
if (!mWillDrawSoon && (intersected || mIsAnimating)) {
//调用下面这个方法
scheduleTraversals();
}
}

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
 void scheduleTraversals() {
....
通过 Handler 发送一个 Runnable。
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
...
}

final class TraversalRunnable implements Runnable {
@Override
public void run() {
//
doTraversal();
}
}

void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
//最终就是回到 View 的绘制的起使方法
performTraversals();

if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}

在 performTraversals 中会进行一些判断如不需要 measure 和 layout ,然后直接进行 performDraw 方法。

四.postInvalidate

Invalidate 是在主线程调用的用于 重绘 View 的方法,那么如果需要在子线程中重新 View 就需要调用 postInvalidate 方法。

1
2
3
postInvalidate();
postInvalidate(l,r,t,b);
postInvalidateDelayed(mills);

postInvalidate 也有一些类型的方法,最终都会调用 mViewRootImpl 的相关方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void postInvalidate() {
postInvalidateDelayed(0);
}


public void postInvalidate(int left, int top, int right, int bottom) {
postInvalidateDelayed(0, left, top, right, bottom);
}

public void postInvalidateDelayed(long delayMilliseconds) {
// We try only with the AttachInfo because there's no point in invalidating
// if we are not attached to our window
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
}
}

在 ViewRootImpl 中

1
2
3
4
5
6
7
8
9
10
11
12
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
//发送一个消息
Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}

public void dispatchInvalidateRectDelayed(AttachInfo.InvalidateInfo info,
long delayMilliseconds) {
//发送一个消息
final Message msg = mHandler.obtainMessage(MSG_INVALIDATE_RECT, info);
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}

在 ViewRootImpl 中可以看到 postInvalidate 最终就会通过 Hanlder 发送一个消息到消息队列,然后就回到了主线程

1
2
3
4
5
6
7
8
9
10
11
12
13
   final ViewRootHandler mHandler = new ViewRootHandler();

@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INVALIDATE:
((View) msg.obj).invalidate();
break;
case MSG_INVALIDATE_RECT:
final View.AttachInfo.InvalidateInfo info = (View.AttachInfo.InvalidateInfo) msg.obj;
info.target.invalidate(info.left, info.top, info.right, info.bottom);
info.recycle();
break;

这个 mHandler 是在主线程中创建的,因此消息的处理也就回到了主线程,最后又调用了 invalidate 方法。
image.png

五.requestLayout

当一个 View 调用 requestLayout 的时候就需要对 View 树进行重新 测量、布局、绘制这三个流程,。在 View 的源码中,

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
     /*Call this when something has changed which has invalidated the
* layout of this view. This will schedule a layout pass of the view
* tree. This should not be called while the view hierarchy is currently in a layout
* pass ({@link #isInLayout()}. If layout is happening, the request may be honored at the
* end of the current layout pass (and then layout will run again) or after the current
* frame is drawn and the next layout occurs.
*/

//如果当前 View 在请求布局的时候,View 树正在进行布局流程的话,
//该请求会延迟到布局流程完成后或者绘制流程完成且下一次布局发现的时候再执行,这个延迟是通过设置标志实现的。
@CallSuper
public void requestLayout() {
if (mMeasureCache != null) mMeasureCache.clear();

if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
// Only trigger request-during-layout logic if this is the view requesting it,
// not the views in its parent hierarchy
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}
//设置标志 PFLAG_FORCE_LAYOUT 和 PFLAG_INVALIDATED
//表示需要 重新 测量,布局,绘制
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;

if (mParent != null && !mParent.isLayoutRequested()) {
//从这个 mParent 就是 ViewGroup
//最终会调用到 viewRootImpl 的 requestLayout
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}

在 ViewRootImpl 的 requestLayout 的过程中

1
2
3
4
5
6
7
8
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}

这里又回到 scheduleTraversals 方法中,由于设置标志位,因此就会进行测量,布局和绘制。

0%