结合源码,重温 Android View 的事件处理知多少 ?
前言
- Android View 的 事件处理在我们的编程中,可谓是无处不在了。但对于大多数人而言,一直都是简单的使用,对其原理缺乏深入地认识。
- 学 Android 有一段时间了,最近发现,很多基础知识开始有些遗忘了,所以从新复习了 View 的事件分发。特地整理成了这篇文章分享给大家。
- 本文不难,可以作为大家茶余饭后的休闲。
祝大家阅读愉快!

方便大家学习,我在 GitHub 上建立个 仓库
- 仓库内容与博客同步更新。由于我在 - 稀土掘金- 简书- CSDN- 博客园等站点,都有新内容发布。所以大家可以直接关注该仓库,即使获得精彩内容
一、View 的事件回调
- 我们结合源码看看 View的事件分发是个怎样的过程,首先我们建立一个类MyButton类继承AppCompatButton用于测试:
public class MyButton extends AppCompatButton {
    private final String TAG = "DeBugMyButton";
        public MyButton(Context context) {
        super(context);
    }
    public MyButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public MyButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}
1.1 事件分发流程
- 我们都知道有一个方法叫做 public boolean dispatchTouchEvent(MotionEvent event)。首先我们要知道,对于我们这个自定义控件,他的触摸事件都是从我们dispatchTouchEvent这个方法开始往下去分发的。所以可以说:这个方法是一个入口方法。
1.1.1 onTouchEvent 作用
- 现在我们重写该方法和另一个方法:onTouchEvent,并且打印一行日志:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    Log.d(TAG, "----on dispatch Touch Event----");
    return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.d(TAG, "----on touch event----");
    }
    return super.onTouchEvent(event);
}
- 然后我们在 MainActivity中,设置一个实例化一个MyButton控件对象用于测试,并且给他添加一个onClickListenter和setOnTouchListener
public class MainActivity extends AppCompatActivity {
    private final String TAG = "DeBugMainActivity";
    /**
     * 自定义控件 MyButton
     */
    private MyButton mMyButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        iniView();
    }
    /**
     * 实例化控件
     */
    private void iniView() {
        mMyButton = findViewById(R.id.my_button);
	mMyButton.setOnTouchListener(new View.OnTouchListener() {
	    @Override
	    public boolean onTouch(View v, MotionEvent event) {
	        switch (event.getAction()) {
	            case MotionEvent.ACTION_DOWN:
	                Log.d(TAG, "----on touch----");
	                break;
	            default:
	                break;
	        }
	        return false;
	    }
	});
	mMyButton.setOnClickListener(new View.OnClickListener() {
	    @Override
	    public void onClick(View v) {
	        Log.d(TAG, "----on click----");
	    }
	});
    }
}
- 然后我们运行这个 Demo,点击MyButton按钮,会的到如下日志:

- 我们可以看到首先回调了这个 - dispatchTouchEvent,然后是它的监听器- OnTouch,接着是它的- onTouchEvent,最后又执行了- dispatchTouchEvent,那么这是为什么呢?
- 这是因为我们这儿只监听了 - ACTION_DOWN而当手指抬起时它同样还回去回调- dispatchTouchEvent,最后我们打印- OnClick的回调。
- 总结一下就是: 
 - dispatchTouchEvent->- setOnTouchListener->- onTouchEvent->- setOnClickListener
- 说明我们 - setOnClickListener是通过- onTouchEvent处理,产生了- OnClick。一会我们再来看看其中的原理。
- 既然说 - dispatchTouchEvent像一个入口,就先让我们来看下它是怎么处理和操作的: 首先,既然我们调用了- super.dispatchTouchEvent(event),那么我们就来看看它父类中是怎么实现该方法的。不信的是,它的父类- AppCompatButton也没有实现该方法 ,最后经过层层搜寻,我们发现这个方法是属于- View的方法。
1.1.2 dispatchTouchEvent 的实现
- 那么现在我们来看看 View的dispatchTouchEvent怎么实现的:
public boolean dispatchTouchEvent(MotionEvent event) {
    ......
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    if (!result && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }
    // Clean up after nested scrolls if this is the end of a gesture;
    // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
    // of the gesture.
    if (actionMasked == MotionEvent.ACTION_UP ||
            actionMasked == MotionEvent.ACTION_CANCEL ||
            (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
        stopNestedScroll();
    }
    return result;
}
- 在 dispatchTouchEvent中,我们可以发现下面这样一个代码块
if (li != null && li.mOnTouchListener != null
        && (mViewFlags & ENABLED_MASK) == ENABLED
        && li.mOnTouchListener.onTouch(this, event)) {
    result = true;
}
- 不难看出:如果执行了这个代码段,那么后面的方法就不会执行了,并且 - dispatchTouchEvent会返回- true。我们再仔细观察下其中的条件:在- if条件中我们发现:只有当其满足- li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))时才会执行- if内的操作
- 经过上面分析,我们可以知道: - onTouch事件必须返回- true时,才会执行该方法块。那么我们就回到- MainActivity中。我们发现- setOnTouchListener的- onTouch默认返回值是- false( 不满足返回值为- true), 这就表明他会继续去执行下一个代码块:
if (!result && onTouchEvent(event)) {
    result = true;
}
- 执行这个 - if语句的过程中。首先调用了- onTouchEvent方法。这就解释了,为什么它先执行了- mOnTouchListener,然后再执行- onTouchEvent。
- 现在我们就可以总结一下:首先我们回调了 - dispatchTouchEvent,然后回调- OnTouchListener。这个时候,如果- TouchListener没有- return true,那么就会接着去运行- onTouchEvent( 当然,如果- return true后面的层级就不会执行了 。一句话说就是:到那个层级- return true那么哪个层级就消费掉了这个事件 )。
1.1.3 onTouchEvent 的处理
- 同时我们还有一个结果:我们 onClick( 包括我们的onLongClick) 是来自于我们onTouchEvent这个方法的处理。那么下面我们就来看看View中是怎么处理onTouchEvent的:
public boolean onTouchEvent(MotionEvent event) {
    。。。
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                。。。
                break;
            case MotionEvent.ACTION_DOWN:
                if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                    mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                }
                mHasPerformedLongPress = false;
                if (!clickable) {
                    checkForLongClick(0, x, y);
                    break;
                }
                if (performButtonActionOnTouchDown(event)) {
                    break;
                }
                // Walk up the hierarchy to determine if we're inside a scrolling container.
                boolean isInScrollingContainer = isInScrollingContainer();
                // For views inside a scrolling container, delay the pressed feedback for
                // a short period in case this is a scroll.
                if (isInScrollingContainer) {
                    mPrivateFlags |= PFLAG_PREPRESSED;
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    mPendingCheckForTap.x = event.getX();
                    mPendingCheckForTap.y = event.getY();
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    // Not inside a scrolling container, so show the feedback right away
                    setPressed(true, x, y);
                    checkForLongClick(0, x, y);
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                。。。
                break;
            case MotionEvent.ACTION_MOVE:
                if (clickable) {
                    drawableHotspotChanged(x, y);
                }
                // Be lenient about moving outside of buttons
                if (!pointInView(x, y, mTouchSlop)) {
                    // Outside button
                    // Remove any future long press/tap checks
                    removeTapCallback();
                    removeLongPressCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        setPressed(false);
                    }
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                }
                break;
        }
        return true;
    }
    return false;
}
二、onClick 和 OnLongClick
- 因为我们是拿 ACTION_DOWN作为举例的。那么我们先来分析一下case MotionEvent.ACTION_DOWN: 中onTouchEvent是怎么执行的,以及onClick和OnLongClick是如何产生的:
2.1 onClick 和 OnLongClick 的产生
- 首先,当我们手指按下时,有一个 - mHasPerformedLongPress标识会先被设为- false。再往下会执行一行- postDelayed(mPendingCheckForTap和- ViewConfiguration.getTapTimeout()); 我们来看看这一行的作用:
- 首先,从名字我们就可以猜测,这是个延时执行的方法。我们进一步阅读发现 - mPendingCheckForTap是一个- Runnable动作;- ViewConfiguration.getTapTimeout()是一个- 100mm的延时。也就是说延时- 100mm后去执行- mPendingCheckForTap中的动作。那么我们就来看看- mPendingCheckForTap中做了什么:
private final class CheckForTap implements Runnable {
    public float x;
    public float y;
    @Override
    public void run() {
        mPrivateFlags &= ~PFLAG_PREPRESSED;
        setPressed(true, x, y);
        checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
    }
}
- 也就是说,停一百秒后就开始检查,用户的手指是否离开了屏幕。( 就是当前 ACTION_DOWN之后,有没有触发了ACTION_UP这个环节 ),但是ACTION_DOWN后,我们还有一个ACTION_MOVE过程。在这个ACTION_MOVE中,如果100mm内离开了屏幕、或者离开了这个控件就会触发ACTION_UP,那么就认为这是一个点击事件onClick。如果没有触发ACTION_UP的话,就会再延时400mm。
2.2 ACTION_DOWN 之后流程
- 在 ACTION_DOWN之后,会先等100mm
- 如果没有离开屏幕或者离开控件,就是没有触发 ACTION_UP的话,就会再延时 400mm。
- 这 500mm后就会触发onLongClick事件。
2.3 那么我们现在来验证一下 onLongClick :
- 首先再 MainActivity中加上:
mMyButton.setOnLongClickListener(new View.OnLongClickListener() {
    @Override
    public boolean onLongClick(View v) {
        return true;
    }
});
- 接着,我们发现 OnLongClick是有返回值的,如果返回值是false还会接着去触发onClick事件,如果返回true的话,那么这个长按事件就直接被消费掉了( 也就是这个点击事件就不会完后传递到OnClickListener中去了 )。
2.4 总结
- 100mm时为点击,- 500mm时为长按,接着触发长按事件。
- 再看长按事件的返回值,如果时 true就结束。
- 如果时 false那么OnClickListener就同样也被执行。
- 这就是由 obTouchEvent产生出来的onClick/onLongClick的来龙去脉。
总结
- 我们 View的事件方法,基本上就是这么一个思路,从dispatchTouchEvent到OnTouchListener监听器,再到onTouchEvent,接着onTouchEvent由产生了onClick/onLongClick。
- 如果大家感兴趣的话可以更深入的去阅读源码。
- 重点:学- Android有一段时间了,我打算好好的梳理一下所学知识,包括- Activity、- Service、- BroadcastRecevier事件分发、滑动冲突、新能优化等所有重要模块,欢迎大家关注 _yuanhao 的 博客园 ,方便及时接收更新
- 如果有可以补充的知识点,欢迎大家在评论区指出。
码字不易,你的点赞是我总结的最大动力!
- 由于我在「稀土掘金」「简书」「 - CSDN」「博客园」等站点,都有新内容发布。所以大家可以直接关注我的- GitHub仓库,以免错过精彩内容!
- 一万多字长文,加上精美思维导图,记得点赞哦,欢迎关注 _yuanhao 的 博客园 ,我们下篇文章见! 
结合源码,重温 Android View 的事件处理知多少 ?的更多相关文章
- Android事件分发机制浅谈(三)--源码分析(View篇)
		写事件分发源码分析的时候很纠结,网上的许多博文都是先分析的View,后分析ViewGroup.因为我一开始理解的时候是按我的流程图往下走的,感觉方向很对,单是具体分析的时候总是磕磕绊绊的,老要跳到Vi ... 
- 源码解析Android中View的measure量算过程
		Android中的Veiw从内存中到呈现在UI界面上需要依次经历三个阶段:量算 -> 布局 -> 绘图,关于View的量算.布局.绘图的总体机制可参见博文< Android中View ... 
- wemall app商城源码中android按钮的三种响应事件
		wemall-mobile是基于WeMall的android app商城,只需要在原商城目录下上传接口文件即可完成服务端的配置,客户端可定制修改.本文分享wemall app商城源码中android按 ... 
- Android Touch事件相关源码【Android SourceCode 2.3.6】
		2018-05-31 17:23:46 Note: 这里的源码来自Android 2.3.6,这个版本的代码比较简单,适合理解Touch事件的传递原理.后续版本源码复杂了很多,但是原理都是类似的. 2 ... 
- 源码分析Android Handler是如何实现线程间通信的
		源码分析Android Handler是如何实现线程间通信的 Handler作为Android消息通信的基础,它的使用是每一个开发者都必须掌握的.开发者从一开始就被告知必须在主线程中进行UI操作.但H ... 
- Android so 文件进阶<二>  从dlsym()源码看android 动态链接过程
		0x00 前言 这篇文章其实是我之前学习elf文件关于符号表的学习笔记,网上也有很多关于符号表的文章,怎么说呢,感觉像是在翻译elf文件格式的文档一样,千篇一律,因此把自己的学习笔记分享出来.dls ... 
- 将Android系统源码导入Android studio的方法
		Android源码目录结构如下: |-- Makefile|-- abi (applicationbinary interface,应用程序二进制接口,生成libgabi++.so相关库文件)|-- ... 
- 【Android源码解析】View.post()到底干了啥
		emmm,大伙都知道,子线程是不能进行 UI 操作的,或者很多场景下,一些操作需要延迟执行,这些都可以通过 Handler 来解决.但说实话,实在是太懒了,总感觉写 Handler 太麻烦了,一不小心 ... 
- Handler Looper源码解析(Android消息传递机制)
		Android的Handler类应该是常用到的,多用于线程间的通信,以及子线程发送消息通知UI线程刷新View等等.这里我主要总结下我对整个消息传递机制,包括Handler,Looper,Messag ... 
随机推荐
- pytest1-Installation and Getting Started
			pytest是python的一种单元测试框架(非自带,需要安装),与python自带的unitest测试框架相比,使用起来更加简洁.效率更高.总之,一句话,pytest优于unitest. 1.安装p ... 
- Spring框架 --- 深入
			1.Spring IOC IOC技术: 控制反转,也叫(依赖注入) 控制反转:Bean的生命周期不受你控制,而是交给Spring容器管理. Spring框架如何利用IOC ?: 实现了控 ... 
- LaTeX常用篇(三)---矩阵与表格
			目录 1. 序言 2. 矩阵 2.1 复杂写法 2.2 简化写法 2.3 复杂矩阵 3. 表格 4. 对齐 更新时间:2019.10.02 1. 序言 矩阵是一个强大的工具,许多东西都能够用矩阵来 ... 
- Linux常用命令-不定时记录
			文件移动命令 命令格式:mv [-fiv] source destination 参数说明:-f:force,强制直接移动而不询问-i:若目标文件(destination)已经存在,就会询问是否覆盖- ... 
- travis-ci + php + casperjs 持续集成
			.travis.yml 文件添加内容: sudo: required language: php php: - 5.5 before_script: - npm install -g casperjs ... 
- 数据类型(二)---day04
			目录 上节课回顾 五 变量 (一)什么是变量 (二)变量的组成 (三)变量名的命名规范 (四)常量 (五)python变量内存管理 (六)变量的三种打印方式 六 数据类型 (一)数字类型 (二)字符串 ... 
- Token refresh的实现
			实现原理: 在access_token里加入refresh_token标识,给access_token设置短时间的期限(例如一天),给refresh_token设置长时间的期限(例如七天).当活动用户 ... 
- windows 10 环境下 使用Anaconda搭建 TensorFlow 环境
			##大致步骤 1 安装Anaconda 2 在Anaconda中建立虚拟TensorFlow的虚拟环境 建立虚拟环境的命令是 conda create -n tensorflow python=3. ... 
- NOIP模拟 8
			T1 水题,可是没A,惭愧. 值得一提的是,虽然我忘了kmp,现场制造的kmp因为缺少针对题目的特判挂了,但是现场制造的kmp板子能过字符串板子题.. 对..把板子改装了...而且改对了! nx[]= ... 
- 模拟80(a)
			其实隔壁的那套题比这套难的多....一道都不会.. T1 题目中已经给出了递推公式,那么这题就没什么了,直接矩阵乘就完了. 然而考场上并没有看出矩阵,主要是用了好久发明crt,我知道原理,但是不会打了 ... 
