关闭

自定义View系列教程06--详解View的Touch事件处理

标签: 自定义ViewmeasureonTouchonClickdispatch
15582人阅读 评论(25) 收藏 举报
分类:

探索Android软键盘的疑难杂症
深入探讨Android异步精髓Handler
详解Android主流框架不可或缺的基石
站在源码的肩膀上全解Scroller工作机制


Android多分辨率适配框架(1)— 核心基础
Android多分辨率适配框架(2)— 原理剖析
Android多分辨率适配框架(3)— 使用指南


自定义View系列教程00–推翻自己和过往,重学自定义View
自定义View系列教程01–常用工具介绍
自定义View系列教程02–onMeasure源码详尽分析
自定义View系列教程03–onLayout源码详尽分析
自定义View系列教程04–Draw源码分析及其实践
自定义View系列教程05–示例分析
自定义View系列教程06–详解View的Touch事件处理
自定义View系列教程07–详解ViewGroup分发Touch事件
自定义View系列教程08–滑动冲突的产生及其处理


PS:如果觉得文章太长,那就直接看视频


在之前的几篇文章中结合Andorid源码还有示例分析完了自定义View的三个阶段:measure,layout,draw。 在自定义View的过程中我们还经常需要处理View的Touch事件,这就涉及到了大伙常说的Touch事件的分发。其实,这一部分还是有些复杂的,而且有的地方不是很好理解,尤其是对于刚上路的新司机来说经常理不清楚,欲求不满,欲罢不能——想搞懂却又觉得难,想放弃又觉得舍不得。

好吧,我也经历过这些痛楚,感同身受。

所以,我们就从相对而言比较简单的View的Touch事件处理入手开始这部分知识的学习和总结。

滴滴,开车了,车门即将关闭。上车请刷卡,没卡的乘客请投币。


如果一个View(比如Button)接收到Touch,那么该Touch事件首先会传入到它的dispatchTouchEvent( )方法,所以我们从这里开始学习View对Touch事件的处理。

    /**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
        if (event.isTargetAccessibilityFocus()) {

            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }

            event.setTargetAccessibilityFocus(false);
        }

        boolean result = false;

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(event)) {
            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);
        }


        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

嗯哼,这段源码不长,除了注释就剩下不到100行了。该方法的输入参数为event它表示Touch事件,这个很好理解;那么它的返回值有是什么含义呢?该boolean值表示的是Touch事件是否被消费。

在此,对该部分源码的核心部分和主要逻辑做一个梳理

第一步:
调用TouchListener中的onTouch()处理Touch事件,请参见代码第31-32行

该if判断中一共包含了4个条件,必须同时满足时才表示Touch事件被消费

  1. li != null
    ListenerInfo是View中的一个静态类,包含了几个Listener,比如TouchListener,FocusChangeListener,LayoutChangeListeners,ScrollChangeListener等等。一般情况下它均不为null,所以我们不用过多关注它。
  2. li.mOnTouchListener != null
    mOnTouchListener是由View设置的,比如mButton.setOnTouchListener()。所以如果View设置了Touch监听那么,那么mOnTouchListener不空;反之,mOnTouchListener为null
  3. (mViewFlags & ENABLED_MASK) == ENABLED
    当前View可用(ENABLED)。通常可调用view.setEnabled( )设置View是否可用
  4. li.mOnTouchListener.onTouch(this, event)
    这一点其实是在li.mOnTouchListener != null的基础上继续判断。判断TouchListener的onTouch( )方法是否消耗了Touch事件。返回值为true表示消费掉该事件,false表示未消费。

在这四个条件中,我们通常最关心的就是最后一个:TouchListener的onTouch()方法。假如这四个条件中的任意一个不满足,那么result仍为false;则进入下一步

第二步:
调用View自身的onTouchEvent()处理Touch事件,请参见代码第36-38行

if (!result && onTouchEvent(event)) {
     result = true;
 }

嗯哼,看到了吧:如果在上一步中Touch事件被消费result为true,就不会执行这三行代码了。该处调用了onTouchEvent()若该方法返回值false那么dispatchTouchEvent()的返回值也为false;反之,若该方法返回值为true,那么dispatchTouchEvent()的返回值亦为true。
既然onTouchEvent()这么重要,我们就接着看该方法的源码

    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        if ((viewFlags & ENABLED_MASK) == DISABLED) {

            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }

            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }

        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            setPressed(true, x, y);
                       }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {

                            removeLongPressCallback();

                            if (!focusTaken) {

                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    boolean isInScrollingContainer = isInScrollingContainer();

                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        setPressed(true, x, y);
                        checkForLongClick(0);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);

                    if (!pointInView(x, y, mTouchSlop)) {
                        removeTapCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {

                            removeLongPressCallback();

                            setPressed(false);
                        }
                    }
                    break;
            }

            return true;
        }

        return false;
    }

这段代码稍微复杂一些,在此分析几个核心点。

  1. 当View为disable时对于Touch的处理,请参见代码第7-16行。
    若一个View是disable的,如果它是CLICKABLE或者LONG_CLICKABLE或CONTEXT_CLICKABLE的就返回true,表示消耗掉了Touch事件。
    但是请注意,该view所对应的ClickListener.onClick( )不会有任何的响应。即官方文档的描述:

    A disabled view that is clickable still consumes the touch events, it just doesn’t respond to them.

    若View虽然是disable的,但只要满足这三个条件中的一个,它就会消费掉Touch事件但不再回调view的onClick( )方法

  2. 处理ACTION_DOWN,ACTION_MOVE,ACTION_UP事件等,请参见代码第24-116行。
    在此请注意在对于ACTION_UP的处理时调用了performClick(),请参见代码第50行。

    public boolean performClick() {
        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);
        return result;
    }

    在该方法中调用了view的mOnClickListener.onClick( ),请参见代码第6行。
    嗯哼,看到了吧:我们平常见得很多的Click事件是在View的onTouchEvent( )中处理ACTION_UP时调用的。

  3. 返回onTouchEvent()方法的输出结果,请参见代码第118-121行。
    在该处请尤其注意:
    如果View是enable的,只要该View满足CLICKABLE和LONG_CLICKABLE以及CONTEXT_CLICKABLE这三者的任意一个(请参见代码第24-26行)不论当前的action是什么,该onTouchEvent()返回的均是true(请参见代码第118行);而且会在ACTION_UP时处理click事件。
    同理,如果这三个条件都不满足,该onTouchEvent()返回的是false。
    也请注意一个细节:
    View的clickable属性视不同的子View有所差异
    比如:Button的clickable默认为true,但是TextView的clickable属性默认为false。
    View的longClickable属性默认为false。
    当然,我们可以通过代码修改这些默认的属性。
    比如:setClickable()和setLongClickListener()可以改变View的CLICKABLE和LONG_CLICKABLE属性。
    除此以外,通过设置监听器也可改变某些属性。
    比如:setOnClickListener()会将View的CLICKABLE设置为true;setOnLongClickListener()会将View的LONG_CLICKABLE设置为true。

第三步:
返回Touch事件是否被消费,请参见代码第52行

以上就为View对于Touch事件的主要步骤。
在此我画了一个简单的流程图,现结合该图和刚才的源码分析对View的Touch事件处理流程做一个总结。

这里写图片描述

  1. View处理Touch事件的总体流程
    dispatchTouchEvent()—>onTouch()—>onTouchEvent()—>onClick()
    Touch事件最先传入dispatchTouchEvent()中;如果该View存在TouchListener那么会调用该监听器中的onTouch()。在此之后如果Touch事件未被消费,则会执行到View的onTouchEvent()方法,在该方法中处理ACTION_UP事件时若该View存在ClickListener则会调用该监听器中的onClick()
  2. onTouch()与onTouchEvent()以及click三者的区别和联系
    2.1 onTouch()与onTouchEvent()都是处理触摸事件的API
    2.2 onTouch()属于TouchListener接口中的方法,是View暴露给用户的接口便于处理触摸事件,而onTouchEvent()是Android系统自身对于Touch处理的实现
    2.3 先调用onTouch()后调用onTouchEvent()。而且只有当onTouch()未消费Touch事件才有可能调用到onTouchEvent()。即onTouch()的优先级比onTouchEvent()的优先级更高。
    2.4 在onTouchEvent()中处理ACTION_UP时会利用ClickListener执行Click事件。所以Touch的处理是优先于Click的
    2.5 简单地说三者执行顺序为:onTouch()–>onTouchEvent()–>onClick()
  3. View没有事件的拦截(onInterceptTouchEvent( )),ViewGroup才有,请勿混淆

关于View对Touch事件的处理就分析到此。

滴滴,到站了,下车的乘客们请往后门走。

PS:若觉得文章太长,那就直接看视频吧。


who is the next one? ——> 详解ViewGroup的Touch事件分发

28
2
查看评论

Android自定义view详解

对于我这样一个Android初级开发者来说,自定义View一直是一个遥不可及的东西,每次看到别人做的特别漂亮的控件,自己心里那个痒痒啊,可是又生性懒惰,自己不肯努力去看书,只能望而兴叹,每次做需求用到自定义控件,就直接去Github上找,找到合适的就用,找不到合适的,凑合也用,反正从来没想过要自己来...
  • pengpenggxp
  • pengpenggxp
  • 2016-07-11 17:39
  • 3232

Android 自定义 View 之处理 TouchEvent

我们都知道,自定义View的第一步是测量当前剩余空间,或者说是界面的大小,也就是measure了;然后是layout,即判断自定义view在父控件上显示的位置,这两点在上一篇通过讲解过了,所以今天我们要说的就是对TouchEvent的处理。
  • jim__charles
  • jim__charles
  • 2017-01-28 19:31
  • 4319

自定义View框架完全解析

前言 在Android中有很多的控件来供大家使用,但是和强大的IOS相比,Android所提供的控件平淡了许多,由于Android的开源可以让每个开发者都能创建自己的视图控件来满足自己的需求,正因为这样就出现各种各样的自定义控件,久而久之就形成了自定义视图框架。 这里介绍两种方法 1、给每一个需要配...
  • weiwozhiyi
  • weiwozhiyi
  • 2017-03-18 11:36
  • 624

从此再有不愁自定义View——Android自定义view详解

更多相关资料参见:Android自定义view详解 对于我这样一个Android初级开发者来说,自定义View一直是一个遥不可及的东西,每次看到别人做的特别漂亮的控件,自己心里那个痒痒啊,可是又生性懒惰,自己不肯努力去看书,只能望而兴叹,每次做需求用到自定义控件,就直接去Githu...
  • xhmj12
  • xhmj12
  • 2016-11-13 19:00
  • 580

Android自定义View(二、深入解析自定义属性)

转载请标明出处: http://blog.csdn.net/xmxkf/article/details/51454685 本文出自:【openXu的博客】 [TOC] 在上一篇博客《Android自定义View(一、初体验)》中我们体验了自定义控件的基本流程: 继承V...
  • u010163442
  • u010163442
  • 2016-05-21 03:53
  • 18735

Android自定义View(一)View绘制流程以及invalidate()等相关方法分析

转自:http://blog.csdn.net/yanbober/article/details/46128379/      【工匠若水 http://blog.csdn.net/yanbober】 原本打算自己写的,但是看到这篇文章,觉得写的太好了,,,...
  • TokgoLiang
  • TokgoLiang
  • 2016-01-08 10:18
  • 1444

自定义View执行invalidate()方法

1、自定义一个view时,重写onDraw。 调用view.invalidate(),会触发onDraw和computeScroll()。前提是该view被附加在当前窗口上 view.postInvalidate(); //是在非UI线程上调用的2、自定义一个ViewGroup,重写onDraw...
  • xuxingxing002
  • xuxingxing002
  • 2015-12-16 17:35
  • 331

深入探索Android 中view的touch事件传递

每个View的子类都具有下面三个方法: 一、这个方法用来分发TouchEvent public boolean dispatchTouchEvent(MotionEvent ev) {          ...
  • chen364567628
  • chen364567628
  • 2016-09-27 21:54
  • 441

Android自定义View之onMeature解析

android中的单元测试一般分为:功能测试,ui测试,单元测试等等; 由于android运行过程中需要android运行环境因此android的单元测试一般无法提供运行环境,因此单元测试主要是功能测试,主要用于测试一些功能性的需求; 新版的android studio中添加了对单元测试的支持;...
  • qq_23547831
  • qq_23547831
  • 2016-01-08 21:56
  • 1516

很有用的自定义View详解教程

注册 登录 转载地址:http://www.jianshu.com/p/c84693096e41 自定义View,有这一篇就够了 字数4899 阅读2302 评论20 喜欢105 我的CSDN博客同步发布:自定义Vi...
  • qq_25143591
  • qq_25143591
  • 2016-10-14 11:14
  • 125
    个人资料
    • 访问:1944276次
    • 积分:27793
    • 等级:
    • 排名:第233名
    • 原创:819篇
    • 转载:0篇
    • 译文:1篇
    • 评论:1296条
    博客专栏
    开发交流


    为方便大家学习和交流Android开发,建了个群,欢迎大家加入。

    QQ群: 183899857(已满)
    QQ群: 250468947(新开)

    文章分类