进阶必备-Android Click事件是怎么触发的?
作者丨Bob
来源丨IT烂笔头
一、背景
由于有同学问到onClick和touch事件的关系,这里就从源码的角度分析下onClick和onLongClick与onTouchEvent事件是怎么关联的。本文将通过View.java、TextView.java、Button.java的源码作为例子分析。
二、源码解读
首先我们知道View、TextView、Button三者的关系,即:Button继承自与TextView,TextView继承自View。
在默认我们不做任何特殊设置时,三者能响应click事件的只有Button。这是什么原因呢?
首先我们看到View的源码中onTouchEvent方法中:
final int viewFlags = mViewFlags;
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
在onTouchEvent方法的一开始,通过viewFlags去判断当前View的CLICKABLE或者LONG_CLICKABLE位是否可以点击或者长按。默认情况下,在View初始化的时候会从xml读clickable属性或者longclickable属性。所以如果不在xml中设置,View和TextView是不会响应点击事件的,那么我们翻开Button的源码看下为什么唯独它是响应的呢?
public Button(Context context, AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.buttonStyle);
}
可以看到在Button初始化的时候,设置了Button的style,我们翻开Styles.xml中找到Button:
<style name="Widget.Button">
<item name="background">@drawable/btn_default</item>
<item name="focusable">true</item>
<item name="clickable">true</item>
<item name="textAppearance">?attr/textAppearanceSmallInverse</item>
<item name="textColor">@color/primary_text_light</item>
<item name="gravity">center_vertical|center_horizontal</item>
</style>
没错,Button在默认的Style中就将clickable设置为true了。所以在默认情况下Button的clickable=true。通过下面这行代码(View.java的13743行)就可以知道,当clickable=true时,
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP){
// 处理逻辑
}
就可以进入if当中继续处理,因为我们响应click事件一般是在我们手按下再抬起后进行。所以,我们猜测是在MotionEvent.ACTION_UP事件后触发click的。所以我们直接看if条件中的ACTION_UP中的逻辑:
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
在进入处理click之前,会先判断是否已经被长按做了处理,并且此次的ACTION_UP事件没有被忽略掉。当这些条件都满足后,首先会将长按的callback remove。然后会通过Post Runable的方式将PerformClick的实例post到队列中等待处理,不直接去处理click事件而是使用post的方式是确保如果有视图相关的更新操作完成后再触发performClickInternal()。我们先看下PerformClick类是干什么的?
private final class PerformClick implements Runnable {
@Override
public void run() {
performClickInternal();
}
}
很显然就是Runable对象,其中就是调用了performClickInternal()方法,而此方法中调用的是performClick方法:
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
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;
}
可以看到,最后是直接同过ListenerInfo中的mOnClickListener对象调用onClick方法。而ListenerInfo中的mOnClickListener对象就是我们通常使用view.setOnclickListener()方法设置赋值的:
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
// 赋值操作
getListenerInfo().mOnClickListener = l;
}
至此,onClick事件是如何从onTouchEvent中触发的就可以完全看出来了。
同理,onLongClick类似,笔者这里就不做详细分析了。留给读者自己去详细的看下源码,这里简单的介绍下。
onLongClick事件是如何处理的呢?因为onCLick事件是在手指抬起后触发的,所以我们选择分析的是ACTION_UP事件,但是长按事件是在我们长按某个View的时候触发的,所以并没有将手指抬起来。所以我们肯定是在分析处理ACTION_DOWN中处理的。我们查看ACTION_DOWN事件下调用的checkForLongClick方法:
private void checkForLongClick(int delayOffset, float x, float y) {
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.setAnchor(x, y);
mPendingCheckForLongPress.rememberWindowAttachCount();
mPendingCheckForLongPress.rememberPressedState();
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
}
我们看到onTouchEvent中的checkForLongClick中在最后一行通过postDelayed延迟发送了一个Runable对象:mPendingCheckForLongPress。延时时间是ViewConfiguration.getLongPressTimeout() - delayOffset的时间差。综上,简单来说,当我们按下屏幕的时候发送了一个延时的Runable,然后等到Runable被执行的时候,在通过一些标志位判断当前是否还满足长按被执行的条件,如果满足,回调listener中的onLongClick。细节请读者自行对着源码看哦。
三、总结
对于一般的View来讲,onTouchEvent中处理的无非是对View的一个点击事件的处理、按下状态的处理、长按的处理。读者可以对类似于ScrollView这种带滑动的控件的onTouchEvent分析一下,对比于此文中的实现也不太一样哦。
-End-
最近有一些小伙伴,让我帮忙找一些 面试题 资料,于是我翻遍了收藏的 5T 资料后,汇总整理出来,可以说是程序员面试必备!所有资料都整理到网盘了,欢迎下载!
面试题
】即可获取