从Android源码的角度理解应用开发(1)-Touch机制

Touch概述

Touch操作即是用手触摸或者用鼠标操作屏幕所造成的事件触发。这些事件最基本的包括按下Down,移动Move,取消Cancel和离开触摸屏Up四种事件。一个完整的Touch过程一般是由Down->(Move)->Up/Cancel这四个事件组成,值得注意的是,一个完整的触摸事件必须由Down开始,再到Up/Cancel结束,中间的Move可以有可以没有,当然Touch事件不止这四个事件,但这四个事件是最基本,开发中必须考虑到的。

当用户开启应用触摸屏幕,系统服务就通过IPC(Binder)通知应用的主线程Looper中,最终传递到我们应用中Activity,View和ViewGroup中。

需要对Touch机制清晰才可以解决以下一些类似问题:

1. touch监听没被调用到

2. 双层滑动模块嵌套后发生滑动不了的现象

3. 设置了onClickListener后,点击View没有反馈

4. 点击两下View才调用onClickListener的bug

宏观

以上是Touch事件的传递顺序,一个Touch事件要传递到View中,必须经过Activity向下分发,如果在ViewGroup在子View中找到可以处理这个事件的View,则向下再传递下去,否则ViewGroup会尝试处理这个事件。下面详细介绍View,ViewGroup,Activity这三个类收到Touch事件的处理已经它们如何分发Touch事件。

View的Touch逻辑

Android中View对于Touch的处理逻辑主要集中在以下三个个位置中

//最主要的触摸事件的分发逻辑,向接收Touch事件的子View(包括自己)派发事件,对于View而非ViewGroup来说,这里只会对自己分发。
boolean dispatchTouchEvent(MotionEvent event);

//当前View处理触摸事件的可选方法,在dispatchTouchEvent()中被调用
void setOnTouchListener(OnTouchListener l);
public interface OnTouchListener {
    boolean onTouch(View v, MotionEvent event);
}

//当前View处理触摸事件的默认方法,在dispatchTouchEvent()中被调用,如果已经设置了OnTouchListener,并且OnTouchListener已经消费了这个Touch事件,返回true,则不会触发这个方法。
boolean onTouchEvent(MottionEvent event);

以上几个方法返回值为true代表事件被处理,如果返回false,则代表事件没有被处理。

View之dispatchTouchEvent

让我们看一下dispatchTouchEvent的逻辑,删除部分不重要代码,源码如下:

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
    //1.停止嵌套滑动
    final int actionMasked = event.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        stopNestedScroll();
    }
    //2.安全监测
    if (onFilterTouchEventForSecurity(event)) {
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;

        //3.如果当前View使能(setEnabled(true)),则调用Touch监听器
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        //4.如果Touch监听器返回false或者没有调用Touch监听器,则返回调用onTouchEvent()
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    //停止嵌套滑动
    if (actionMasked == MotionEvent.ACTION_UP ||
            actionMasked == MotionEvent.ACTION_CANCEL ||
            (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
        stopNestedScroll();
    }

    return result;
}

从上面的源码中可以看出View的dispatchTouchEvent()主要的逻辑如下:

1.停止嵌套滑动(5.0以后添加的)

2.做了安全监测,如果View开启了安全检测(setFilterTouchesWhenObscured(true))并且当前View所在的Window被其他Window遮盖的话,则不会调用再处理Touch事件

3.如果当前View使能,才会调用OnTouchListener

4.不管使能与否,只要OnTouchListener没有处理事件,就会让onTouchEvent()来处理事件

View之onTouchEvent

前面我们已经知道如果事件没有被OnTouchListener处理的话,将会被onTouchEvent()处理。

onTouchEvent()在源码中主要是处理

press :按下时候View状态的改变,比如View的背景的drawable会变成press 状态
click/tap: 快速点击
longClick:长按
focus:跟press类似,也是View状态的改变
touchDelegate:分发这个点击事件给其他的View,这个点击事件传到其他View前会改变这个事件的点击坐标,如果在指定的Rect里面,则是View的中点坐标,否则在View之外

让我们看一下OnTouchListener的逻辑,源码如下:

public boolean onTouchEvent(MotionEvent event) {
        final int viewFlags = mViewFlags;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            //1.View不使能的情况下(setEnabled(false)),依然可能消费事件
            return (((viewFlags & CLICKABLE) == CLICKABLE ||
                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
        }

        //2.用TouchDelegate将自己的区域变成其他View中心点的操作
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        //3.从这里跟1结合可以知道,只要View是Clickable或者LongClickable,就一定消费事件
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    //只有在press的情况下,才可能click,longClick也做了同样的判断
                    if ((mPrivateFlags & PRESSED) != 0) {
                        //4.如果我们在当前View还没获取焦点,并且能在touch下foucus,那么第一次点击只会将这个View的状态改成focus,而不会触发click
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }
                        //5.已经有longClick执行过了,就不再执行click了
                        if (!mHasPerformedLongPress) {
                            if (mPendingCheckForLongPress != null) {
                                removeCallbacks(mPendingCheckForLongPress);
                            }
                            if (!focusTaken) {
                                performClick();
                            }
                        }
                        //6.取消press
                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (!post(mUnsetPressedState){
                            mUnsetPressedState.run();
                        }
                    }
                    break;

                case MotionEvent.ACTION_DOWN:
                    //7.press,定时检测并且执行longclick
                    mPrivateFlags |= PRESSED;
                    refreshDrawableState();
                    if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
                        postCheckForLongClick();
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    //8.清理状态
                    mPrivateFlags &= ~PRESSED;
                    refreshDrawableState();
                    break;

                case MotionEvent.ACTION_MOVE:
                    final int x = (int) event.getX();
                    final int y = (int) event.getY();

                    //9.如果移动到View外,则不press,如果移动到View内,则press
                    int slop = ViewConfiguration.get(mContext).getScaledTouchSlop();
                    if ((x < 0 - slop) || (x >= getWidth() + slop) ||
                            (y < 0 - slop) || (y >= getHeight() + slop)) {
                        // Outside button
                        if ((mPrivateFlags & PRESSED) != 0) {
                            // Remove any future long press checks
                            if (mPendingCheckForLongPress != null) {
                                removeCallbacks(mPendingCheckForLongPress);
                            }
                            mPrivateFlags &= ~PRESSED;
                            refreshDrawableState();
                        }
                    } else {
                        // Inside button
                        if ((mPrivateFlags & PRESSED) == 0) {
                            mPrivateFlags |= PRESSED;
                            refreshDrawableState();
                        }
                    }
                    break;
            }
            return true;
        }

        return false;

View的onTouch方法归纳为一下几点:

1.不管View使能与否,只要clickable或者longclickable,就一定消费事件(返回true)

2.如果View不使能,并且clickable或者longclick,就只会消费事件但不做其他任何操作

3.如果View使能,先看看TouchDelegate消费与否,如果不消费再给自己消费

4.处理包括focus,press,click,longclick

ViewGroup的Touch逻辑

而ViewGroup继承与View,并覆盖了

dispatchTouchEvent(MotionEvent event);

而且比View多了一个处理Touch的位置:

viewgroup.onInterceptTouchTouchEvent(MotionEvent event);

这个方法的返回值主要用于是否阻止向子View派发触摸事件,默认返回false,不阻止。

精简后的源码如下:

public boolean dispatchTouchEvent(MotionEvent ev) {
    final int action = ev.getAction();
    final float xf = ev.getX();
    final float yf = ev.getY();
    final float scrolledXFloat = xf + mScrollX;
    final float scrolledYFloat = yf + mScrollY;
    final Rect frame = mTempRect;

    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

    if (action == MotionEvent.ACTION_DOWN) {
        //1.只有在非拦截的情况的下寻找target
        if (disallowIntercept || !onInterceptTouchEvent(ev)) {
            // 防止onInterceptTouchEvent()的时候改变Action
            ev.setAction(MotionEvent.ACTION_DOWN);
            // 遍历子View,第一个消费这个事件的子View的为Target
            final int scrolledXInt = (int) scrolledXFloat;
            final int scrolledYInt = (int) scrolledYFloat;
            final View[] children = mChildren;
            final int count = mChildrenCount;
            for (int i = count - 1; i >= 0; i--) {
                final View child = children[i];
                //当然只遍历可见的,并且没有在进行动画的。
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                        || child.getAnimation() != null) {
                    child.getHitRect(frame);
                    if (frame.contains(scrolledXInt, scrolledYInt)) {
                        // offset the event to the view‘s coordinate system
                        final float xc = scrolledXFloat - child.mLeft;
                        final float yc = scrolledYFloat - child.mTop;
                        ev.setLocation(xc, yc);
                        if (child.dispatchTouchEvent(ev))  {
                            // Event handled, we have a target now.
                            mMotionTarget = child;
                            return true;
                        }
                                            }
                }
            }
        }
    }

    boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
            (action == MotionEvent.ACTION_CANCEL);
    //up或者cancel的时候清空DisallowIntercept
    if (isUpOrCancel) {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }

    // 如果没有target,则把自己当成View,向自己派发事件
    final View target = mMotionTarget;
    if (target == null) {
        // We don‘t have a target, this means we‘re handling the
        // event as a regular view.
        ev.setLocation(xf, yf);
        return super.dispatchTouchEvent(ev);
    }

    // 如果有Target,拦截了,则对Target发送Cancel,并且清空Target
    if (!disallowIntercept && onInterceptTouchEvent(ev)) {
        final float xc = scrolledXFloat - (float) target.mLeft;
        final float yc = scrolledYFloat - (float) target.mTop;
        ev.setAction(MotionEvent.ACTION_CANCEL);
        ev.setLocation(xc, yc);
        if (!target.dispatchTouchEvent(ev)) {
            // target didn‘t handle ACTION_CANCEL. not much we can do
            // but they should have.
        }
        // clear the target
        mMotionTarget = null;
        // Don‘t dispatch this event to our own view, because we already
        // saw it when intercepting; we just want to give the following
        // event to the normal onTouchEvent().
        return true;
    }
    //up 或者 cancel清空Target
    if (isUpOrCancel) {
        mMotionTarget = null;
    }

    //如果有Target,并且没有拦截,则向Target派发事件,这个事件会转化成Target的坐标系
    final float xc = scrolledXFloat - (float) target.mLeft;
    final float yc = scrolledYFloat - (float) target.mTop;
    ev.setLocation(xc, yc);

    return target.dispatchTouchEvent(ev);
}

综上,ViewGroup的主要的任务是找一个Target,并且用这个target传递事件,主要逻辑如下

1.在Down并且不拦截的时候会多出一个寻找Target的过程,在这个过程中遍历子View,如果子View的dispatchTouch为true,则这个子View就是当前ViewGroup的Target。找Target是处理Down事件时候特有的,其他事件不会触发找Target。

2.如果没有Target,则发送把自己当做一个View去处理这个事件(super.dispatchTouch())

3.如果有Target并且拦截,则发送Cancel给子View

4.如果有Target并且不拦截,则调用Target的dispatchTouch

5.可以利用requestDisallowInterceptTouchEvent(boolean)来强制viewparent不拦截事件。但是作用域限于一个Touch的过程(Down->Up/Cancel)

Activity的Touch逻辑

ViewGroup收到的事件是由Activity发送出去的。Activity的Touch逻辑非常简单,源码如下:

public boolean dispatchTouchEvent(MotionEvent ev) {
      if (ev.getAction() == MotionEvent.ACTION_DOWN) {
          onUserInteraction();
      }
      //这个最终传递到setContentView对应的View中
      if (getWindow().superDispatchTouchEvent(ev)) {
          return true;
      }
      //如果ContentView没有对时间进行处理,统一由Activity的onTouchEvent()来处理
      return onTouchEvent(ev);
}

Activity的Touch逻辑归纳如下:

Activity将事件经过一些步骤发送给ContentView,如果ContentView没消费,就交给Activity自己处理。

问题解决

文章开头提出了几个问题,可以用这篇文章的分析解决:

  1. touch监听器没被调用到?

    View.dispatchTouchEvent(),ViewGroup.dispatchTouchEvent()

    a)如果是View非使能,直接用setEnabled(true)

    b)如果是事件被这个View的viewparent拦截了。可以修改这个viewparent的onInterceptTouchTouchEvent(),或者在这个View中调用getParent().requestDisallowInterceptTouchEvent()

  2. 双层滑动模块嵌套后发生滑动不了的现象?

    ViewGroup.dispatchTouchEvent()

    如果是事件被这个View的viewparent拦截了。可以修改这个viewparent的onInterceptTouchTouchEvent(),或者在这个View中调用getParent().requestDisallowInterceptTouchEvent()

  3. 设置了onClickListener后,点击View没有反应?

    View.onTouchEvent()

    a)如果是View非使能,直接用setEnabled(true)

    b)可能覆盖了onTouchEvent(),需要在覆盖的方法调用super.onTouchEvent()或者手动调用performClick()

  4. 点击两下View才调用onClickListener的bug?

    View.onTouchEvent()

    这个其实是安卓的设计,当某个View调用了setFocusableInTouchMode(true)后,第一次点击会引起这个View的focus,第二次点击才会调用onClickListener,只需要设置setFocusableInTouchMode(false)即可。

时间: 2024-08-01 14:46:29

从Android源码的角度理解应用开发(1)-Touch机制的相关文章

Android Volley完全解析(四),带你从源码的角度理解Volley

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/17656437 经过前三篇文章的学习,Volley的用法我们已经掌握的差不多了,但是对于Volley的工作原理,恐怕有很多朋友还不是很清楚.因此,本篇文章中我们就来一起阅读一下Volley的源码,将它的工作流程整体地梳理一遍.同时,这也是Volley系列的最后一篇文章了. 其实,Volley的官方文档中本身就附有了一张Volley的工作流程图,如下图所示. 多数朋友突然看到一张这样

[转]Android Volley完全解析(四),带你从源码的角度理解Volley

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/17656437 经过前三篇文章的学习,Volley的用法我们已经掌握的差不多了,但是对于Volley的工作原理,恐怕有很多朋友还不是很清楚.因此,本篇文章中我们就来一起阅读一下Volley的源码,将它的工作流程整体地梳理一遍.同时,这也是Volley系列的最后一篇文章了. 其实,Volley的官方文档中本身就附有了一张Volley的工作流程图,如下图所示. 多数朋友突然看到一张这样

Android中图片加载框架Glide解析2----从源码的角度理解Glide的执行流程

转载地址:http://blog.csdn.net/guolin_blog/article/details/53939176 在本系列的上一篇文章中,我们学习了Glide的基本用法,体验了这个图片加载框架的强大功能,以及它非常简便的API.还没有看过上一篇文章的朋友,建议先去阅读 Android图片加载框架最全解析(一),Glide的基本用法 . 在多数情况下,我们想要在界面上加载并展示一张图片只需要一行代码就能实现,如下所示: Glide.with(this).load(url).into(i

从源码的角度理解四大组件的工作过程——Android开发艺术探索笔记

原文链接http://sparkyuan.me/2016/03/14/四大组件的工作过程/ 转载注明出处 系统对四大组件的过程进行了很大程度的封装,日常开发中并不需要了解底层的工作原理,那么研究这些原理的意义在哪里呢? 如果你想在技术上更进一步,那么了解一些系统的工作原理是十分必要的,也是开发人员日后成长为高级工程师所必备的技术能力. Android作为一个优秀的基于Linux操作系统,其内部一定有很多值得我们学习的地方,通过对Android操作系统的学习对提高开发人员的内功有很大的好处. 如果

Android图片加载框架最全解析(二),从源码的角度理解Glide的执行流程

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/53939176 本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每天都有文章更新. 在本系列的上一篇文章中,我们学习了Glide的基本用法,体验了这个图片加载框架的强大功能,以及它非常简便的API.还没有看过上一篇文章的朋友,建议先去阅读 Android图片加载框架最全解析(一),Glide的基本用法 . 在多数情况下,我们想要在界面上加载并展示一

从Android源码的角度分析Binder机制

IPC 为了弄懂IPC的来龙去脉,我将从以下三个方面为大家来讲解,希望对大家理解IPC会有帮助 什么是IPC IPC是Inter Process Communication的缩写,其意思就是进程间的通信,也就是两个进程之间的通信过程.我们都知道在Android系统中,每个应用都运行在一个进程上,具有自己的DVM实例,而且进程之间是相互隔离的,也就是说各个进程之间的数据是互相独立,互不影响的,而如果一个进程崩溃了,也不会影响到另一个进程.采取这样的设计是有一定道理的,例如这样的前提下将互相不影响的

Android 源码分析(四) Handler 异步消息机制

一.Handler 使用方法: Handler handler = new Handler() { @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what) { case 1: Log.i(TAG,"UI thread sendEmptyMessage...1"); break; case 2: Log.i(TAG,"thread sendEmp

第五节:从源码的角度理解各种Result(ActionResult、JsonResult、JavaScriptResult等)

一. 背景 二. 逐个分析 1. 自动属性 1. 自动属性 1. 自动属性 1. 自动属性 1. 自动属性 1. 自动属性 1. 自动属性 1. 自动属性 三. 自己扩展 四. 重点研究(ViewResult)

《Android源码设计模式解析》读书笔记——Android中你应该知道的设计模式

断断续续的,<Android源码设计模式解析>也看了一遍,书中提到了很多的设计模式,但是有部分在开发中见到的几率很小,所以掌握不了也没有太大影响. 我觉得这本书的最大价值有两点,一个是从设计模式的角度去理解Android源码,结合着日常开发中的常用类,对设计模式的理解会更加的深刻:另外一个好处就是了解常用模式,再看其他人写的代码的时候,更容易理解代码思路.下面是我的读书笔记和一些思考,设计模式只整理我认为重要的部分. 建造者模式 建造者模式最明显的标志就是Build类,而在Android中最常