全局点击事件监听拦截

2,137 阅读3分钟

起因

最近发现一个快速点击的问题,纵使有RxView.clicks。或者其他的方法,但是每次要在setOnClickListener里处理,而且老是忘加。得有一个全局的懒人神器来解决。

思路

首先肯定是放在BaseActivity里的,用反射去获取setOnClickListener的事件,并且(动态)代理这个接口。
自定义注解+反射?那还是得每个都添注解,不符合要求。
1.对DecorView的视图树遍历,这样就能得到,页面所有的View。
2.根据View获取到View.OnClickListener
3.利用反射 将View的mOnClickListener,设置成我们自己的代理mOnClickListener(做一层拦截)Field.set()

实现

  1. 观察setOnClickListener

其实就是将new的OnClickListener赋值给了getListenerInfo()的mOnClickListener

image.png image.png

  1. 获取View,可以通过遍历根布局,获取当前页的所有View,也就是遍历DecorView

image.png

RecyclerView 的回收复用机制,会清空item,导致没法这样操作,要过滤掉 image.png

  1. 为OnClickListener设置代理

image.png

想要mOnClickListener得取到ListenerInfo --> 也就是getListenerInfo()
mOnClickListener是ListenerInfo的一个成员变量。
ListenerInfo是View的一个内部类,也就是getListenerInfo()。 image.png

所以

image.png

这时候已经获取到了View.OnClickListener,得为他设置代理,然后中间插入我们的代码。

image.png

image.png

补充:View设置过了,就为他设置一个int的tag,标记,防止重复的设置
view.setTag(mPrivateTagKey, recycledContainerDeep);

补充更新

在使用的过程中,发现有一些View不需要防快速点击,得过滤掉这一部分View

解决:为View设置tag,用来区分这些View

image.png

image.png

xml写法:

image.png

java代码写法:

image.png

踩坑回来

1.当跨模块使用的时候,tag在不同的资源文件里id的名字取一个就行,名字一样取出来的id也一样

image.png

2.id名字一样的View,ViewId也一样。这样在同一activity,都叫iv_delete的两个View。会被认为是一个View,得自行处理(加上tag、手动调用等)

image.png

完整代码

public class ViewHook {

    private static final String TAG = "ViewHook";
    IProxyClickListener mInnerClickProxy;
    Field sHookField;
    Method sHookMethod;

    //是否已经设置了代理
    final int mPrivateTagKey = R.id.view_tag_proxy_click;

    public void initHookClick() {
        if (sHookMethod == null) {
            try {
                Class viewClass = Class.forName("android.view.View");
                if (viewClass != null) {
                    sHookMethod = viewClass.getDeclaredMethod("getListenerInfo");
                    if (sHookMethod != null) {
                        sHookMethod.setAccessible(true);
                    }
                }
            } catch (Exception e) {
                reportError(e, "init");
            }
        }
        if (sHookField == null) {
            try {
                Class listenerInfoClass = Class.forName("android.view.View$ListenerInfo");
                if (listenerInfoClass != null) {
                    sHookField = listenerInfoClass.getDeclaredField("mOnClickListener");
                    if (sHookField != null) {
                        sHookField.setAccessible(true);
                    }
                }
            } catch (Exception e) {
                reportError(e, "init");
            }
        }
    }

    private void hookClickListener(View view, int recycledContainerDeep, boolean forceHook) {
        boolean needHook = forceHook;
        if (!needHook) {
            needHook = view.isClickable();
            if (needHook && recycledContainerDeep == 0) {
                needHook = view.getTag(mPrivateTagKey) == null;
            }
        }
        if (needHook) {
            try {
                Object getListenerInfo = sHookMethod.invoke(view);
                View.OnClickListener baseClickListener = getListenerInfo == null ?
                        null : (View.OnClickListener) sHookField.get(getListenerInfo);//获取已设置过的监听器
                if ((baseClickListener != null && !(baseClickListener instanceof IProxyClickListener.WrapClickListener))) {
                    sHookField.set(getListenerInfo, new IProxyClickListener.WrapClickListener(baseClickListener, mInnerClickProxy));
                    view.setTag(mPrivateTagKey, recycledContainerDeep);
                }
            } catch (Exception e) {
                reportError(e, "hook");
            }
        }
    }

    public void hookViews(View view, int recycledContainerDeep) {
        if (view.getVisibility() == View.VISIBLE && view.getTag(R.id.view_tag_not_proxy_click) == null) {
            boolean forceHook = recycledContainerDeep == 1;
            if (view instanceof ViewGroup) {
                boolean existAncestorRecycle = recycledContainerDeep > 0;
                ViewGroup p = (ViewGroup) view;
                if (!(p instanceof AbsListView || p instanceof RecyclerView) || existAncestorRecycle) {
                    hookClickListener(view, recycledContainerDeep, forceHook);
                    if (existAncestorRecycle) {
                        recycledContainerDeep++;
                    }
                } else {
                    recycledContainerDeep = 1;
                }
                int childCount = p.getChildCount();
                for (int i = 0; i < childCount; i++) {
                    View child = p.getChildAt(i);
                    hookViews(child, recycledContainerDeep);
                }
            } else {
                hookClickListener(view, recycledContainerDeep, forceHook);
            }
        }
    }

    private void reportError(Exception e, String init) {
        Log.e(TAG, init);
    }

    public void onDestroy() {
        sHookField = null;
        sHookMethod = null;
        mInnerClickProxy = null;
    }
}
public interface IProxyClickListener {

    boolean onProxyClick(WrapClickListener wrap, View v);

    class WrapClickListener implements View.OnClickListener {
        public static int oldId = -1;
        public static long lastClickTime;
        IProxyClickListener mProxyListener;
        View.OnClickListener mBaseListener;

        public WrapClickListener(View.OnClickListener l, IProxyClickListener proxyListener) {
            mBaseListener = l;
            mProxyListener = proxyListener;
        }

        @Override
        public void onClick(View v) {
            boolean handled = mProxyListener == null ? false : mProxyListener.onProxyClick(WrapClickListener.this, v);
            if (!handled && mBaseListener != null) {
                if (avoidDoubleClick(v)){
                    mBaseListener.onClick(v);
                }
            }
        }

        /**
         * 快速点击拦截
         */
        public boolean avoidDoubleClick(View v) {
            int viewId = v.getId();
            if (oldId == -1) {
                lastClickTime = SystemClock.elapsedRealtime();
                oldId = viewId;
            } else if (viewId == oldId) {
                long time = SystemClock.elapsedRealtime();
                if (time - lastClickTime < 1000) {
                    return false;
                }
                lastClickTime = SystemClock.elapsedRealtime();
            } else {
                lastClickTime = SystemClock.elapsedRealtime();
                oldId = viewId;
            }
            return true;
        }
    }
}
public abstract class BaseActivity extends AppCompatActivity {

    ViewHook viewHook;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(getLayoutId());
        init();
        viewHook = new ViewHook();
        viewHook.initHookClick();
        getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                viewHook.hookViews(getWindow().getDecorView(), 0);
            }
        });
    }

    abstract public int getLayoutId();

    abstract public void init();

    @Override
    protected void onDestroy() {
        super.onDestroy();
        viewHook.onDestroy();
        viewHook = null;
    }
}

参考文章: www.jianshu.com/p/1c672083f…