Android 自定义View学习(十三)——View触控事件学习

学习资料:

个人理解:
View的事件体系主要包含两个方面:触控事件和滑动事件
触控事件主要学习:MotionEvent,事件分发拦截机制
滑动事件主要学习:Velocity速度追踪,GestureDector手势检测,Scorller滑动对象

本篇主要学习触控事件,下篇进行学习滑动事件


1.View的事件分发拦截 <p>

View的测量方法学习大概了解了UI架构图

ActivityonCreate()方法中调用了setContentVie(int myLayoutId),一些列方法回调之后,布局文件中的各种控件就添加到了ContentView,而ContentView则包含在Activity的根View也就是DecorView

View的触控事件的整个过程可以分为: 事件传递,事件拦截,事件处理

View的测量过程是从外向内,由最外层DecorView开始,而事件分发也是由最外层DecorView开始。通常情况下最外层DecorView并不做管理,而是直接开始考虑Activity


1.1 MotionEvent 触摸事件 <p>

手指接触屏幕的一些列事件就封装在MotionEvent,典型的事件:

  • ACTION_DOWN 手指刚接触屏幕
  • ACTION_MOVE 手指在屏幕滑动
  • ACTION_UP 手指离开屏幕

手指触摸屏幕的坐标可以通过getX/Y()getRawX/Y()方法拿到

getX()拿到的是相对于自身左上角的x坐标,getRawX()相对于屏幕左上角的x坐标


1.2 点击事件的分发拦截 <p>

点击事件的分发有3个重要的方法:

  • public boolean dispachTouchEvent(MotionEvent event)
    返回结果表示是否拦截当前事件。返回true,拦截;false,不拦截
    事件分发的第一步,当事件传递到当前View一定会调用。返回结果受此ViewonTouchEvent()方法和下级childViewdispachTouchEvent影响。虽然是事件分发第一步,但绝多数情况不推荐直接修改这个方法

  • public boolean onIntercepTouchEvent(MotionEvent event)
    返回结果用来判断是否拦截某个事件。
    如果当前view拦截了某个事件,在同一个事件的序列中,此方法便不会被再次调用

  • public boolean onTouchEvent(MotionEvent event)
    返回结果表示是否消费了事件。true,消费了,不用在审核了;false,不消费,给父容器处理


一段伪码:

public boolean diapatchTouchEvent(MotionEvent event){
   boolean consume = false;
   //判断是否拦截
   if(onIntercetTouchEvent(ev)){ //拦截
      consume = onTouchEvent(ev);//消费事件
   }else{
      consume = child.dispatchTouchEvent(ev);//chileView开始事件分发
   }
   return consume;  //返回事件拦截结果 默认为false
}

对于一个根ViewGroup(A),点击事件产生后,事件会先传递给A,首先会调AdispatchTouchEvent()
在这个dispatchTouchEvent()方法内部,调用A.onIenterceptTouchEvvent(),并对这个方法的返回值使用if()进行判断:

  • onIenterceptTouchEvvent()方法返回结果为true时,就表示A要拦截当前事件,接着AonTouchEvent()方法就会被调用
  • onIenterceptTouchEvvent()方法返回结果为false时,表示A不拦截当前事件,这时便会childView调用事件分发的第一步dispatchTouchEvent()方法,如此反复,直到事件被消费掉

传递顺序:

Acticty -> Window -> ViewGroup -> View

消费顺序:

Acticty <- Window <- ViewGroup <- View

注意:
当一个View(V)需要修理一个事件时,当V设置了onTouchListener()时,onTouchListener()onTouch()就会被回调。事件具体会如何处理,要看onTouch()的返回值

  • onTouch()返回true,可以理解为onTouchListener消费了事件,便不会传递给onTouchEvent()
  • onTouch()返回false,可以理解为onTouchListener不消费事件,传递给onTouchEvent()来处理

onTouchEvent()方法中,只有V设置了onClickListener()时,onClick()才会被回调

结论1:onTouchListener()优先级比onTouchEvent()高,onClickListener()优先级比onToucnEvent()低


当一个事件由Activity(A)经过ViewGroup(VG)传递到了一个View(V)时,如果V.onTouchEvent()方法不处理,返回false时,VG也不做处理,VG.onTouchEvent()方法也返回false,这个事件最终也便交给了A.onTouchEvent()方法来处理。

分发拦截方法

截图来自GcsSloop安卓自定义View进阶-事件分发机制原理


2.类生活举例 <p>

下面用一些不恰当的实例来演示事件分发拦截的过程


2.1 情景1 <p>

演示一个日常:老板派发工作,给经理提出需求,经理给组长分发任务,组长再给程序员安排任务

  • Activity:老板
  • VG_Manager : 经理
  • VG_GroupLoader:组长
  • V_Programmer:程序员

经理代码:

public class VG_Manager extends LinearLayout {
    private final String TAG = "英勇青铜5";
    private Paint mPaint;

    public VG_Manager(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.WHITE);
        mPaint.setTextSize(70f);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawText("经理",30f,getHeight()-80f,mPaint);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e(TAG, "&&&VG_Manager.dispatchTouchEvent ---> 经理接到老板发的任务通知");
        return super.dispatchTouchEvent(ev);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.e(TAG, "&&&VG_Manager.onInterceptTouchEvent ---> 经理拦截任务,查看任务通知");
        return super.onInterceptTouchEvent(ev);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "&&&VG_Manager.onTouchEvent --->经理把自己的任务做了");
        return super.onTouchEvent(event);
    }
}

组长的代码和经理几乎一摸一样

程序员代码:

public class V_Programmer extends View {
    private final String TAG = "英勇青铜5";
    private Paint mPaint;
    public V_Programmer(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.WHITE);
        mPaint.setTextSize(70f);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawText("程序员",30f,getHeight()-50f,mPaint);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.e(TAG, "&&&V_Programmer.dispatchTouchEvent ---> 程序员接到组长发的任务通知");
        return super.dispatchTouchEvent(event);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "&&&V_Programmer.onTouchEvent ---> 程序员把自己的任务做完");
        return super.onTouchEvent(event);
    }
}

差别就在于View没有onInterceptTouchEvent()方法

xml代码:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.szlk.customview.eventl.VG_Manager
        android:layout_width="270dp"
        android:layout_height="480dp"
        android:background="@color/colorPrimary">

        <com.szlk.customview.eventl.VG_GroupLoader
            android:layout_width="180dp"
            android:layout_height="320dp"
            android:background="@color/colorAccent">

            <com.szlk.customview.eventl.V_Programmer
                android:layout_width="90dp"
                android:layout_height="160dp"
                android:background="@android:color/holo_orange_dark" />
        </com.szlk.customview.eventl.VG_GroupLoader>

    </com.szlk.customview.eventl.VG_Manager>

</RelativeLayout>

运行效果也很简单

运行效果

点击程序员,查看Log信息

点击程序员
点击程序员事件过程

箭头的方向大致就是一个事件的走向


2.2 情景2 <p>

有一天的老板的电脑突然出问题了,老板重启电脑,问题没有解决,老板于是便找来了经理,经理听了老板的描述后,觉得自己能搞定,于是便自己尝试解决问题,没有去找组长

简单修改经理的代码:

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e(TAG, "&&&VG_Manager.dispatchTouchEvent ---> 被老板喊来修电脑");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.e(TAG, "&&&VG_Manager.onInterceptTouchEvent ---> 听了老板描述问题后,决定自己先给老板修一下");
        return true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "&&&VG_Manager.onTouchEvent --->经理把电脑修好了");
        return super.onTouchEvent(event);
    }

主要是将onInterceptTouchEvent()方法返回值改true,也就是将事件拦截下来

点击经理,查看Log信息:

经理给老板修电脑

经理给老板修好电脑

组长onInterceptTouchEvent()返回true和上图就类似了


上面的情况是假设经理会修,如果经理不会修,经理喊来了组长,组长看了老板的电脑后觉得,必须把程序员同学喊来了,于是他们两个都没有拦截事件,把任务安排给了程序员同学,程序员同学到了老板的办公室,给老板直接重装了系统。程序员同学修理好后,觉得并没有必要向组长和经理进行汇报,就把这件事给over掉了,也就是V_Programmer.onTouchEvent()返回true

修改代码:

经理代码:

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e(TAG, "&&&VG_Manager.dispatchTouchEvent ---> 被老板喊来修电脑");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.e(TAG, "&&&VG_Manager.onInterceptTouchEvent ---> 听了老板描述问题后,喊组长过来");
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "&&&VG_Manager.onTouchEvent --->经理把组长喊来,任务完成");
        return false;
    }

组长代码:

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e(TAG, "&&&VG_GroupLoader.dispatchTouchEvent ---> 组长被经理喊来给老板修电脑");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.e(TAG, "&&&VG_GroupLoader.onInterceptTouchEvent ---> 组长觉得老板的电脑问题太大,喊来程序员");
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "&&&VG_GroupLoader.onTouchEvent ---> 组长安排给程序员,任务完成");
        Log.e(TAG, "&&&VG_GroupLoader.onTouchEvent ---> 默认"+super.onTouchEvent(event));
        return false;
    }
    

程序员的代码:

 @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.e(TAG, "&&&V_Programmer.dispatchTouchEvent ---> 程序员被喊来给老板修电脑");
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "&&&V_Programmer.onTouchEvent ---> 程序员给老板重装系统,修好,奖金15元");
        return true;
    }

再次点击程序员Log信息

程序员修电脑

这里我出了点问题,实际打印结果将上面的Log信息完整重复打印了3遍,有时候4遍,我查找了一会也没查出来为啥,有知道我哪里出错的同学,请告诉我 : )

2016.11.09补充
打印3,4次的原因知道了,因为没有对事件类型进行判断,MotionEvent中并不是只有DOWN一个事件,加上类型判断就会只打印一次了

此时的事件流程图

程序员给老板修电脑

3.一些结论 <p>

这些结论摘自Android开发艺术探索

  • 同一个序列从手指落在屏幕开始,以down事件开始,中间数量不定的move事件,最终以up事件结束,整个过程都是一个事件
  • 一般,一个事件只能由一个View消费
  • 一个View(V)对事件进行了拦截,该事件只能由这个V来消费
  • 某个View(V)(不是ViewGroup)一旦开始处理事件,如果它不消费ACTION_DOWN事件,也就是onTouchEvent()返回false,那么同一事件序列的其他事件都不会交给这个V消费,并且事件将重新交给V的父容器的onTouchEvent()进行消费
  • 某个View(V)不消耗除ACTION_DOWN以外的其他事件,这个点击事件便会消失,此时父元素的onTouchEvent()不会被调用,并且V可以持续收到后续事件,最终这些消失的点击事件会传递给Activity处理。ps:这条并不理解到底啥意思
  • ViewGroup(VG)默认不会拦截任何事件,VG.onInterceptTouchEvent()默认返回false
  • View(V)没有onIntercepTouchEvent(),一旦点击事件传递给VV.onTouchEvent()便会消费这个事件
  • View(V)onTouchEvent()默认都会消耗事件,返回true(ps:这里有疑问,我测试返回为false)。除非V是不可不可点击的(clickablelongClickable同时为false)。V的的longClickable默认都为falseclickable要看控件,ButtonclickabletrueTextViewfalse
  • View(V)enable属性不影响onTouchEvent的默认返回值。哪怕Vdisable状态,只要Vclickable或者longClickable有一个返回为trueVonTouchEvent就返回true
  • onClicck进行回调前提是View是可以点击的,并且收到了downup事件
  • 事件的传递是由外向内的,事件总是先传递给父容器,然后父容器向下传递。通过requestDisallowInterceptTouchEvent方法,可以在childView中干预父容器事件的分发过程,但ACTION_DOWN事件除外

这里后面几条并不是很理解。事件分发的源码,也就在Android开发艺术探索中大体看了看


4.最后 <p>

又是十一,记得去年十一找同学开始学习Android做一些小的Demo,经过一年的学习,感觉也算入门了 : )

国庆快乐

本人很菜,有错误,请指出

共勉 : )

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,406评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,976评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,302评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,366评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,372评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,457评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,872评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,521评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,717评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,523评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,590评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,299评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,859评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,883评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,127评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,760评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,290评论 2 342

推荐阅读更多精彩内容