Android 中 View 的手势事件处理

View 作为Android中最直接的和用户进行交互的单位,对于 View 的事件处理重要程度自然不言而喻,View 的事件处理直接影响到用户体验,下面我们来看一下对 View 的触摸事件的处理:

首先,View 的源代码中已经给我们写了一个 onTouchEvent 方法用于处理最直接的触摸事件,我们可以在官方文档中看到对这个方法的介绍:

public boolean  onTouchEvent (MotionEvent event) 


Added in API level 1



Implement this method to handle touch screen motion events. 

If this method is used to detect click actions, it is recommended that the actions be performed by implementing and calling performClick(). This will ensure consistent system behavior, including: 
•obeying click sound preferences 
•dispatching OnClickListener calls 
•handling ACTION_CLICK when accessibility features are enabled 



Parameters


event 
The motion event. 


Returns
True if the event was handled, false otherwise. 

大致意思是:实现这个方法去处理屏幕的触摸事件,如果这个方法用于处理单击事件,它将会:播放单击事件的声音,回调OnClickListener 接口的方法,如果可能的话处理单击动作。 简答来说就是我们可以在这个方法中处理当前 View 的触摸事件(单击事件也是一种触摸事件)。 方法的参数是一个 MotionEvent 类,用于储存当前触摸事件的信息,我们可以利用这些信息达到我们想要的效果。

接下来介绍一个配合这个方法使用的类:VelocityTracker(速度追踪类),这个类用于获取触摸移动的时候的速度,一般来说,我们会在 onTouchEvent 中使用这个类,先看看官方文档的说明:

文档里面提到的方法已经可以完成一些基本的需求,这里解释一下里面提到的方法的作用:

//// 通过静态方法实例化这个类的一个对象
VelocityTracker velocityTracker = VelocityTracker.obtain();
// 设置这个类要检测的触摸事件对象
velocityTracker.addMovement(event);
// 设置计算速度的时间间隔(毫秒)
velocityTracker.computeCurrentVelocity(1000);
/*
 * 获取在上一个设置的时间间隔(这里是1000ms)内这个检测的触摸事件在 X 方向和 Y 方向上移动的距离,
 * 那么就可以根据移动的距离和时间间隔算出速度
 */
 // 获取 x 方向上的移动速度
velocityTracker.getXVelocity();
 // 获取 y 方向上的移动速度
velocityTracker.getYVelocity();

下面我们通过一个小例子来具体的看一下怎么使用,假设我们要在屏幕上自由的移动手指,并且随时把手指的坐标和在 X 、Y 方向上的移动速度显示出来。新建一个 Android 工程:

activty_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    tools:context="com.example.administrator.viewontouchevent.MainActivity"
    android:orientation="vertical">

    <TableRow
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
        <TextView
            android:id="@+id/showXYTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="坐标:"
            android:background="#3300ff00"/>
        <TextView
            android:id="@+id/showVelocityTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="速度:"
            android:background="#220000ff"/>
    </TableRow>


</LinearLayout>

主布局文件中有两个 TextView 控件成一行排列,分别用于显示当前手指的坐标和 X 、 Y 方向上的移动速度。接下来,我们要自定义一个控件,然后重写 onTouchEvent 方法用于检测我们的手指移动的触摸信息并且传递给两个 TextView 控件。

但是在这里我们仔细思考一下:我们重写的 onTouchEvent 方法的参数和返回值都是固定的,不允许我们更改,那么我们该怎么将里面的数据传递出去呢?其实利用java中的回调机制就可以很好地解决这个问题: 我们新建一个自定义接口 GetInfFromMotionEvent:

/**
 * Created by Administrator on 2017/2/27.
 */

public interface GetInfFromMotionEvent {


    public void getX(float x);

    public void getY(float y);

    public void getXVelocity(float xVelocity);

    public void getYVelocity(float yVelocity);
}

在这个自定义接口中我们写了四个抽象方法分别用于获取当前触摸事件的触摸点坐标和 X 、 Y 方向上的速度。这个接口我们要在 MainActivity.java 中实现并且重写里面的四个方法,接下来我们先看我们自定义的View: CustomView.java:

import android.content.Context;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;

/**
 * Created by Administrator on 2017/2/27.
 */

public class CustomView extends View {

    private VelocityTracker velocityTracker = VelocityTracker.obtain();
    // 这个接口用于随时获取触摸事件相关信息并返回(坐标、速度)
    private GetInfFromMotionEvent getInfFromMotionEvent = null;

    public void init(Context context, GetInfFromMotionEvent getInfFromMotionEvent) { // 初始化
        this.getInfFromMotionEvent = getInfFromMotionEvent; // 初始化自定义触摸事件获取接口
    }

    public CustomView(Context context, GetInfFromMotionEvent getInfFromMotionEvent) { // 构造方法
        super(context);
        init(context, getInfFromMotionEvent);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 添加要检测的触摸事件
        velocityTracker.addMovement(event);
        // 对触摸事件正在进行的动作类型进行判断
        switch (event.getAction()) {
            // 手指正在移动:
            case MotionEvent.ACTION_MOVE:
                Log.i("onTouchEvent", "MotionEvent.ACTION_MOVE");
                /*
                 * computeCurrentVelocity 方法设定一个时间间隔(毫秒),那么此时通过 getXVelocity
                 * 方法和 getYVelocity 方法 得到的速度就是在上一个
                 * 设定的这个时间间隔里面得到的触摸移动距离
                 * 这里设定为 1 毫秒,即为上个 1 毫秒的间隔内 X 、 Y 方向上手指移动的距离
                 * 这么短的时间可以理解为瞬时速度,算的时候乘上1000就变成了单位为 “秒” 的速度
                 */
                velocityTracker.computeCurrentVelocity(1);
                getInfFromMotionEvent.getX(event.getRawX());
                getInfFromMotionEvent.getY(event.getRawY());
                getInfFromMotionEvent.getXVelocity(velocityTracker.getXVelocity()*1000);
                getInfFromMotionEvent.getYVelocity(velocityTracker.getYVelocity()*1000);
                break;
            // 手指按下:
            case MotionEvent.ACTION_DOWN:
                /*
                 * getRawX 和 getRawY 方法返回的是当前触摸点相对于屏幕左上角得到的 X 和 Y
                 * getX 和 getY 方法返回的是当前触摸点相对于当前 View 左上角得到的 X 和 Y
                 */
                Log.i("onTouchEvent", "MotionEvent.ACTION_DOWN");
                getInfFromMotionEvent.getX(event.getRawX());
                getInfFromMotionEvent.getY(event.getRawY());
                break;
            // 手指抬起:
            case MotionEvent.ACTION_UP:
                Log.i("onTouchEvent", "MotionEvent.ACTION_UP");
                break;
        }
        /*
         * 如果这个返回值为false,那么触摸事件不会继续向下传递,
         * 此时就只有 MotionEvent.ACTION_DOWN 这个动作被执行了,这个触摸事件就不由这个 View 继续处理了,
         * 也就是说这个 View 的OnTouchEvent方法对于当前的触摸事件(在这一次触摸事件手指松开之前)已经失效了
         */
        return true;
    }
}

这个自定义控件只能在代码中新建,不能再布局文件中使用,因为我们只写了一个构造方法,并且这里我们利用 GetInfFromMotionEvent 接口将得到的触摸信息传递出去,接下就是MainActivity.java:

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    private Button button = null;
    private TextView textView1 = null;
    private TextView textView2 = null;
    private String xYString = null;
    private String xYVelocity = null;

    private GetInfFromMotionEvent getInfFromMotionEvent = new GetInfFromMotionEvent() {
        @Override
        public void getX(float x) {
            xYString = "(" + x + ",  ";
        }

        @Override
        public void getY(float y) {
            xYString += y + ")";
            textView1.setText("坐标:" + xYString);
        }

        @Override
        public void getXVelocity(float xVelocity) {
            xYVelocity = "(" + xVelocity + ",  ";
        }

        @Override
        public void getYVelocity(float yVelocity) {
            xYVelocity += yVelocity + ")";
            textView2.setText("速度: " + xYVelocity);
        }
    };


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView1 = (TextView) findViewById(R.id.showXYTextView);
        textView2 = (TextView) findViewById(R.id.showVelocityTextView);
        // 新建我们自定义的控件对象
        CustomView customView = new CustomView(this, getInfFromMotionEvent);
        LinearLayout linearLayout = (LinearLayout) findViewById(R.id.activity_main);
        // 设置自定义控件对象的参数
        customView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT));
        // 用代码将自定义控件加入布局中
        linearLayout.addView(customView);

    }

}

在 MainActivity.java 中我们实现了 GetInfFromMotionEvent 接口,在里面对应的方法中更新两个 TextView 控件中显示的数据,分别为更新坐标和 X、Y 方向上的速度,并且新建自定义 View 的时候将这个接口对象传入,这样的话只要坐标和速度一经更新,我们就可以通过这个接口中的方法同步更新TextView 控件中的数据显示。好了,让我们来运行一下:

Ok,成功的显示了坐标和速度。

其实,对于 View 的触摸事件的处理,Android还提供了另一个方法:通过 GestureDetector 类(手势识别)和 这个类里面的提供的3个接口和一个类,先看一下官方文档:

提供的接口有:

我们常用的两个接口是:

GestureDetector.GestureListener
GestureDetector.OnDoubleTapListener

我们下面来看一下 GestureDetector 类中提供的两个常用的接口中的方法: GestureDetector.GestureListener:

// 当控件被触摸到的一瞬间就会调用的方法,对应触摸事件动作:MotionEvent.ACTION_DOWN
        public boolean onDown(MotionEvent e) {
            return false;
        }

        // 手指触摸控件还未移动或者松开的时候(强调还未移动或者松开),对应动作:MotionEvent.ACTION_DOWN
        public void onShowPress(MotionEvent e) {
        }

        // 控件单击时候调用的方法,对应动作:MotionEvent.ACTION_DOWN --->  MotionEvent.ACTION_UP
        public boolean onSingleTapUp(MotionEvent e) {
            return false;
        }

        /*
         * 手指触摸控件并且拖动,对应动作:MotionEvent.ACTION_DOWN  --->  MotionEvent.ACTION_MOVE(多个)
         * 方法中四个参数的含义:e1:第一次调用 onScroll 方法储存的触摸信息,e2:当前触摸点的触摸信息,
         * distanceX、distanceY:上一次调用 onScroll 方法和这一次调用 Scroll 方法
         * 过程中X 方向上和 Y 方向上移动的距离
         */
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            return false;
        }

        /*
         * 控件长按的时候调用的方法,如果这个方法被调用,那么是无法识别滑动事件的,也就是说长按之后,
         * 这个触摸事件就对于这个手势监听器来说就结束了,之后只有松开之后在次触摸(重新触发触摸事件)
         * 但是如果对手势识别器(myGestureDetector)对象设置了 setIsLongpressEnabled(false) 方法之后,
         * 那么无论怎么长按当前 View 也不会回调这个方法
         */
        public void onLongPress(MotionEvent e) {
        }

        /*
         * 当产生系统识别出来的滑动事件的时候,会调用这个方法,这里值得注意的是:
         * 并不是调用了 onScroll 之后就一定会调用这个方法,这个方法对滑动的速率大小有要求,
         * 即滑动的速率达到一定大小的时候才会调用这个方法,方法参数分别代表:
         * 触摸开始点的事件信息,触摸结束(手指松开)的时候储存的事件信息,
         * 整个滑动过程 x 方向上滑动的平均速度,整个滑动过程 y 方向上滑动的平均速度。
         */
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            return false;
        }

下面是 GestureDetector.OnDoubleTapListener 接口的方法:

        /* 
        * 严格的单击行为,如果一次单击之后到系统认为双击的时间段内没发生另一次单击行为时调用,
        * 也就是说这个方法被调用了之后不可能再调用双击行为方法,注意和 onSingleTap的区别,
        * 调用顺序:onDown --> onSingleTapUp --> onSingleTapConfirmed
        */
        public boolean onSingleTapConfirmed(MotionEvent e) {
            return false;
        }

        // 双击,它不可能和 onSingleTapConfirmd 方法同时被调用
        public boolean onDoubleTap(MotionEvent e) {
            return false;
        }

        /*
         * 表示发生了双击(onDoubleTap)行为,在onDoubleTap调用的期间,发生了 MotionEvent.ACTION_DOWN
         * MotionEvent.ACTION_UP 动作都会调用这个方法
         */
        public boolean onDoubleTapEvent(MotionEvent e) {
            return false;
        }

接下来来看一下 GestureDetector 类的构造方法:

可以看到,所有的构造方法里面都需要一个 GestureDetector.OnGestureListener 接口对象作为参数。

一般来说,要使用 GestureDetector 类来检测一个 View 中的手势事件,我们会通过以下步骤:

1、新建一个 GestureDetector 对象并且设置它的手势监听器接口对象

2、这个 View 本身设置 OnTouchListener 接口并且在接口中的 onTouch 方法中设置:GestureDetector.onTouchEvent(Event event) 方法来将 View 的触摸事件交给 GestureDetector 对象去处理,GestureDetector 对象会将触摸事件交给其设置的手势监听接口处理

3、根据要求实现 GestureDetector 类中提供的手势监听接口来识别对应的触摸事件。

需要注意的是一个 View 如果要使用 GestureDetector.onDoubleTapListener 接口中的方法就一定先要实现 GestureDetector.OnGestureListener 接口,因为 GestureDetector 的构造方法中必须要有一个 GestureDetector.OnGestureListener 对象作为参数,得到 GestureDetector 对象之后调用

GestureDetector.setOnDoubleTapListener(
    GestureDetector.OnDoubleTapListener onDoubleTapListener)

方法来监听 View 的单双击事件来进行处理。

有些小伙伴可能会觉得使用 OnDoubleTapListener 接口会很麻烦,还要实现 OnGestureListener 接口,那么 GestureDetector 中还有一个类:GestureDetector.SimpleOnGestureListener,就是上图里面的最后一个类,这个类为我们重写了 GestureDetector.OnGestureListener 接口和 GestureDetector.OnDoubleTapListener 接口中的抽象方法,让我们在使用这两个接口的时候只需要重写我们需要的方法就行了,根据 Java 的多态性,这个类就是 GestureDetector.OnGestureListener 接口的对象,所以可以作为 GestureDetector 构造方法的参数,即我们可以写成这样:

gestureDetector = new GestureDetector(context, simpleOnGestureListener);

同样的步骤,将触摸事件交给 GestureDetector 对象处理:

gestureDetector.onTouchEvent(event);

对于这个类,小伙伴们可以自己尝试一下。

下面我们通过 GestureDetector 来实现上面的小例子: 只需修改 Custom.java 中的代码:

import android.content.Context;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;


public class CustomView extends View {

    // 使用这个类的对象来计算手指滑动的速度
    private VelocityTracker velocityTracker = VelocityTracker.obtain();
    // 这个接口用于随时获取触摸事件相关信息并返回(坐标、速度)
    private GetInfFromMotionEvent getInfFromMotionEvent = null;
    private GestureDetector myGestureDetector = null;

    public CustomView(Context context, GetInfFromMotionEvent getInfFromMotionEvent) {
        super(context);
        this.getInfFromMotionEvent = getInfFromMotionEvent;
        // 新建一个手势识别器,并设置它的手势监听器
        myGestureDetector = new GestureDetector(context, gestureListener);
        this.setOnTouchListener(onTouchListener); // 设置触摸事件监听对象
    }

    /*
     * 自定义触摸事件的监听对象
     */
    private View.OnTouchListener onTouchListener = new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            myGestureDetector.onTouchEvent(event); // 将触摸事件交给手势识别器去处理
            /*
             * 一定要返回 true ,这个如果返回值为false,
             * 证明这一系列的触摸事件不由该 View 处理。
             * 那么接下来的触摸事件都不会传递给该 View ,也就无法将触摸事件作为手势进行处理
             * 详细的可以搜索一下 Android 里面的触摸事件的分发和拦截机制
             */
            return true;
        }
    };

     /*
     * 新建一个手势监听器,实现手势监听接口,并重写其中的抽象方法
     */
    private GestureDetector.OnGestureListener gestureListener = new GestureDetector.OnGestureListener() {

        // 当控件被触摸到的一瞬间就会调用的方法,对应触摸事件动作:MotionEvent.ACTION_DOWN
        @Override
        public boolean onDown(MotionEvent e) {
            Log.i("OnTouchListener", "onDown");
            getInfFromMotionEvent.getX(e.getRawX());
            getInfFromMotionEvent.getY(e.getRawY());
            return false;
        }

        // 手指触摸控件还未移动或者松开的时候(强调还未移动或者松开),对应动作:MotionEvent.ACTION_DOWN
        @Override
        public void onShowPress(MotionEvent e) {
            Log.i("OnTouchListener", "onShowPress");
        }

        // 控件单击时候调用的方法,对应动作:MotionEvent.ACTION_DOWN --->  MotionEvent.ACTION_UP
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            Log.i("OnTouchListener", "onSingleTapUp");
            return false;
        }

        /*
         * 手指触摸控件并且拖动,对应动作:MotionEvent.ACTION_DOWN  --->  MotionEvent.ACTION_MOVE(多个)
         * 方法中四个参数的含义:e1:调用 onDown 方法储存的触摸点信息,e2:当前 onScroll 方法触摸点的触摸信息,
         * distanceX、distanceY:上一次调用 onScroll 方法和这一次调用 Scroll 方法
         * 过程中X 方向上和 Y 方向上移动的距离
         */
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            Log.i("OnTouchListener", "onScroll");
            getInfFromMotionEvent.getX(e2.getRawX());
            getInfFromMotionEvent.getY(e2.getRawY());

            velocityTracker.addMovement(e2);
            velocityTracker.computeCurrentVelocity(1);
            getInfFromMotionEvent.getXVelocity(velocityTracker.getXVelocity()*1000);
            getInfFromMotionEvent.getYVelocity(velocityTracker.getYVelocity()*1000);
            return false;
        }

        /*
         * 控件长按的时候调用的方法,如果这个方法被调用,那么是无法识别滑动事件的,也就是说长按之后,
         * 这个触摸事件就结束了,之后只有松开之后在次触摸(重新触发触摸事件)
         * 但是如果对手势识别器(myGestureDetector)对象设置了 setIsLongpressEnabled(false) 方法之后,
         * 那么无论怎么长按当前 View 也不会回调这个方法
         */
        @Override
        public void onLongPress(MotionEvent e) {
            Log.i("OnTouchListener", "onLongPress");
        }

        /*
         * 当产生系统识别出来的滑动事件的时候,会调用这个方法,参数分别是:
         * 触摸开始点的事件信息,触摸结束(手指松开)的时候储存的事件信息,
         * 整个滑动过程 x 方向上滑动的平均速度,整个滑动过程 y 方向上滑动的平均速度。
         */
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            Log.i("OnTouchListener", "onFling");
            return false;
        }
    };

}

让我们来看看效果:

单击一个位置之后,onDown 方法和 onSingleTapUp 方法被调用,并且模拟器中显示出了单击位置的坐标,接下来试试滑动:

确实显示出了坐标和速度信息,来看看LogCat中打印的信息:

可以看到,滑动的时候先执行了onDown 和 onShowPress 方法,然后再执行 onScroll 方法

在鼠标指针方开之后(手指松开),还调用了 onFling 方法,上文代码中说过:onFling 方法只有滑动速率达到一定要求时候才会被调用。

你可以试试慢慢滑动,那么 onFling 方法将不会调用,所以在处理的时候要注意一下。

接下来,我们再试试长按 View :

你会发现长按 View 之后就不会更新坐标和速度了,其实就像上面代码注释中所说的那样,当 View 调用了 onLongPress 方法之后就无法识别滑动事件了,也就是说在这个接口中长按和滑动无法同时存在。我们可以在 构造方法中加一句代码:

myGestureDetector.setIsLongpressEnabled(false); // 设置手势识别器不监听长按事件

之后再试试,你会发现即使长按当前 View 之后在开始滑动仍然可以调用 onScroll 和 onFling 方法来更新坐标和速度信息。

最后还有一个问题:当一个 View 中既有 onTouchEvent 方法并且设置了 OnTouchListener 接口时,情况是怎样的呢? 这个问题我们可以看一下Android关于 onTouchEvent 方法调用的源码:

public boolean dispatchTouchEvent(MotionEvent event){  
    ... ...  
    if(onFilterTouchEventForSecurity(event)){  
        ListenerInfo li = mListenerInfo;  
        if(li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED  
            && li.mOnTouchListener.onTouch(this, event)) {  
            return true;  
        }  
        if(onTouchEvent(event)){  
            return true;  
        }  
    }  
    ... ...  
    return false;  
}  

我们可以看到里面有这么一段:li.mOnTouchListener.onTouch(this, event)onTouchEvent(event) 并且前者写在前面,那么我们就可以推出,OnTouchListener 接口的 onTouch 方法的优先级是高于 View 里面自带的 onTouchEvent 方法的,当 View 设置了 OnTouchListener 接口并且 onTouch 方法返回 true 的时候, View 里面的 onTouchEvent 方法是不会被调用的。 所以说 OnTouchListener 接口和 View 中自带的 onTouchEvent 方法是不能同时使用的。有兴趣的小伙伴可以自己去试验一下。Ok,这个问题就解决了。

如果博客中有什么不正确的地方,还请多多指点。 谢谢观看。。。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券