上一个版本的焦点自动释放工具有一个明显的缺陷:那就是在初始化时为每一个布局都设置了一个新的OnTouchListener,这样会覆盖掉用户或其他框架设置的OnTouchListener,为了不影响原先的触摸逻辑,需要对初始化方法进行优化。
如何保障原先的触摸逻辑不受影响呢?因为View.OnTouchListener是一个接口,可以使用动态代理来拓展原先触摸监听器的逻辑(类似于AOP,面向切片编程),首先需要获取到控件的触摸监听器。View的所有监听器是保存在mListenerInfo里的,mListenerInfo保存了几乎所有常用的监听器(如点击监听器(OnClickListener)、布局变化监听器(OnLayoutChangeListener)、聚焦变化监听器(OnFocusChangeListener)),同样也包括触摸监听器。因为View类没有暴露OnTouchListener这个属性,所以需要用反射获取。
同样ListenerInfo的触摸监听器也是私有的(private),并且没有提供get方法,所以需要第二层反射。下面这个方法(getOnTouchListener)用于获取View的触摸监听器。
/**
* get onTouchListener from view
* 通过反射获取View的触摸监听器
* @param childAt view
* @return view's onTouchListener
*/
private View.OnTouchListener getOnTouchListener(View childAt) {
View.OnTouchListener onTouchListener = null;
try { //通过反射获取控件的mListenerInfo
Field _fListenerInfo = View.class.getDeclaredField("mListenerInfo");
_fListenerInfo.setAccessible(true);
Object listenerInfo = _fListenerInfo.get(childAt); //mListenerInfo
if(DBG) Log.e(TAG,"_fListenerInfo->"+_fListenerInfo+",listenerInfo->"+listenerInfo);
if(listenerInfo!=null){ //listenerInfo为空表示此控件没有设置任何监听器
Field _fOnTouchListener = listenerInfo.getClass().getDeclaredField("mOnTouchListener");
_fOnTouchListener.setAccessible(true);
Object obj = _fOnTouchListener.get(listenerInfo);
if(DBG) Log.e(TAG,"onTouchListener->"+obj);
if(obj instanceof View.OnTouchListener){ //获取触摸监听器
onTouchListener = (View.OnTouchListener) obj;
}
}
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
} finally {
if(onTouchListener == null) onTouchListener = (v, event) -> false;
}
return onTouchListener;
}
为了使用动态代理拓展View原先的触摸逻辑,需要创建一个InvocationHandler。
class ExternalTouchHandler implements InvocationHandler {
View.OnTouchListener onTouchListener;
ExternalTouchHandler(View.OnTouchListener onTouchListener){
this.onTouchListener = onTouchListener;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//因为OnTouchListener接口只有一个方法,不需要考虑方法区分。
Boolean bool = (Boolean) method.invoke(onTouchListener,args);
detectClick(); //先执行原先的触摸逻辑,然后检测焦点。
return bool;
}
}
然后是创建触摸代理方法。
private View.OnTouchListener generateTouchProxy(View.OnTouchListener onTouchListener){
return (View.OnTouchListener) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{View.OnTouchListener.class},new ExternalTouchHandler(onTouchListener));
}
最后优化初始化方法。
/**
* 初始化点击探测器
* @param root 拦截点击事件的根布局
*/
@SuppressLint("ClickableViewAccessibility")
public void init(ViewGroup root){
if(imm==null){
imm = (InputMethodManager) root.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
}
for(int i=0;i<root.getChildCount();i++){
if(root.getChildAt(i) instanceof ViewGroup){ root.getChildAt(i).setOnTouchListener(generateTouchProxy(getOnTouchListener(root.getChildAt(i))));
init((ViewGroup) root.getChildAt(i)); //递归初始化内层布局
}
}
}
完整代码: