为第三方依赖中的View添加自己的监听器

前言

在实际开发时,项目中难免会用到一些第三方开源库,比如常见的OkHttp、Picasso等。但是有时候第三方的库提供的对外接口并不能满足我们的实际需求,笔者前段时间就遇到了一个问题:使用Google 的EXOPlayer的时候需要能够获取得到上一个、下一个按钮的点击事件得监听。但是在API中没有找到相关的方法(也有可能时真的有但是我没有找到),所以只好想了一个比较非常规的方法来解决。在这里把这个方案总结出来。

需求描述

在项目中获取到第三方依赖库中View对象的onClick、OnTouch等事件的监听,同时不能干扰第三方依赖库自己原有的监听逻辑。我们默认用户时可以获取到View的实例的。

问题分析

通过需求我们知道,想要获取到View对象的事件监听,最直观的方法就是获取到View实例后,调用相应的方法给它设置监听器:

1
2
3
4
5
6
//设置点击事件的监听
view.setOnClickListener(myOnClickListener);
//设置touch事件的监听
view.setOnTouchListsner(myOnTouckListener);

但是,这种方法会打断第三方库中自己原有的监听逻辑。通过View的源码我们知道setOnClickListener时新的监听器是会覆盖掉原有的监听器的,这就导致了原有的监听器无法收到view的事件通知(OnTouchListsner的原理与OnClick的类似,本文只以OnClick的为例来说明)。假如该View实例自己本身没有监听器的话,那就可以使用这种方法了,鉴于这种清醒较简单,本文将不再考虑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*View 类的setOnClickListener方法,在里面调用了
getListenerInfo()方法
*/
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
//这里会把新的监听器赋值给mOnClickListener,原有的监听器
//将不会收到View的相关事件
getListenerInfo().mOnClickListener = l;
}
/*getListenerInfo 方法,它返回了View的成员mListenerInfo的对象,mListenerInfo是一个ListenerInfo类型,ListenerInfo是View的一个子类。
ListenerInfo 类里面包含了Vie的所有Listener,如OnScrollChangeListener、OnClickListener等,它的成员mOnClickListener 就是View类的onClick监听器
*/
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}

既然无法用最直接的方法来设置监听器,而且view已经有一个监听器了,那么我们为什么不能直接把它拿出来呢?顺着这个思路,很自然得就会想到使用反射来获取到View对象里面所包含得监听器对象。 拿出来监听器之后我们就可以自己给view设置自己得监听器,并在自己得监听器中主动调用原始监听器得回掉方法已达到不打断原来逻辑得目的。

解决方案

通过以上得分析,我们可以知道解决这样得问题大致可以分为以下几个步骤:

  1. 通过view对象获取到mListenerInfo对象
  2. 通过mListenerInfo对象获取到mOnClickListener(或者其他得监听器)
  3. 给目标view设置自己得onClickListener,并在其中主动调用原始监听器的方法。
  4. 在自己的onClickListener中完成自己想要的操作。

以上几个步骤中存在一个问题:我们获取到的View很可能是一个View的子类,而mListenerInfo是View基类的对象,在通过子类获取到基类的成员时有一点复杂,这里提供一种方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Field getDeclaredField(Object object, String name){
Field field = null ;
//递归查找父类的成员是否有满足条件的
for(Class<?> clazz = object.getClass() ; clazz != Object.class ; clazz = clazz.getSuperclass()) {
try {
field = clazz.getDeclaredField(name) ;
return field ;
} catch (Exception e) {
//这里什么都不要做
}
}
return null;
}

解决了这个问题之后就能顺利拿到目标view的监听器对象了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private View.OnClickListener getOnClickListener(View view){
View.OnClickListener clickListener = null;
try {
Field field = getDeclaredField(view, "mListenerInfo");
field.setAccessible(true);
Object object = field.get(view);
//调用上文中提到的方法
Field field1 = object.getClass().getDeclaredField("mOnClickListener");
clickListener = (OnClickListener) field1.get(object);
if (clickListener != null){
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return clickListener;
}

现在,已经可以获取到第三方依赖中view对象的监听器了,我们还需要做到不打断他们原有的业务逻辑,同时添加自己的监听。

1
2
3
4
5
6
7
8
9
10
View.OnClickListener oldListener = getOnClickListener(myView);
View.OnClickListener myListener = new View.OnClickListener(){
@Override
public void onClick(View v){
oldListener.onClick(v);
//做自己的操作
};
}
myView.setOnClickListener(myListener);

总结

以上,就完成了最开始的要求。该方案不限于点击事件,View的其他事件也可以通过该方法进行拦截。

坚持原创技术分享,您的支持将鼓励我继续创作!