37、Android手势识别

手势识别

前面已经讲解了Android事件传递和自定义控件相关的知识,接下来介绍Android中手势的控制。说到触摸事件,不得不提的就是MotionEvent。

MotionEvent

事件坐标

每个触摸事件都代表用户在屏幕上的一个动作,而每个动作必定有其发生的位置。在MotionEvent中就有一系列与标触摸事件发生位置相关的函数:

  • getX()和getY():这两个函数获得的x、y值是相对的坐标值,相对于消费这个事件的视图的左上点的坐标。
  • GetRawX()和getRawY():这两个函数获得的x、y值是绝对坐标,是相对于屏幕的。

具体如下图所示:

事件类型

Android用一个32位的整型值表示一次TouchEvent事件,低8位表示Touch事件的具体动作。

Android动作都封装在TouchEvent中,其中Action有两种,它们分别如下所示:

getAction():触摸动作的原始32位信息,包括事件的动作,触控点信息。

getActionMask():触摸的动作、按下、抬起、滑动、多点按下、多点抬起。

分别对应相应的常量,系统内置了很多种事件常量,它们分别如下所示:

MotionEvent.ACTION_DOWN:当屏幕检测到第一个触点按下之后就会触发到这个事件。
MotionEvent.ACTION_MOVE:当触点在屏幕上移动时触发,触点在屏幕上停留也是会触发的,主要是由于它的灵敏度很高。
MotionEvent.ACTION_POINTER_DOWN:当屏幕上已经有触点处于按下的状态的时候,再有新的触点被按下时触发。
MotionEvent.ACTION_POINTER_UP:当屏幕上有多个点被按住,松开其中一个点时触发(即非最后一个点被放开时)触发。
MotionEvent.ACTION_UP:当触点松开时被触发。
MotionEvent.ACTION_OUTSIDE: 表示用户触碰超出了正常的UI边界.
MotionEvent.ACTION_SCROLL:android3.1引入,非触摸滚动,主要是由鼠标、滚轮、轨迹球触发。
MotionEvent.ACTION_CANCEL:当用户保持按下操作,并从你的控件转移到外层控件时触发。

最主要的包括:按下、移动、抬起。

MotionEvent.ACTION_DOWN:当屏幕检测到第一个触点按下之后就会触发到这个事件。

MotionEvent.ACTION_MOVE:当触点在屏幕上移动时触发,触点在屏幕上停留也是会触发的,主要是由于它的灵敏度很高。

MotionEvent.ACTION_UP:当触点松开时被触发。

代码如下所示:

int action = MotionEvent.getAction(event);
switch(action) {
    case MotionEvent.ACTION_DOWN:	// 当屏幕检测到第一个手指按下之后就会触发到这个事件
        break;
    case MotionEvent.ACTION_MOVE:	// 手指在屏幕移动或抖动时执行的事件,会多次执行
        break;
    case MotionEvent.ACTION_UP:		// 手指抬起时执行的事件
        break;        
}

触点个数

为了可以表示多个触摸点的动作,MotionEvent中引入了Pointer的概念,一个pointer就代表一个触摸点,每个pointer都有自己的事件类型,也有自己的横轴坐标值。

getActionIndex():多点触控获取经过掩码和平移后的索引。
getPointerId(id):对于每个触控的点的细节,我们可以通过一个循环执行getPointerId方法获取索引。

一个MotionEvent对象中可能会存储多个pointer的相关信息,每个pointer都会有一个自己的id和index。pointer的id在整个事件流中是不会发生变化的,但是index会发生变化。

由于pointer的index值在不同的MotionEvent对象中会发生变化,但是id值却不会变化。所以,当我们要记录一个触摸点的事件流时,就只需要保存其id,然后使用findPointerIndex(int)来获得其index值,然后再获得其他信息。

Index变化

假设我们现在按下一只手指,然后再按下另一只手指,再把其中一只手指抬起,最后抬起另外一只手指,代码如下:

@Override
public boolean onTouchEvent(MotionEvent event) {
    int actionMasked = event.getActionMasked();
    int index = event.getActionIndex();
    switch (actionMasked){
        case MotionEvent.ACTION_DOWN:
            Log.e(TAG, "ACTION_DOWN:" + index);
            break;
        case MotionEvent.ACTION_POINTER_DOWN:
            Log.e(TAG, "ACTION_POINTER_DOWN:" + index);
            break;
        case MotionEvent.ACTION_POINTER_UP:
            Log.e(TAG, "ACTION_POINTER_UP:" + index);
            break;
        case MotionEvent.ACTION_MOVE:
            //Log.e(TAG, "ACTION_MOVE" + index);
            break;
        case MotionEvent.ACTION_UP:
            Log.e(TAG, "ACTION_UP:" + index);
            break;
    }
    return super.onTouchEvent(event);
}

打印的日志信息如下所示:

2021-07-06 02:29:33.331 31441-31441/com.legend.touch E/MainActivity: ACTION_DOWN:0
2021-07-06 02:29:34.687 31441-31441/com.legend.touch E/MainActivity: ACTION_POINTER_DOWN:1
2021-07-06 02:29:36.975 31441-31441/com.legend.touch E/MainActivity: ACTION_POINTER_UP:0
2021-07-06 02:29:39.149 31441-31441/com.legend.touch E/MainActivity: ACTION_UP:0

从结果可以看出,当第二只手指按下的时候,index是1,但是因为相继抬起,index就变为0了。(所以,Index的值会根据在屏幕上的手指个数而变化)

ID不变

假设我们现在按下一只手指,然后再按下另一只手指,再把其中一只手指抬起,最后抬起另外一只手指,代码如下:

@Override
public boolean onTouchEvent(MotionEvent event) {
    int actionMasked = event.getActionMasked();
    int pointerId = event.getPointerId(event.getActionIndex());
    switch (actionMasked){
        case MotionEvent.ACTION_DOWN:
            Log.e(TAG, "ACTION_DOWN:" + pointerId);
            break;
        case MotionEvent.ACTION_POINTER_DOWN:
            Log.e(TAG, "ACTION_POINTER_DOWN:" + pointerId);
            break;
        case MotionEvent.ACTION_POINTER_UP:
            Log.e(TAG, "ACTION_POINTER_UP:" + pointerId);
            break;
        case MotionEvent.ACTION_MOVE:
            //Log.e(TAG, "ACTION_MOVE" + index);
            break;
        case MotionEvent.ACTION_UP:
            Log.e(TAG, "ACTION_UP:" + pointerId);
            break;
    }
    return super.onTouchEvent(event);
}

打印的日志信息如下所示:

2021-07-06 02:36:38.855 2006-2006/com.legend.touch E/MainActivity: ACTION_DOWN:0
2021-07-06 02:36:40.423 2006-2006/com.legend.touch E/MainActivity: ACTION_POINTER_DOWN:1
2021-07-06 02:36:42.159 2006-2006/com.legend.touch E/MainActivity: ACTION_POINTER_UP:1
2021-07-06 02:36:43.662 2006-2006/com.legend.touch E/MainActivity: ACTION_UP:0

从结果可以看出,id的值并没有发生变化,所以在开发中要使用id来作为依据。

禁用多点触控

在开发中一般情况下不会使用到多点触控的情况,如果想屏蔽多点触控只需要在需要屏蔽的控件的的直接父容器使用如下属性:

android:splitMotionEvents="false"

如果想针对整个Activity中禁用多点触控,只需要在theme.xml中修改主题即可:

<style name="xxxxxx" parent="AppTheme.NoActionBar">
	...
    <item name="android:windowEnableSplitTouch">false</item>
    <item name="android:splitMotionEvents">false</item>
</style>

GestureDetector手势

当用户触摸屏幕的时候,会产生许多手势,例如down,up,scroll,filing等等。

一般情况下,我们知道View类有个View.OnTouchListener内部接口,通过重写他的onTouch(View v, MotionEvent event)方法,我们可以处理一些touch事件。

如果需要处理一些复杂的手势,用这个接口就会很麻烦,Android给我们提供了GestureDetector类,通过这个类我们可以识别很多的手势,主要是通过他的onTouchEvent(event)方法完成了不同手势的识别。

GestureDetector这个类对外提供了两个接口和一个内部类:

OnGestureListener、OnDoubleTapListener、SimpleOnGestureListener

OnGestureListener

OnGestureListener提供了六个函数,它们分别如下:

private class MyGestureListener implements GestureDetector.OnGestureListener{
    @Override
    public boolean onDown(MotionEvent e) {
        // 用户按下屏幕就会触发
        return false;
    }

    @Override
    public void onShowPress(MotionEvent e) {
        // 如果是按下的时间超过瞬间,而且在按下的时候没有松开或者是拖动的,那么onShowPress就会执行
        // 执行顺序:onDown->onShowPress->onLongPress
    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        // 一次单独的轻击抬起操作,也就是轻击一下屏幕,立刻抬起来,才会有这个触发
        // 点击一下非常快的(不滑动)Touchup:onDown->onSingleTapUp->onSingleTapConfirmed 
        // 点击一下稍微慢点的(不滑动)Touchup:onDown->onShowPress->onSingleTapUp->onSingleTapConfirmed
        return false;
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        // 在屏幕上拖动事件。无论是用手拖动view,或者是以抛的动作滚动,都会多次触发,这个方法
        // 滑屏:手指触动屏幕后,稍微滑动后立即松开: onDown->onScroll->onScroll->………->onFling
        // 拖动:onDown->onScroll->onScroll->onFiling
        return false;
    }

    @Override
    public void onLongPress(MotionEvent e) {
        // 长按触摸屏,超过一定时长,就会触发这个事件
    }

    /**
      * @param e1    第1个ACTION_DOWN MotionEvent
      * @param e2    最后一个ACTION_MOVE MotionEvent
      * @param velocityX    X轴上的移动速度,像素/秒
      * @param velocityY    Y轴上的移动速度,像素/秒 
    */
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        // 滑屏,用户按下触摸屏、快速移动后松开  
        return false;
    }
}

然后创建自定义的手势类来使用:

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    private GestureDetector mGestureDetector;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mGestureDetector = new GestureDetector(this, new MyGestureListener());
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return mGestureDetector.onTouchEvent(event);
    }
}    

OnDoubleTapListener

OnDoubleTapListener提供了三个函数:

private class MyDoubleTapListener implements GestureDetector.OnDoubleTapListener{
    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) {
        // 单击事件,用来判定该次点击是SingleTap而不是DoubleTap
        // 触发顺序是:OnDown->OnSingleTapUp->OnSingleTapConfirmed
        return false;
    }

    @Override
    public boolean onDoubleTap(MotionEvent e) {
        // 双击事件
        return false;
    }

    @Override
    public boolean onDoubleTapEvent(MotionEvent e) {
        // 双击间隔中发生的动作
        // 触发顺序是:先触发OnDoubleTap,然后再触发OnDown
        return false;
    }
}

然后创建自定义的手势类来使用,通过setOnDoubleTapListener方法设置:

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    private GestureDetector mGestureDetector;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mGestureDetector = new GestureDetector(this, new MyGestureListener());
        mGestureDetector.setOnDoubleTapListener(new MyDoubleTapListener());
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return mGestureDetector.onTouchEvent(event);
    }
}    

SimpleOnGestureListener

OnGestureListener和OnDoubleTapListener接口里的函数都是强制必须重写的,即使用不到也要重写出来一个空函数但在SimpleOnGestureListener类的实例或派生类中不必如此,可以根据情况,用到哪个函数就重写哪个函数,因为SimpleOnGestureListener类本身已经实现了这两个接口的所有函数,只是里面全是空的而已。

private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener {
    @Override
    public boolean onDown(MotionEvent e) {
        return super.onDown(e);
    }

    @Override
    public void onShowPress(MotionEvent e) {
        super.onShowPress(e);
    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return super.onSingleTapUp(e);
    }

    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) {
        return super.onSingleTapConfirmed(e);
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        return super.onScroll(e1, e2, distanceX, distanceY);
    }

    @Override
    public void onLongPress(MotionEvent e) {
        super.onLongPress(e);
    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        return super.onFling(e1, e2, velocityX, velocityY);
    }

    @Override
    public boolean onDoubleTap(MotionEvent e) {
        return super.onDoubleTap(e);
    }

    @Override
    public boolean onDoubleTapEvent(MotionEvent e) {
        return super.onDoubleTapEvent(e);
    }
}

至此,手势相关的讲解到此结束。

原文地址:https://www.cnblogs.com/pengjingya/p/14961630.html