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

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

setEnabled(false)

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

setClickable(false)

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

setOnTouchListener{ return true}

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

直接重写onTouchEvent 不要super相关逻辑
override fun onTouchEvent(event: MotionEvent?): Boolean {
    return true
}
直接重写 dispatchTouchEvent 不要super 相关逻辑
override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
    return true
}

dispatchTouchEvent 相关

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

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

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

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

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的代码:

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

onFilterTouchEventForSecurity:

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() 源码中的部分代码:

setFlags(enabled ? ENABLED : DISABLED, ENABLED_MASK);

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

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 源码:

public void setClickable(boolean clickable) {
    setFlags(clickable ? CLICKABLE : 0, CLICKABLE);
}

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

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():
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) 无效

可以看到下面的代码:

isClickable=false
setOnClickListener {
    LogUtils.e("setOnClickListener")
}

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

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,这种思路还是直接放弃了源码的实现,所以函数也没有地方调用。

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

Android 学习笔录

Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap
Android 性能优化篇:https://qr18.cn/FVlo89
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo

你可能感兴趣的:(移动开发,Android,面试题,android,Android,移动开发,APP框架,面试)