Android 获取、移除 View 的 OnClickList

2018-09-02  本文已影响0人  _发强

之前在代码中设置的通过 View.isClickable 去控制 View 的重复点击,昨天突然发现即使控制了,仍然能够再次触发点击事件,让我很是懵逼。

后来翻阅一系列的资料之后,发现了 View.setOnClickListener 源码中的这段代码:

    /**
     * Register a callback to be invoked when this view is clicked. If this view is not
     * clickable, it becomes clickable.
     *
     * @param l The callback that will run
     *
     * @see #setClickable(boolean)
     */
    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

我不知道这里是在哪个版本改的,我只知道,以后不能通过 isClickable 属性去随性的控制点击事件了。 😤

因为我代码里很多都是通过 isClickable 属性控制,并且是统一控制,不可能去一个个的修改。所以,只能针对于这个问题去解决了。

通过 View 的源码,我们可以看到 它的 ClickListener 是传递给了 getListenerInfo() 返回值的 mOnClickListener 去处理。我们来看看这个方法返回的类型(在 View 源码的 5989 行)。

    ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }

通过名字我们能猜出个大概,就是一些 监听事件的信息。来看一下对象内容(View.java 4208行):

    static class ListenerInfo {
        /**
         * Listener used to dispatch focus change events.
         * This field should be made private, so it is hidden from the SDK.
         * {@hide}
         */
        protected OnFocusChangeListener mOnFocusChangeListener;

        /**
         * Listeners for layout change events.
         */
        private ArrayList<OnLayoutChangeListener> mOnLayoutChangeListeners;

        protected OnScrollChangeListener mOnScrollChangeListener;

        /**
         * Listeners for attach events.
         */
        private CopyOnWriteArrayList<OnAttachStateChangeListener> mOnAttachStateChangeListeners;

        /**
         * Listener used to dispatch click events.
         * This field should be made private, so it is hidden from the SDK.
         * {@hide}
         */
        public OnClickListener mOnClickListener;

        /**
         * Listener used to dispatch long click events.
         * This field should be made private, so it is hidden from the SDK.
         * {@hide}
         */
        protected OnLongClickListener mOnLongClickListener;

        /**
         * Listener used to dispatch context click events. This field should be made private, so it
         * is hidden from the SDK.
         * {@hide}
         */
        protected OnContextClickListener mOnContextClickListener;

        /**
         * Listener used to build the context menu.
         * This field should be made private, so it is hidden from the SDK.
         * {@hide}
         */
        protected OnCreateContextMenuListener mOnCreateContextMenuListener;

        private OnKeyListener mOnKeyListener;

        private OnTouchListener mOnTouchListener;

        private OnHoverListener mOnHoverListener;

        private OnGenericMotionListener mOnGenericMotionListener;

        private OnDragListener mOnDragListener;

        private OnSystemUiVisibilityChangeListener mOnSystemUiVisibilityChangeListener;

        OnApplyWindowInsetsListener mOnApplyWindowInsetsListener;

        OnCapturedPointerListener mOnCapturedPointerListener;
    }

根据刚刚 OnClickListener 事件处理的方式,再看到这个对象中的这么多参数,我们基本可以猜到 View 中的大多数 Listener 都交由到这里来存储控制了。再加上 ListenerInfo 是一个非公开的内部类,我们这里想要获取到 View 的 onClickListener( 或者其他) 事件,只能通过反射去操作了。

另外,因为 ListenerInfo 是 View 类中的对象,所以我们在获取该对象时,如果你的 操作类型 不是 View 本身,而是其子类(TextView、ImageView等等)的话,那么就需要获取父类中的属性对象或方法。

这里是获取当前对象 中的 某一个方法,会进行向上查找:

    /**
     * https://blog.csdn.net/zmx729618/article/details/51320688
     *
     *  获取当前类型的 某个方法
     */
    fun getDeclaredMethod(obj: Any, methodName: String, vararg parameterTypes: Class<*>): Method? {
        var method: Method? = null
        var clazz: Class<*> = obj.javaClass
        while (clazz != Any::class.java) {
            try {
                method = clazz.getDeclaredMethod(methodName, *parameterTypes)
//                method?.isAccessible = true
                return method
            } catch (e: Exception) {
                //这里甚么都不要做!并且这里的异常必须这样写,不能抛出去。
                //如果这里的异常打印或者往外抛,则就不会执行clazz = clazz.getSuperclass(),最后就不会进入到父类中了
            }
            clazz = clazz.superclass
        }
        return null
    }

通过上面方法,我们可以获取到 ListenerInfo 对象:

    fun getListenerInfo(view: View): Any? {
        val method = getDeclaredMethod(view, "getListenerInfo")
        method?.isAccessible = true
        val info = method?.invoke(view)
        return info
    }

接下来就是获取其对应的属性参数了,(我这里是调试的 OnClickListener ,可以根据自己需要获取相关属性)

    fun getOnClickListener(view: View): View.OnClickListener? {
        val info = getListenerInfo(view)
        info?.let {
            val m = getFieldValue(it, "mOnClickListener") as View.OnClickListener?
            return m
        }
        return null
    }

我这边的需求是暂时屏蔽掉 OnClickListener , 那么不能通过 isClickable 控制了,我就只能先把它设置为 null 了。

    fun removeOnClickListener(view: View) {
        val info = getListenerInfo(view)
        info?.let {
            setFieldValue(it, "mOnClickListener", null)
        }
    }

在实际应用场景中,操作流程大概是当 某个按钮点击之后,进行数据提交,在提交过程中,不允许该按钮再次触发事件。

下面是演示代码:

        vc_tv_click.setOnClickListener {
            Log.i("TAG", "ViewClick")
        }

        val click = getOnClickListener(vc_tv_click)

        vc_btn_cancel.setOnClickListener {
            removeOnClickListener(vc_tv_click)
        }
        vc_btn_reset.setOnClickListener {
            vc_tv_click.setOnClickListener(click)
        }

到此,基本该操作就完成了。
示例代码

在本篇博客中,我们既能获取到 OnClickListener (用于工具方法中,保存临时点击事件, 事件操作完之后,用来恢复点击事件) ,又能暂时移除 OnClickListener。
这种方式,我们也可以用来控制 Android 中重复点击的问题。
个人感觉这个应该比那些控制间隔时间 1s 、2s 内不触发点击事件要优雅许多。并且是从根源上处理 触发事件。

上一篇下一篇

猜你喜欢

热点阅读