提出问题
在自定义 View
的时候,我们经常要往一个自定义的 ViewGroup
中添加子 View
。但是如何监听子 View
被添加到该 ViewGroup
中呢,或者我们需要监听一个 View
是什么时候添加到父 ViewGroup
中的,相应的回调在哪里。
所以这就是我们今天的问题,有两个点,分别是:
1:如何监听
ViewGroup
添加子View
?
2:对于一个View
,在什么时机可以判断他已经被添加至窗口,可以使用getParent()
方法来获得父ViewGroup
?
分析问题
以下代码基于Android 7.1(API = 25)
上面说了这么多,有一定Android开发基础的童鞋都可以想到这两个问题其实上整个过程都是围绕着 ViewGroup#addView
方法,甚至第一个问题可以这么解决:
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
super.addView(child, index, params);
/**
* 定义回调接口
*/
}
解决方法就是在注释处编写监听器的回调。
其实这种方法绝大多数情况下的确是能解决问题的,不管你是通过 addView
方法来添加一个 View
到 ViewGroup
,还是使用xml文件来编写布局文件,最后都是通过这个方法来添加到ViewGroup
内。但是这种解决方案有两个小问题:
1:
addView
本身并不具有监听器的回调功能,你可能需要自己定义一个接口,然后设置回调函数使其在外部可以调用。
2:实际上并不是所有添加子View
到ViewGroup
中都会调用这个方法,存在另外一种情况,可能并不会调用addView
方法。
对于 ViewGroup
添加子 View
,其实最终调用的是 addViewInner
方法,上面提到的解决方案,覆盖了图中蓝色和绿色的所有路径,我们重写的方法的位置是即使图中青色的位置,但是我们忽略了一个路径,就是图中的淡红色路径。
ViewGroup#addViewInLayout
方法,这个方法虽然不是经常使用,但是的确是客观存在,可以将一个 View
添加至一个 ViewGroup
。
这个方法的注释写着:Adds a view during layout. This is useful if in your onLayout() method, you need to add more views (as does the list view for example). If index is negative, it means put it at the end of the list.(在layout过程中添加 View
,这个在 onLayout
方法中很有用,当你需要添加更多 View
时{比如列表 View
},如果你传入一个负数作为index的参数,代表着该 View
放在 ViewGroup
最后面)。
所以,我们还是继续向里看,看看在 ViewGroup#addViewInLayout
方法中,有没有合适的回调函数。
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
······
addInArray(child, index);
// tell our children
if (preventRequestLayout) {
child.assignParent(this);
} else {
child.mParent = this;
}
if (child.hasFocus()) {
requestChildFocus(child, child.findFocus());
}
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;
}
if (child.isLayoutDirectionInherited()) {
child.resetRtlProperties();
}
dispatchViewAdded(child);
······
}
这个方法定义在 ViewGroup
类中,笔者删除了一些和本文无关的代码。我们从上往下看重点。
addInArray
这个方法,将child加入了ViewGroup
的children
中,也就是说,理论上执行完这句代码,你就能通过ViewGroup#getChildAt
和ViewGroup#getChildCount
方法来获取child和children的数量。
接下来的一小段代码,child的mParent变量被赋值,理论上执行完这行代码,作为child的View
就可以执行View.getParent
方法来获取父布局。
下面的代码我们先跳过一段,直接看AttachInfo
下面的if判断内部的代码,里面调用了 View#dispatchAttachedToWindow
方法,提到Android中的Window机制,不得不提一篇老罗写的神文 Android应用程序窗口(Activity)的窗口对象(Window)的创建过程分析我们进入这个dispatchAttachedToWindow
方法去看。
这个方法定义在View.java中:
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
···
onAttachedToWindow();
ListenerInfo li = mListenerInfo;
final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
li != null ? li.mOnAttachStateChangeListeners : null;
if (listeners != null && listeners.size() > 0) {
// NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
// perform the dispatching. The iterator is a safe guard against listeners that
// could mutate the list by calling the various add/remove methods. This prevents
// the array from being modified while we iterate it.
for (OnAttachStateChangeListener listener : listeners) {
listener.onViewAttachedToWindow(this);
}
}
···
}
dispatchAttachedToWindow
方法里面调用了这样一个回调函数,其中涉及到一个接口。
/**
* Interface definition for a callback to be invoked when this view is attached
* or detached from its window.
*/
public interface OnAttachStateChangeListener {
/**
* Called when the view is attached to a window.
* @param v The view that was attached
*/
public void onViewAttachedToWindow(View v);
/**
* Called when the view is detached from a window.
* @param v The view that was detached
*/
public void onViewDetachedFromWindow(View v);
}
从这个接口的注释可以读出来这个方法将在 View
和 Window
建立联系时调用,继续看这个接口的具体使用部分:
/**
* Add a listener for attach state changes.
*
* This listener will be called whenever this view is attached or detached
* from a window. Remove the listener using
* {@link #removeOnAttachStateChangeListener(OnAttachStateChangeListener)}.
*
* @param listener Listener to attach
* @see #removeOnAttachStateChangeListener(OnAttachStateChangeListener)
*/
public void addOnAttachStateChangeListener(OnAttachStateChangeListener listener) {
ListenerInfo li = getListenerInfo();
if (li.mOnAttachStateChangeListeners == null) {
li.mOnAttachStateChangeListeners
= new CopyOnWriteArrayList<OnAttachStateChangeListener>();
}
li.mOnAttachStateChangeListeners.add(listener);
}
所以,我们率先解决了第二个问题,找到了 View
在什么时候有 mParent
这个问题的答案。
我们重新回到addViewInner
方法,我们继续往下走,发现又调用了ViewGroup#dispatchViewAdded
这个方法。我们来看看这个方法相关代码。
/**
* Register a callback to be invoked when a child is added to or removed
* from this view.
*
* @param listener the callback to invoke on hierarchy change
*/
public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
mOnHierarchyChangeListener = listener;
}
void dispatchViewAdded(View child) {
onViewAdded(child);
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewAdded(this, child);
}
}
/**
* Called when a new child is added to this ViewGroup. Overrides should always
* call super.onViewAdded.
*
* @param child the added child view
*/
public void onViewAdded(View child) {
}
setOnHierarchyChangeListener
这个方法的注释写的很清楚了,将会在有 View
添加和删除的时候调用。 dispatchViewAdded
方法内部则是调用了这个接口的回调函数,顺便一提,这个方法还是买一送一的, onViewAdded
方法也具有相同的功能,只是不具有回调的功能。
解决问题
所以到这里,我们的两个问题就算是得到了解决,你可以在一切可以获得View对象的地方去调用下面这个两个方法,因为其都是公开方法而不是保护方法,顺带一提,addOnAttachStateChangeListener
方法可以被多个地方调用,因为内部维护了一个list去调用。
问题1:
问:如何监听
ViewGroup
添加子View
?
答:你可以给你的viewgroup对象添加下面这个监听器,可以监听每一个child添加和删除的过程。
parent.setOnHierarchyChangeListener(new OnHierarchyChangeListener() {
@Override
public void onChildViewAdded(View parent, View child) {
Log.v("onChildViewAdded",child.toString());
}
@Override
public void onChildViewRemoved(View parent, View child) {
Log.v("onChildViewRemoved",child.toString());
}
});
问题2:
问:对于一个
View
,在什么时机可以判断他已经被添加至窗口,可以使用getParent()
方法来获得父ViewGroup
?
你可以给你的View添加下面这个监听器,这个监听器可以声明多次,在每一处你都可以收到这个View添加窗口和被窗口移除的回调。
view.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
Log.v("onViewAttachedToWindow",v.toString());
}
@Override
public void onViewDetachedFromWindow(View v) {
Log.v("onViewDetachedFromWindow",v.toString());
}
});