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()方法来处理。

《Android 自定义View学习(十三)——View触控事件学习》 分发拦截方法

截图来自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>

运行效果也很简单

《Android 自定义View学习(十三)——View触控事件学习》 运行效果

点击程序员,查看Log信息

《Android 自定义View学习(十三)——View触控事件学习》 点击程序员
《Android 自定义View学习(十三)——View触控事件学习》 点击程序员事件过程

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

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信息:

《Android 自定义View学习(十三)——View触控事件学习》 经理给老板修电脑

《Android 自定义View学习(十三)——View触控事件学习》 经理给老板修好电脑

组长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信息

《Android 自定义View学习(十三)——View触控事件学习》 程序员修电脑

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

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

此时的事件流程图

《Android 自定义View学习(十三)——View触控事件学习》 程序员给老板修电脑

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,经过一年的学习,感觉也算入门了 : )

国庆快乐

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

共勉 : )

    原文作者:英勇青铜5
    原文地址: https://www.jianshu.com/p/eb21fc5ebf4e
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞