第十课Android界面事件梳理1:界面事件分类、触屏动作事件之单点触屏

  同学们, 在一次课中我们在讲输入与输出组件时说到了,程序与用户的交互过程中,用户对程序的输入除了输入文字之外,还通过屏幕的触摸、滑动、拖动、长按等手势给程序输入信息。当然手机相对于PC机更进步的地方是除了键盘鼠标之外还集成了很多传感器,大家熟悉的倾斜手机、摇一摇、指纹识别、面部识别等都是通过传感器来对手机进行输入。另外还有使用摄像头的扫一扫,使用话筒的语音指令,这些也是手机上常见的用户输入方式。我把传感器、摄像头和话筒的输入方式放到后面,这里只讲通过界面的输入。

一、界面事件分类

  当用户通过界面的输入发生时,Android系统会立即反应,把相关输入信息封装成事件(Event)派发(dispatch)给界面组件。界面组件类从基类View开始就定义了一系列的事件处理方法与回调方法用于处理多种事件,这些界面事件按发生源分为4类。包括:

  • 触屏动作事件。包括Touch与Drag事件,分别在触摸屏幕与进行拖放(Drag and drop)手势时触发。

  • 键盘输入事件。包括KeyDown、KeyLongPress、KeyMultiple和KeyUp事件,在按键按下、按键长按、多键模拟轨迹球(现在基本不用了)和按键弹起时触发。

  • 焦点改变事件。包括FocusChanged与WindowFocusChanged事件,在视图及视图所在窗口获得或失去焦点时触发。

  • 轨迹球运动事件。包括Trackball事件,在发生轨迹球运动时触发。(这个不常用了,我们在开发时一般不用管)

  下面我们挨个对这些事件进行介绍。

二、触屏动作事件

1. Touch事件及由其触发的3种回调

  我们一个个地来分析,Touch事件包含了所有触摸屏幕相关的动作,但是我们平时更熟悉的点击、长按等手势,以及拖动、快滑、缩放和拖放等手势又是在哪呢?这是因为调用onTouchEvent方法时传入的MotionEvent对象对这些手势动作信息进行了封装。在View对Touch事件进行处理的过程中,分化成了onTouch(触摸)、onClick(点击)与onLongClick(长按)3种回调。View的调用者要对它们进行处理,就要通过View的setOnTouchListener、setOnClickListener和setOnLongClickListener3个方法来设置监听者对象。

  也就是说,Touch事件经过onTouchEvent方法的处理后分化为3个回调。3个回调中,onClick与onLongClick对应点击与长按两种具体的手势,而onTouch则是把Touch事件包含的触屏动作信息进行直接的传递,需要我们进一步进行分化整理,对应成具体的手势再进行处理。

下面的图可以帮助进行理解:

大家可以看到屏幕派发Touch事件给View,View把这个事件进行分化,分别通过3个监听者调用3个回调方法。调用者可以选择实现这3种回调方法,如果要处理的话需要先通过对应的注册方法设置监听者对象。

  那这3种回调有什么区别呢?View的Touch回调直接把屏幕Touch事件时传来的MotionEvent对象直接转给了调用者,所以它们是一回事。Click与LongClick这个容易区分,但是Touch回调和Click回调之间怎么区分呢?

  我们用一个示例来说明。我们新建一个Activity,名叫UIEventTest,在它的正中央放一个ImageView,id为imgLogo。在Java文件中,我们在onCreate方法下添加如下代码:

final ImageView imgLogo = (ImageView)findViewById(R.id.imgLogo);imgLogo.setOnTouchListener(new View.OnTouchListener() {    @Override    public boolean onTouch(View v, MotionEvent event) {        Log.i("uieventtest", "onTouch");        return false;    }});imgLogo.setOnLongClickListener(new View.OnLongClickListener() {    @Override    public boolean onLongClick(View v) {        Log.i("uieventtest", "onLongClick");        return false;    }});imgLogo.setOnClickListener(new View.OnClickListener() {    @Override    public void onClick(View v) {        Log.i("uieventtest", "onClick");    }});

  运行效果如下:

  在界面的图片上长按,得到输出结果如下:

I/uieventtest: onTouch

I/uieventtest: onLongClick

I/uieventtest: onTouch

I/uieventtest: onClick

  大家可以看到,在屏幕上一次长按的过程中,onTouch方法被调用了2次,一次是在LongClick发生之前,一次是在LongClick发生之后。而在LongClick发生之后,Click也发生了。要想明白为什么onTouch方法被调用2次,需要对它的MotionEvent对象参数进行分析。我们对OnTouch方法进行如下修改:

@Overridepublic boolean onTouch(View v, MotionEvent event) {    switch (event.getAction()){        case MotionEvent.ACTION_DOWN:            Log.i("uieventtest", "onTouch ACTION_DOWN");            break;        case MotionEvent.ACTION_UP:            Log.i("uieventtest", "onTouch ACTION_UP");            break;    }    return false;}

  再次运行后长按,获得如下输出:

I/uieventtest: onTouch ACTION_DOWN

I/uieventtest: onLongClick

I/uieventtest: onTouch ACTION_UP

I/uieventtest: onClick

  现在可以看出第一次调用onTouch是因为ACTION_DOWN,也就是手指碰上屏幕。在接触上一定时间后,LongClick发生,onLongClick方法被调用。在手指离开屏幕后是ACTION_UP的情况,onTouch方法再次被调用。之后才是Click发生,调用onClick方法。也就是说手指接触屏幕,触发一次Touch事件,手指离开屏幕再触发一次Touch事件。Click的发生时间点是手指点击完成并离开屏幕之后,而且是在第二次Touch事件之后。

2. 用GestureDetector检测常用手势

  Touch事件其实是包含了很多可以分解的手势动作在里面的。用户在屏幕上的触摸动作的信息被封装在View类的onTouchEvent方法和监听者对象onTouch方法的MotionEvent对象里,调用这个对象的getAction方法,可以获得手势动作的具体信息,分为以下类别:MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE, MotionEvent.ACTION_OUTSIDE, MotionEvent.ACTION_POINTER_DOWN, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_UP, MotionEvent.EDGE_BOTTOM, MotionEvent.EDGE_LEFT, MotionEvent.EDGE_RIGHT, MotionEvent.EDGE_TOP。我们熟悉的手势(按下、弹起、移动、双击、长按、滑动、滚动、缩放等)就是由这些信息的单个或多个组合来判断的。比如,MotionEvent.ACTION_UP就是Click,MotionEvent.ACTION_DOWN之后维持一定时长没有发生MotionEvent.ACTION_UP,就是LongClick。那么

答案是必须有啊。这个类就是GestureDetector(Gesture手势,Detector检测器),这类的onTouchEvent方法就是用来处理Touch事件的,在onTouch回调方法中通过GestureDetector对象调用它的onTouchEvent方法,就会通过OnGestureListener和OnDoubleTapListener两个监听者分化为9种回调,如下图所示:

从这个图中可以看出GestureDetector对象对Touch事件进行深度分析,通过OnGestureListener与OnDoubleTapListener两个监听者(这2个监听者分别通过构造方法与setOnDoubleTapListener方法来设置)分化为9种回调方法。他们的区别为:

  • onDown(MotionEvent):用户按下屏幕就被回调。

  • onShowPress(MotionEvent):如果是按下的时间超过瞬间,而且在按下的时候没有松开或者是拖动的,说明只是单纯的按下时,就被回调。

  • onLongPress(MotionEvent):长按触摸屏,超过一定时长(比onShowPress时间更长),就会被回调。

  以上3个方法被调用的时序为:

onDown->onShowPress->onLongPress

  • onSingleTapUp(MotionEvent):一次单独的轻触后抬起,也就是轻触一下屏幕,立刻抬起来以后被回调。如果在Down以后还有其它动作,那就不再算是SingleTapUp了,也就不会被回调。

  • onSingleTapConfirmed(MotionEvent):单独轻触确认,用来判定该次轻触是SingleTap而不是DoubleTap。如果一次SingleTap后一段时间后没有第二次SingleTap,则为SingleTapConfirmed,onSingleTapConfirmed被调用。反之,一次SingleTap后不久又出现一次SingleTap,这就是DoubleTap手势,这种情况onSingleTapUp不会被调用,onSingleTapConfirmed也不会被调用。 

  非常快地触屏(不滑动)时方法被调用的时序为:

onDown->onSingleTapUp->onSingleTapConfirmed 

  • onDoubleTap(MotionEvent):连续两次触屏。前面说了短时间内两次SingleTap即为DoubleTap,第二次触屏手指按下后,调用onDoubleTap方法。

  连续两次触屏时方法被调用时序为:

onDown->onSingleTapUp->onDoubleTap

  • onDoubleTapEvent(MotionEvent):连续两次触屏,第二次触屏从手指按下到抬起之间发生的事件,包括1个ACTION_DOWN, 0到多个ACTION_MOVE和1个ACTION_UP。

  连续两次触屏时方法被调用时序为:

onDown->onSingleTapUp->onDoubleTap->onDoubleTapEvent->onDoubleTapEvent…->onDoubleTapEvent

  • onScroll(MotionEvent, MotionEvent,float, float):官方解释是拖动,指用户在触摸屏上拖动手指时发生的一种滚动。实际是手指接触屏幕后不离开并移动时,onScroll方法会不断被调用。

  • onFling(MotionEvent, MotionEvent, float, float) :官方解释是滑动,指用户快速拖动并抬起手指时发生的一种滚动。实际是手指接触屏幕后不离开并移动(移动速度要够快,这个值与系统配置有关),手指离开时onFling方法被调用。

  以滑屏手势浏览图片时方法被调用的时序为:

onDown->onScroll->onScroll…->onFling

  以上各种方法在实际使用中根据不同的目的来选择重写:

  • 单击:选择onSingleTapUp。

  • 长按:选择onLongPress。

  • 展示按下效果:选择onShowPress()——该方法名中有ShowPress,表明它是展示按下效果的最好时机,可以在该方法中给视图切换为按下时的图片。不能在该方法中处理单击事件,因为有可能会发展成LongPress。

  • 拖动:选择onScroll。

  • 快速滑动:选择onFling。

  • 双击:选择onDoubleTap。

  可以看到,单个手指在屏幕上的手势都被GestureDetector类给整理好了,我们只要重写这些方法就可以对用户的手势进行处理了。同时又因为实现开发中我们不需要对所有的手势都进行处理,Android SDK贴心地提供了一个GestureDetector.SimpleOnGestureListener类,这个类实现了OnGestureListener与OnDoubleTapListener这2个接口,虽然都是空实现,但我们只需要继承这个类,再挑选需要处理的手势重写对应的回调方法就可以了。重写的回调方法如果是返回boolean值的,返回true是代表这个事件传递到此就处理完毕了不用再传递给别的视图(因为手指接触的位置一般会嵌套着布局、容器和输入输出组件,事件会沿着这些组件传递),返回false则是让系统继续传递。但要注意的是,onDown方法必须重写并返回true,表明对事件进行了处理。这是因为所有手势处理流程都以调用onDown方法开头。如果不重写,在SimpleOnGestureListener类的实现中是返回false,系统会认为你想要忽略其余手势,其它回调方法都不会被调用。

  现在我以手指左右滑屏切换图片内容的功能来向大家演示如何检测并处理用户手势。我们先向项目app>res>drawable中加入5张图片,分别为logo1到logo5,再在UIEventTest的Java代码中修改onCreate方法如下:

import androidx.appcompat.app.AppCompatActivity;import androidx.core.view.GestureDetectorCompat;
import android.os.Bundle;import android.view.GestureDetector;import android.view.MotionEvent;import android.view.View;import android.widget.ImageView;
public class UIEventTest extends AppCompatActivity { private GestureDetectorCompat mGestureDetector;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_u_i_event_test);
final ImageView imgLogo = (ImageView)findViewById(R.id.imgLogo);
MyGesture gestureListener = new MyGesture(imgLogo); mGestureDetector = new GestureDetectorCompat(this, gestureListener); mGestureDetector.setOnDoubleTapListener(gestureListener);
imgLogo.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { return mGestureDetector.onTouchEvent(event); } }); } private class MyGesture extends GestureDetector.SimpleOnGestureListener{ private ImageView mImg; private int[] res = {R.drawable.logo1, R.drawable.logo2, R.drawable.logo3, R.drawable.logo4, R.drawable.logo5 }; private int mIndex = 4;
public MyGesture(ImageView img){ this.mImg = img; }
@Override public boolean onDown(MotionEvent e) { return true; }
@Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (velocityX > 0 ){// 向右滑屏 this.mIndex--; if(this.mIndex < 0) this.mIndex = 4;
this.mImg.setImageResource(this.res[this.mIndex]); }else{// 向左滑屏 this.mIndex++; if(this.mIndex > 4) this.mIndex = 0;
this.mImg.setImageResource(this.res[this.mIndex]); }
return true; } }}

  在上面的代码中用MyGesture继承GestureDetector.SimpleOnGestureListener类,重写了onDown与onFling方法。onDown方法中只是返回了true,在onFling中对左右滑屏进行了区分并分别进行了ImageView图片内容的设置。Android SDK推荐用GestureDetectorCompat类代替GestureDetector类,这里改用它的实例。分别在GestureDetectorCompat的构造方法与setOnDoubleTapListener中给它设置GestureListener和DoubleTapListener(这里实际上没有回调DoubleTapListener中的方法,只是作为一个示例)。在对ImageView重写的onTouch方法中调用了GestureDetectorCompat对象的onTouchEvent,这样手势的检测就全部委托给了GestureDetectorCompat对象。运行效果如下:

  我们还可以通过重写onScroll方法来实现手指拖动时的图片跟随。代码如下:

private class MyGesture extends GestureDetector.SimpleOnGestureListener{    private ImageView mImg;    private int[] res = {R.drawable.logo1, R.drawable.logo2, R.drawable.logo3,            R.drawable.logo4, R.drawable.logo5 };    private int mIndex = 4;    private float mDensity;
public MyGesture(ImageView img){ this.mImg = img;
// 获得当前屏幕密度 Resources resources = this.mImg.getResources(); DisplayMetrics dm = resources.getDisplayMetrics(); this.mDensity = dm.density; }
@Override public boolean onDown(MotionEvent e) { return true; }
@Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { mImg.offsetLeftAndRight((int)-(distanceX/this.mDensity)); mImg.offsetTopAndBottom((int)-(distanceY/this.mDensity)); return true; }}

  这个功能的实现很简单,就是在onScroll方法中分别调用View对象的offsetLeftAndRight和offsetTopAndBottom来实现在X轴与Y轴的位移,而distanceX与distanceY正好是每次手指移动的距离,但是要取个反不然移动方向就错了。同时由于这2个方法要求输入的数值单位是像素,而distanceX与distanceY是经过屏幕密度放大的像素值,需要除掉密度值。前面我们在21点游戏编程时介绍过屏幕密度相关的知识,那时的密度值是通过放大值与原始值相除获得的。这里我们改用更规范的方法,通过Resources类的getDisplayMetrics方法来获得。为了效果更明显,我把图片缩小了一半。运行效果如下:

  通过前面的讲解,我们就把单个手指在单个视图上的手势动作事件如何处理讲完了。这里同学们可能还有疑问:不能说包括了所有的手势吧?还有缩放与拖放啊。这是因为缩放是多个手指在单个视图的手势,而拖放是单个手指在2个视图之间的手势。


跟陶叔学编程第一季 零基础学习Android开发

第一课 第一个Android程序(1)

第一课 第一个Android程序(2)

第二课 Java语言基础1(1)

第二课 Java语言基础1(2)

第三课 Java语言基础2-1

第三课 Java语言基础2-2

第四课 Java语言基础3-1

第四课 Java语言基础3-2

第五课 类与面向对象编程1

第五课 类与面向对象编程1-2

第五课 类与面向对象编程1-3

第六课 类与面向对象编程2-1

第六课 类与面向对象编程2-2

第六课 类与面向对象编程2-3

第七课 JDK的使用1

第七课 JDK的使用2

第七课 JDK的使用3

第七课 JDK的使用4

第八课 Android界面编程1-1:界面设计、XML表示、布局组件

第八课 Android界面编程1-2:代码控制组件、尺寸单位

第八课 Android界面编程1-3:代码生成界面组件并设置位置 使用ConstrainLayout约束布局组件进行组件位置设置

第八课 Android界面编程1-4:完整21点扑克游戏、界面交互、对话框

第八课 Android界面编程1-5:游戏配置、横竖屏切换、生成签名apk文件

第九课 Android界面编程2-1:界面组件总体介绍、Activity

第九课 Android界面编程2-2:Layouts(布局)和Containers(容器)组件:CardView

第九课 Android界面编程2-3:Layouts(布局)和Containers(容器)组件:Spinner

第九课 Android界面编程2-4:Layouts(布局)和Containers(容器)组件:RecyclerView

第九课 Android界面编程2-5:布局与容器的实现原理、输入与输出组件

文章转载自微信公众号:跟陶叔学编程

类似文章