面试题:设置view点击事件不回调的几种方式和原理

如何设置view 点击事件不回调,如何实现?有什么区别?

setEnabled(false)

这个方案用于设置view是否可以响应用户的其他交互事件如触摸,轨迹球等。

setClickable(false)

这个方法用于设置view是否可以响应用户的点击事件。

setOnTouchListener{ return true}

设置监听,并且表示消费事件。

直接重写onTouchEvent 不要super相关逻辑

kotlin 复制代码
override fun onTouchEvent(event: MotionEvent?): Boolean {
    return true
}

直接重写 dispatchTouchEvent 不要super 相关逻辑

kotlin 复制代码
override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
    return true
}

dispatchTouchEvent 相关

事件的责任链模式中,在view层只有两个:

  • onTouchEvent,返回true 表示消费事件
  • dispatchTouchEvent false 表示不分发,自己消费

但是view 层对这两个函数有默认实现。所以我们自定义view的时候,很少全部都放弃super 相关逻辑,这很毒瘤。而且dispatchTouchEvent 作为事件的分发,这个一般不会重写。最多是处理onTouchEvent。

但是setOnTouchListener 的分发则是在dispatchTouchEvent 函数中。在dispatchTouchEvent这里:

ini 复制代码
if (li != null && li.mOnTouchListener != null
        && (mViewFlags & ENABLED_MASK) == ENABLED
        && li.mOnTouchListener.onTouch(this, event)) {
    result = true;
}
​
if (!result && onTouchEvent(event)) {
    result = true;
}

当我们onTouch 返回了true,则导致下面if 中前面的条件 !result=false,那么onTouchEvent函数就没有调用了,这也是setOnTouchListener 优先级高于 的原因,所以我们这里解决了为什么 setOnTouchListener{ return true}直接重写 dispatchTouchEvent 不要super 相关逻辑点击事件不回调的问题。

在来看一个问题 setEnabled(false) 是可以管控到触摸事件的,我们再来dispatchTouchEvent的代码:

csharp 复制代码
 if (onFilterTouchEventForSecurity(event)) {
    // 上面分发代码在这个里面。
 }

onFilterTouchEventForSecurity:

csharp 复制代码
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
    //noinspection RedundantIfStatement
    if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
            && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
        // Window is obscured, drop this touch.
        return false;
    }
    return true;
}

结合 setEnabled() 源码中的部分代码:

scss 复制代码
setFlags(enabled ? ENABLED : DISABLED, ENABLED_MASK);

可以看到,对于mViewFlags 赋值成了DISABLED,就变成了:

ini 复制代码
static final int FILTER_TOUCHES_WHEN_OBSCURED = 0x00000400;
static final int DISABLED = 0x00000020;
boolean result= (DISABLED&FILTER_TOUCHES_WHEN_OBSCURED)!=0;

导致onFilterTouchEventForSecurity 直接返回false,所以后续的 mOnTouchListener.onTouchonTouchEvent(event) 都没有被执行了。

onTouchEvent 相关

通过上面的知识点,我们就只剩下setClickable 没有开始找为什么了,如果其他的都正确的话,那么我们事件就会传递到onTouchEvent 中。

我们先来看serClickable 源码:

typescript 复制代码
public void setClickable(boolean clickable) {
    setFlags(clickable ? CLICKABLE : 0, CLICKABLE);
}

很单纯,设置了一个Flag =CLICKABLE。在OnTouchEvent 中:

ini 复制代码
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
        || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
        || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

clickable=false ,就导致if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) 这个循环根本就没有进去。所以说setClickable(false) 最终影响到了 判断的执行。

我们知道点击事件回调是当action=MotionEvent.ACTION_UP的时候触发:

  • performClickInternal();
  • performClick():
ini 复制代码
public boolean performClick() {
    notifyAutofillManagerOnClick();
​
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
​
    notifyEnterOrExitForAutoFillIfNeeded(true);
​
    return result;
}

可以看到。performClick实现内部调用了li.mOnClickListener.onClick(this);而mOnClickListener就是我们设置的点击事件。通过这个逻辑,那么 直接重写onTouchEvent 不要super相关逻辑 也可以实现点击事件不回调了。

setClickable(false) 无效

可以看到下面的代码:

ini 复制代码
isClickable=false
setOnClickListener {
    LogUtils.e("setOnClickListener")
}

我们先设置了clickable,又设置了点击事件。但是点击事件可以响应,为什么呢?我们来看下设置点击事件的源码就知道了:

less 复制代码
public void setOnClickListener(@Nullable OnClickListener l) {
    if (!isClickable()) {
        setClickable(true);
    }
    getListenerInfo().mOnClickListener = l;
}

非常单纯的代码,如果是不可点击,那就设置为可以点击。所以 setClickable(false)得写到设置点击事件之后。

总结

其实,这个逻辑还是蛮简单的,主要是要点一下代码。最终汇总下:

  • view 的dispatchTouchEvent 有默认实现,当重写后,放弃super,那么直接影响了点击事件和触摸事件等事件的分发,滚动也被影响了。所以点击事件回调就无法触发,因为没有代码调用到点击事件。
  • setEnabled 将flag 修改成了DISABLED,导致onFilterTouchEventForSecurity返回了false,所以触摸事件回调和onTouchEvent 事件都没有调用到。而点击事件回调在onTouchEvent 里面。
  • setOnTouchListener{ return true} 会导致onTouchEvent 不会被调用,是这么屏蔽的点击事件回调。
  • setClickable(false) 也是更改的flag=CLICKABLE,会导致onTouchEvent 中的clickable 等于false,所以事件还没有分发就结束了。
  • 重写onTouchEvent,不要super,这种思路还是直接放弃了源码的实现,所以函数也没有地方调用。

事件分发和绘制原理,还是得懂一下,毕竟现在各个系统打架,懂了,跨平台方案可能学习得快一点吧。

相关推荐
后端码匠2 小时前
MySQL 8.0安装(压缩包方式)
android·mysql·adb
梓仁沐白4 小时前
Android清单文件
android
董可伦6 小时前
Dinky 安装部署并配置提交 Flink Yarn 任务
android·adb·flink
每次的天空6 小时前
Android学习总结之Glide自定义三级缓存(面试篇)
android·学习·glide
恋猫de小郭7 小时前
如何查看项目是否支持最新 Android 16K Page Size 一文汇总
android·开发语言·javascript·kotlin
flying robot8 小时前
小结:Android系统架构
android·系统架构
xiaogai_gai8 小时前
有效的聚水潭数据集成到MySQL案例
android·数据库·mysql
鹅鹅鹅呢9 小时前
mysql 登录报错:ERROR 1045(28000):Access denied for user ‘root‘@‘localhost‘ (using password Yes)
android·数据库·mysql
在人间负债^9 小时前
假装自己是个小白 ---- 重新认识MySQL
android·数据库·mysql
Unity官方开发者社区9 小时前
Android App View——团结引擎车机版实现安卓应用原生嵌入 3D 开发场景
android·3d·团结引擎1.5·团结引擎车机版