Android 中多次设置 OnClickListener 只执行一次吗?

Posted 吴豪杰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 中多次设置 OnClickListener 只执行一次吗?相关的知识,希望对你有一定的参考价值。

问题

对于 android 初学者,可能对这个问题会比较疑惑: 对于一个 View,比如 Button,如果为其设置多次点击监听 OnClickListener 回调方法,同时还在布局中设置了 onClick 属性,并且也实现了点击回调方法,那么问题来了,哪些回调方法会执行呢?又是以怎样的顺序执行呢?请跟随脚步和我一探究竟…

实验现象

我们先来做个实验,观察一下实验现象。
首先在布局文件中声明一个 Button,并为其设置好点击属性:

<Button
    android:id="@+id/btn"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:onClick="click()"
    android:text="BUTTON"/>

嗯,对,然后再在 Activity 中实现方法:

void click(View v) 
    Log.i(TAG, "click: in layout file");

这样第一组测试样例放置好了,第二组和第三组很容易,先后在 onCreate() 中设置两次监听,都记得打上 Log 日志:

mButton.setOnClickListener(new View.OnClickListener() 
    @Override
    public void onClick(View v) 
        Log.i(TAG, "click: in onCreate() first");
    
);
mButton.setOnClickListener(new View.OnClickListener() 
    @Override
    public void onClick(View v) 
        Log.i(TAG, "click: in onCreate() second");
    
);

然后,见证奇迹了,运行观察实验结果:

I/MainActivity: click: in onCreate() second

你没有看错,只有一条结果,而且是第二次的结果,说明优先级 java 设置监听的优先级大于布局文件,而且最后一次设置的监听会覆盖前一次设置的监听。

结论
点击监听优先级: 之后设置的优先级大于之前设置的优先级,代码中设置的优先级大于布局中设置的优先级

寻找答案

结果很出乎意料,也许猜的都会执行,也许你猜的布局先执行,却没想到只有最后一次设置的执行,这其中的神秘之处究竟何在呢?让我们 Read The Fuck Source Code.

setOnClickListener

先从 setOnClickListener() 入手:

public void setOnClickListener(@Nullable OnClickListener l) 
    if (!isClickable()) 
        setClickable(true);
    
    getListenerInfo().mOnClickListener = l;

先是将该 View 设置为可点击状态,所以即便一个 View 是不可点击的,你为其设置了监听,也会将其恢复成可点击状态。再是将 getListenerInfo() 返回的对象中的成员 mOnClickListener 直接复制为参数 l,还不明白?再看:

ListenerInfo getListenerInfo() 
    if (mListenerInfo != null) 
        return mListenerInfo;
    
    // 如果为空,重新创建用于保存所有监听器的容器类
    mListenerInfo = new ListenerInfo();
    return mListenerInfo;


// 容器类定义
static class ListenerInfo 
    ...
    // 是数组 事件发生回调所有监听器
    private CopyOnWriteArrayList<OnAttachStateChangeListener> mOnAttachStateChangeListeners;

    // 不是数组 直接对其进行赋值
    public OnClickListener mOnClickListener;

    protected OnLongClickListener mOnLongClickListener;
    ...

哈,是不是恍然大悟,所以每一次设置监听都是对这个 mOnClickListener 成员进行替换赋值,所以说最后一次设置监听才是有效的。

布局文件

Java 中的监听确实明白了,那么布局文件中又是怎么回事呢?
通过 setOnClickListener() 可以对 mOnClickListener 进行修改,那么我们找一下这个函数在哪里有调用,果然找到了,
View 的构造函数中,有这样两行代码:

// 获取布局 android:onClick="" 属性值
final String handlerName = a.getString(attr);
if (handlerName != null) 
    setOnClickListener(new DeclaredOnClickListener(this, handlerName));

也就是说,View 默认就设置了一个叫做 DeclaredOnClickListener 的监听器:

private static class DeclaredOnClickListener implements OnClickListener 

    ...
    @Override
    public void onClick(@NonNull View v) 
        if (mMethod == null) 
            mMethod = resolveMethod(mHostView.getContext(), mMethodName);
        
        // 调用方法,并携带参数 v
        mMethod.invoke(mHostView.getContext(), v);
        ...
    

默认的实现是通过 resolveMethod() 获取到方法,并调用之:

private Method resolveMethod(@Nullable Context context, @NonNull String name) 
    ...
    if (!context.isRestricted()) 
        // 使用参数 name 反射取得方法
        return context.getClass().getMethod(mMethodName, View.class);
    
    ...

是不是很眼熟了,这不就是反射获取到方法吗?!一切都明白了吧,在 View 创建之初就设置了一个默认监听, 默认监听是调用的所在 Context 中的符合布局中定义的方法签名的方法。

结论

所以,总的来说是这样的,View 在构造之初就默认设置好了一个监听器,View 的构造也是在 onCreate() 方法执行前完成的,对于每一次设置监听都会覆盖上一次的监听,所以最后一次设置的监听才会是有效的。

以上是关于Android 中多次设置 OnClickListener 只执行一次吗?的主要内容,如果未能解决你的问题,请参考以下文章

Android 中多次设置 OnClickListener 只执行一次吗?

多次包含时在布局内设置Android RemoteViews的文本

归纳总结Android的点击事件

android中,如何让布局文件中定义的一个Layout接收点击事件,并为它添加Listener。或者是类似的功能实现

Android DatePicker Fragment 返回一个月前的日期

多次点击事件