一、理解ViewTreeObserver概念
ViewTreeObserver用来注册监听器,在视图树全局发生变化时收到通知。它不能被应用实例化,因为它是由视图提供,通过android.view.View#getViewTreeObserver()来获取。
ViewTree:视图树。在Android中,所有视图由View和View的子类组成。ViewGroup也是view的子类,它是View的容器,它可以装载View和ViewGroup。这样ViewGroup和View以树形结构一层一层的嵌套组合,就形成了视图树。
Observer:观察者。使用了观察者的设计模式,在这里,即ViewTree时被观察者,ViewTreeObserver是观察者,通过ViewTreeObserver注册监听来观察ViewTree的变化,当ViewTree发生变化,就会调用ViewTreeObserver的相关方法来通知其这一改变。我们可以在ViewTreeObserver中add自己的监听器,从而得到ViewTree的某一变化的通知做出自己的逻辑处理。
二、如何获取ViewTreeObserver
ViewTreeObserver是不能被应用程序实例化的,因为它是由视图提供的,通过view.getViewTreeObserver()获取。
//View.java
public ViewTreeObserver getViewTreeObserver() {
if (mAttachInfo != null) {
return mAttachInfo.mTreeObserver;
}
if (mFloatingTreeObserver == null) {
mFloatingTreeObserver = new ViewTreeObserver();
}
return mFloatingTreeObserver;
}
其中,AttachInfo是View的一个内部类。当一个View附着到它的父Window中时,这个View能获取到一组View和父Window之间的信息,就存储在AttachInfo当中。AttachInfo中就有一个ViewTreeObserver对象。当mAttachInfo为空时,返回mFloatingTreeObserver,一个特殊的ViewTreeObserver。
那么,每一个view的mAttachInfo是怎么获取到的呢?AttachInfo是在View第一次attach到Window时,ViewRoot传给自己的子View的,然后沿着视图树,AttachInfo会一直传递到每一个View。
//ViewGroup.java
@Override
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
...
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
final View child = children[i];
child.dispatchAttachedToWindow(info,
combineVisibility(visibility, child.getVisibility()));
}
...
}
//View.java
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
//System.out.println("Attached! " + this);
mAttachInfo = info;
...
}
另外,当新的View加入到ViewGroup中时,也会将AttachInfo传入。
//ViewGroup.java
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
...
AttachInfo ai = mAttachInfo;
if (ai != null && (mGroupFlags & FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW) == 0) {
boolean lastKeepOn = ai.mKeepScreenOn;
ai.mKeepScreenOn = false;
child.dispatchAttachedToWindow(mAttachInfo, (mViewFlags&VISIBILITY_MASK));
if (ai.mKeepScreenOn) {
needGlobalAttributesUpdate(true);
}
ai.mKeepScreenOn = lastKeepOn;
}
...
}
三、ViewTreeObserver的可用性
通过View.getViewTreeObserver()获得的ViewTreeObserver并不能保证在View的整个生命周期中一直是存活的。所以如果我们调用这个方法的时候,要长期引用这个ViewTreeObserver的话,在使用ViewTreeObserver之前就需要通过isAlive()方法去检查它是否是可用的。
//ViewTreeObserver.java
public boolean isAlive() {
return mAlive;
}
其实我们在调用ViewTreeObserver的addOnXxxListener和removeOnXxxListener时,这些方法内部执行的一个操作就是checkIsAlive()去判断ViewTreeObserver是否可用。当ViewTreeObserver不可用时,将抛出IllegalStateException异常,并提示重新通过getViewTreeObserver()获取ViewTreeObserver。
//ViewTreeObserver.java
private void checkIsAlive() {
if (!mAlive) {
throw new IllegalStateException("This ViewTreeObserver is not alive, call "
+ "getViewTreeObserver() again");
}
}
四、走入ViewTreeObserver类内部
前面提到,ViewTreeObserver用来注册监听器,在视图树全局发生变化时收到通知。目前ViewTreeObserver可以监听11种全局事件,对应了11个内部类,以下8个是公开的。
/**
* 当视图树attached/detached到window时,回调的接口类定义
*/
public interface OnWindowAttachListener
/**
* 当window的焦点状态发生变化时,回调的接口类定义
*/
public interface OnWindowFocusChangeListener
/**
* 当视图树的焦点状态发生变化时,回调的接口类定义
*/
public interface OnGlobalFocusChangeListener
/**
* 当视图树的全局布局状态发生变化或者视图树中某个view的可见状态发生变化时,回调的接口类定义
*/
public interface OnGlobalLayoutListener
/**
* 当视图树将要绘制时,回调的接口类定义
*/
public interface OnPreDrawListener
/**
* 当视图树绘制时,回调的接口类定义
*/
public interface OnDrawListener
/**
* 当触摸模式发生变化时,回调的接口类定义
*/
public interface OnTouchModeChangeListener
/**
* 当视图树中某些组件发生滚动时,回调的接口类定义
*/
public interface OnScrollChangedListener
对于以上每一种接口,又对应:
- 1 对象集合:mOnXxxListeners
- 2 增加监听: addOnXxxListener(OnXxxListener listener)
- 3 移出监听: removeOnXxxListener(OnXxxListener victim)
- 4 分发事件: dispatchOnXxx()
五、ViewTreeObserver使用示例
接下来我们具体来看一个使用流程。比如,我们要在onCreate中测量一个控件的宽高。
以下是一种通过OnPreDrawListener来计算图片宽高的实现。通过imageView.getViewTreeObserver()获取ViewTreeObserver,然后增加一个自定义的ViewTreeObserver.OnPreDrawListener,覆写onPreDraw()方法。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final ImageView imageView = (ImageView) findViewById(R.id.imageview);
imageView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
public boolean onPreDraw() {
int height = imageView.getMeasuredHeight();
int width = imageView.getMeasuredWidth();
imageView.getViewTreeObserver().removeOnPreDrawListener(this);
return true;
}
});
}
首先看看addOnPreDrawListener方法。第一步,检测ViewTreeObserver是否可用,不可用的话抛出异常提示再获取一次。可用的话,则将这个listener添加到mOnPreDrawListeners(所有OnPreDrawListener对象的集合)当中。
//ViewTreeObserver.java
public void addOnPreDrawListener(OnPreDrawListener listener) {
checkIsAlive();
if (mOnPreDrawListeners == null) {
mOnPreDrawListeners = new CopyOnWriteArray<OnPreDrawListener>();
}
mOnPreDrawListeners.add(listener);
}
为了防止多次收到通知调用onPreDraw,就需要在合适的时候移除这个监听。同样,也是先检测ViewTreeObserver是否可用,不可用的话抛出异常提示再获取一次。可用的话,则将这个listener从mOnPreDrawListeners删除掉。
//ViewTreeObserver.java
public void removeOnPreDrawListener(OnPreDrawListener victim) {
checkIsAlive();
if (mOnPreDrawListeners == null) {
return;
}
mOnPreDrawListeners.remove(victim);
}
使用方法就此完成。在内部,ViewTreeObserver还需要负责分发事件通知给相应的监听对象,由dispatchOnXxx方法来执行。dispatchOnXxx方法一般由系统来调用。可以看到在dispatchOnPreDraw方法中,会去遍历mOnPreDrawListeners,然后逐个调用OnPreDrawListener.onPreDraw方法。
//ViewTreeObserver.java
public final boolean dispatchOnPreDraw() {
boolean cancelDraw = false;
final CopyOnWriteArray<OnPreDrawListener> listeners = mOnPreDrawListeners;
if (listeners != null && listeners.size() > 0) {
CopyOnWriteArray.Access<OnPreDrawListener> access = listeners.start();
try {
int count = access.size();
for (int i = 0; i < count; i++) {
cancelDraw |= !(access.get(i).onPreDraw());
}
} finally {
listeners.end();
}
}
return cancelDraw;
}
其他几个OnXXXListener使用及原理基本同上。
六、View的绘制过程中dispatchOnXxx方法的调用时机
在实际应用中,虽然知道几个监听事件的大致概念,但怎么使用其实还是很迷惑的。也就是说,什么时候能触发这些事件然后通知ViewTreeObserver呢?即dispatchOnXxx系列方法系统到底是什么时候调用的呢?只有彻底明白了dispatchOnXxx()执行时机,才能更清楚地使用ViewTreeObserver。
我们知道view的真正绘制流程从ViewRootImpl类中的performTraversals()开始。我只截取相关部分代码说明。
//ViewRootImpl.java
// 第一段代码:
final View host = mView; //DecorView根布局
......
final View.AttachInfo attachInfo = mAttachInfo;
......
if (mFirst) { //表示第一次被请求执行测量、布局和绘制
//计算Activity窗口的宽高
......
// 通知视图树已经绑定到它的父窗口上
......
host.dispatchAttachedToWindow(attachInfo, 0);
attachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
......
} else {
......
}
可以看到,如果Activity窗口第一次执行测量、布局和绘制操作,计算完窗口的宽和高之后,就会给mAttachInfo设置一些activity的属性信息,然后通过attachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true)分发通知视图树已经绑定到父窗口。
接下来看第二段代码:
// 第二段代码:
boolean layoutRequested = mLayoutRequested && !mStopped;
if (layoutRequested) {
final Resources res = mView.getContext().getResources();
if (mFirst) {
// mInTouchMode表示View所处的Window是否处于触摸模式
mAttachInfo.mInTouchMode = !mAddedTouchMode;
// 确保这个Window的触摸模式已经被设置
ensureTouchModeLocally(mAddedTouchMode);
} else {
......
}
进入到ensureTouchModeLocally方法内部,可以看到如果触摸模式发生变化,则会mAttachInfo.mTreeObserver.dispatchOnTouchModeChanged通知出发模式变化的通知。
private boolean ensureTouchModeLocally(boolean inTouchMode) {
if (mAttachInfo.mInTouchMode == inTouchMode) return false;
mAttachInfo.mInTouchMode = inTouchMode;
mAttachInfo.mTreeObserver.dispatchOnTouchModeChanged(inTouchMode);
return (inTouchMode) ? enterTouchMode() : leaveTouchMode();
}
接下来看第三段代码。
开始执行measure过程。
//第三段代码
if (!mStopped) { //此窗口的拥有者Activity是否处于停止状态
//此窗口当前获得焦点的控件是否发生变化
boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
(relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
......
// 开始执行测量操作
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
......
if (measureAgain) { //是否需要重新测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
layoutRequested = true;
}
}
} else {
......
}
第四段代码:执行layout过程。
在layout过程执行结束,则会通过attachInfo.mTreeObserver.dispatchOnGlobalLayout()通知全局布局变化事件。
//第四段代码
final boolean didLayout = layoutRequested && !mStopped;
boolean triggerGlobalLayoutListener = didLayout
|| attachInfo.mRecomputeGlobalAttributes;
if (didLayout) {
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
......
}
......
if (triggerGlobalLayoutListener) {
attachInfo.mRecomputeGlobalAttributes = false;
attachInfo.mTreeObserver.dispatchOnGlobalLayout();
}
第五段代码:执行draw过程。
首先,会通过attachInfo.mTreeObserver.dispatchOnPreDraw()通知观察者绘制过程开始了。如果某一个观察者OnPreDrawListener.onPreDraw返回true,则绘制过程会被取消掉或者重新开始调度。
//第五段代码
boolean cancelDraw = attachInfo.mTreeObserver.dispatchOnPreDraw() ||
viewVisibility != View.VISIBLE; //是否取消绘制
if (!cancelDraw && !newSurface) {
if (!skipDraw || mReportNextDraw) {
......
performDraw();
}
} else {
if (viewVisibility == View.VISIBLE) {
// 重新开始
scheduleTraversals();
} ......
}
在performDraw()->draw()执行过程中,又会涉及到两个事件的分发。如果有发生滚动的话,则会通过attachInfo.mTreeObserver.dispatchOnScrollChanged()通知滚动事件;另外,通过attachInfo.mTreeObserver.dispatchOnDraw()通知观察者绘制的执行。
//ViewRootImpl.java
private void draw(boolean fullRedrawNeeded) {
......
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo.mViewScrollChanged) {
attachInfo.mViewScrollChanged = false;
attachInfo.mTreeObserver.dispatchOnScrollChanged();
}
......
attachInfo.mTreeObserver.dispatchOnDraw();
......
}
到这里,绘制过程与ViewTreeObserver.dispatchOnXxx方法的调用关系大致就清晰了。还有几个事件这里就不一一说明啦。
七、总结
关于ViewTreeObserver的源码学习就大致就这样,谈了谈基本概念、如何用、还有底层的一些分发机制等。还有很多细节还需要不断深入了解,本文不对之处欢迎大家多多讨论。