二十三、 View 的事件体系(1)--- View 基础知识

什么是 View:

    View 是 Android 中所有控件的基类。它是一种界面层的控件的一种抽象,代表一个控件。我们平常使用的 TextView 和 ImageView 等都是继承自 View 的,源码如下:

public class TextView extends View implements ViewTreeObserver.OnPreDrawListener { ... }
public class ImageView extends View { ... }

    接着我们看看平常使用的布局控件 LinearLayout,它继承自 ViewGroup。ViewGroup 又是什么呢?ViewGroup 可以理解为 View 的组合,它可以包含很多 View 以及 ViewGroup,而且包含的 ViewGroup 又可以包含 View 和 ViewGroup,以此类推,形成一个 View 树。如下图所示:

 

    需要注意的是 ViewGroup 也继承自 View,并且还是个抽象类,所以我们在开发的过程中一般都是使用 Android 提供的实现好的 ViewGroup 的实现类。

public abstract class ViewGroup extends View implements ViewParent, ViewManager { ... }

    ViewGroup 作为 View 或者 ViewGroup 这些组件的容器,派生了多种布局控件子类,比如 LinearLayout、RelativeLayout 等。

    下面这张图是 Android 中 View 的部分继承关系,也就是我们常常使用的控件类:

public class LinearLayout extends ViewGroup { ... }
public class RelativeLayout extends ViewGroup { ... }
public class FrameLayout extends ViewGroup { ... }
...

View 的位置参数    

    Android 系统中有两种坐标系,分别为 Android 坐标系和 View 坐标系。

    Android 坐标系:

    在 Android 中,将屏幕左上角的顶点作为 Android 坐标系的原点,这个原点向右是 X 轴正方向,向下是 Y 轴正方向。另外在触控事件中,使用 getRawX() 和 getRawY() 方法获得的坐标也是 Android 坐标系的坐标(绝对坐标)。

    View 坐标系:

    除了 Android 坐标系,还有一个坐标系:View 坐标系,它与 Android 坐标系并不冲突,两者是共同存在的,通过这两个坐标系我们可以更好地控制 View。

    在 Android 系统中,View 的位置主要由它的四个顶点来决定,分别对应于 View 的四个属性:top、left、right、bottom,其中 top 是左上角纵坐标,left 是左上角横坐标,right 是右下角横坐标,bottom 是右下角纵坐标。需要注意的是,这些坐标都是相对于 View 的父容器来说的,因此它们都是相对坐标。

    我们可以通过如下方法,获取 View 的四个顶点坐标(也就是 View 到其父控件(ViewGroup)的距离):

    getTop():获取 View 自身顶边到父控件顶边的距离。

    getLeft():获取 View 自身左边到父控件左边的距离。

    getRight():获取 View 自身右边到父控件左边的距离。

    getBottom():获取 View 自身底边到父控件顶边的距离。

    getWidth() 和 getHeight() 是获取 View 的最终宽高。

public final int getWidth() {
    return mRight - mLeft;
}

public final int getHeight() {
    return mBottom - mTop;
}

    上面 View 图中的那个圆点,假设就是我们触摸的点。我们知道无论是 View 还是 ViewGroup,最终的点击事件都会由 onTouchEvent(MotionEvent event) 方法来处理。关于 MotionEvent 待会介绍,这里先看一下它提供的获取焦点坐标的方法:

    getX():获取触点距离 View 控件左边的距离,即视图坐标。(也是相对坐标)

    getY():获取触点距离 View 控件上边的距离,即视图坐标。(也是相对坐标)

    getRawX():获取触点距离屏幕左边的距离,即绝对坐标。

    getRawY():获取触点距离屏幕顶边的距离,即绝对坐标。

    从 Android 3.0 开始,View 增加了额外的几个参数:x、y、translationX、translationY,其中 x 和 y 是 View 左上角的坐标,而 translationX 和 translationY 是 View 左上角相对于父容器的偏移量。这几个参数也是相对于父容器的坐标,并且 translationX 和 translationY 的默认值是 0,和 View 的四个基本位置参数一样,View 也为它们提供了 get/set 方法:

public float getX() {
    return mLeft + getTranslationX();
}

public float getY() {
    return mTop + getTranslationY();
}

public float getTranslationX() {
    ...
}

public float getTranslationY() {
    ...
}

    从上面源码我们可以看到它们之间的关系,同时我们要注意,View 在平移的过程中,top 和 left 表示的是原始左上角的位置参数,其值并不会发生改变,此时发生改变的是 x、y、translationX、translationY 这四个参数。

 

MotionEvent 和 TouchSlop:

    MotionEvent:

    用于报告移动(鼠标,笔,手指,轨迹球)事件的对象。运动事件可以保持绝对或相对运动以及其他数据,具体取决于设备的类型。这里我们关注一下关于手指接触屏幕后所产生的几种典型事件:

    ACTION_DOWN --- 手指刚接触屏幕;

    ACTION_MOVE --- 手指在屏幕上移动;

    ACTION_UP --- 手指从屏幕上松开的一瞬间。

    正常情况下,一次手指触摸屏幕的行为会触发一系列点击事件,如下所示:

    * 点击屏幕后离开松开,事件序列为 ACTION_DOWN -> ACTION_UP;

    * 点击屏幕滑动一会再松开,事件序列为 ACTION_DOWN -> ACTION_MOVE -> ... -> ACTION_MOVE -> ACTION_UP。

    上面就是典型的事件序列,同时通过 MotionEvent 对象我们可以得到点击事件发生的 x 和 y 坐标。上面也列出来了(getX、getY(相对坐标); getRawX、getRawY(绝对坐标)),其中 getX() 和 getY() 返回的是相对于当前 View 左上角的 x 和 y 坐标,而 getRawX() 和 getRawY() 返回的是相对于手机屏幕左上角的 x 和 y 坐标。

    

    TouchSlop:

    TouchSlop 是系统所能识别出的被认为是滑动的最小距离。这是一个常量,和设备有关,在不同设备上这个值可能是不同的。可通过如下方式获取这个常量值:

ViewConfiguration.get(getContext()).getScaledTouchSlop();

    获取这个常量的意义:当我们处理滑动时,可以利用这个常量来做一些过滤,比如当两次滑动事件的滑动距离小于这个值,我们就可以认为未达到滑动距离的临界值,因此就可以认为它们不是滑动,这样做可以有更好的用户体验。

 

VelocityTracker(速度追踪对象):

    速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向上的速度,常用于实现投掷和其他此类手势。当你要跟踪一个 touch 事件的时候,使用 obtain() 方法得到这个类的实例,然后用 addMovement(MotionEvent) 方法将你接受到的 Motionevent 加入到 VelocityTracker 类实例中。当你使用到速率时,使用 computeCurrentVelocity(int) 初始化速率的单位,并计算当前的事件的速率,然后使用 getXVelocity() 或 getXVelocity() 获得横向和竖向的速率。

    使用方法:

    1. eg:在 View 的 onTouchEvent() 方法中追踪当前点击事件的速度。

VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);

    2. 获取当前的滑动速度:

// 参数为时间,单位 ms,计算速率
velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();

    3. 最后,当不需要它的时候,需要调用 clear() 方法来重置并回收内存。

velocityTracker.clear();
velocityTracker.recycle();

    这里我们需要注意:

    1. 获取速度之前必须先计算速度,即 getXVelocity() 和 getYVelocity() 这两个方法的前面必须要调用 computeCurrentVelocity() 方法。

    2. 这里的速度是指一段时间内手指所滑过的像素数,比如将时间间隔设为 1000ms 时,在 1s 内,手指从左向右滑过 100 像素,那么水平速度就是 100。注意速度可以为负数,当手指从右向左滑动时,水平方向速度即为负值。

                                速度的计算公式: 速度 = (终点位置 - 起点位置)/ 时间间隔

代码示例:

MainActivity.java:

package com.cfm.viewtest;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        LinearLayout layout = findViewById(R.id.linear_layout);
        layout.setOnTouchListener(new View.OnTouchListener() {

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                // 步骤1
                VelocityTracker velocityTracker = VelocityTracker.obtain();
                velocityTracker.addMovement(event);

                switch (event.getAction()){
                    case MotionEvent.ACTION_DOWN:
                        break;

                    case MotionEvent.ACTION_MOVE:
                        // 步骤2
                        velocityTracker.computeCurrentVelocity(2000);
                        int xVelocity = (int) velocityTracker.getXVelocity();
                        int yVelocity = (int) velocityTracker.getYVelocity();
                        Log.d("cfmtest", "xVelocity: " + xVelocity + " ,yVelocity: " + yVelocity);
                        break;

                    case MotionEvent.ACTION_UP:
                        // 步骤3
                        velocityTracker.clear();
                        velocityTracker.recycle();
                        break;
                }
                return true;
            }
        });
    }
}

Log 打印信息:

2019-06-03 22:26:15.724 30852-30852/com.cfm.viewtest D/cfmtest: xVelocity: 0 ,yVelocity: 0
2019-06-03 22:26:15.761 30852-30852/com.cfm.viewtest D/cfmtest: xVelocity: 1170 ,yVelocity: 0
2019-06-03 22:26:15.779 30852-30852/com.cfm.viewtest D/cfmtest: xVelocity: 2438 ,yVelocity: 0
2019-06-03 22:26:15.797 30852-30852/com.cfm.viewtest D/cfmtest: xVelocity: 3150 ,yVelocity: 0
2019-06-03 22:26:15.816 30852-30852/com.cfm.viewtest D/cfmtest: xVelocity: 3504 ,yVelocity: 0
2019-06-03 22:26:15.834 30852-30852/com.cfm.viewtest D/cfmtest: xVelocity: 3703 ,yVelocity: 0
2019-06-03 22:26:15.852 30852-30852/com.cfm.viewtest D/cfmtest: xVelocity: 4243 ,yVelocity: 0
2019-06-03 22:26:15.870 30852-30852/com.cfm.viewtest D/cfmtest: xVelocity: 2687 ,yVelocity: 0
2019-06-03 22:26:15.888 30852-30852/com.cfm.viewtest D/cfmtest: xVelocity: 1521 ,yVelocity: 0
2019-06-03 22:26:15.906 30852-30852/com.cfm.viewtest D/cfmtest: xVelocity: 1045 ,yVelocity: 0
2019-06-03 22:26:15.924 30852-30852/com.cfm.viewtest D/cfmtest: xVelocity: 584 ,yVelocity: 0
2019-06-03 22:26:15.943 30852-30852/com.cfm.viewtest D/cfmtest: xVelocity: 0 ,yVelocity: 0
2019-06-03 22:26:15.961 30852-30852/com.cfm.viewtest D/cfmtest: xVelocity: 464 ,yVelocity: 0
2019-06-03 22:26:15.979 30852-30852/com.cfm.viewtest D/cfmtest: xVelocity: 0 ,yVelocity: 0
2019-06-03 22:26:15.998 30852-30852/com.cfm.viewtest D/cfmtest: xVelocity: 0 ,yVelocity: 0
2019-06-03 22:26:16.017 30852-30852/com.cfm.viewtest D/cfmtest: xVelocity: 0 ,yVelocity: 0
2019-06-03 22:26:16.035 30852-30852/com.cfm.viewtest D/cfmtest: xVelocity: -2105 ,yVelocity: 0
2019-06-03 22:26:16.054 30852-30852/com.cfm.viewtest D/cfmtest: xVelocity: -3488 ,yVelocity: 0
2019-06-03 22:26:16.071 30852-30852/com.cfm.viewtest D/cfmtest: xVelocity: -5386 ,yVelocity: 0

 

GestureDetector(手势检测对象):

    手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。

    方法解释:

package android.view;



public class GestureDetector {

  
    public interface OnGestureListener {
        /**
         * 手指轻轻触摸屏幕的一瞬间,由一个 ACTION_DOWN 触发。
         */
        boolean onDown(MotionEvent e);

        /**
         * 手指轻轻触摸屏幕,尚未松开或拖动,由一个 ACTION_DOWN 触发。
         * (注意,这里和 onDown() 的区别,它强调的是没有松开或者拖动的状态)
         */
        void onShowPress(MotionEvent e);

        /**
         * 手指(轻轻触摸屏幕后)松开,伴随着一个 ACTION_UP 而触发,这是单击行为。
         */
        boolean onSingleTapUp(MotionEvent e);

        /**
         * 手指按下并在屏幕上拖动,由一个 ACTION_DOWN 和 多个 ACTION_MOVE 触发,这是拖动行为。
         */
        boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);

        /**
         * 长按事件
         */
        void onLongPress(MotionEvent e);

        /**
         * 用户按下触摸屏,快速滑动后松开,由一个 ACTION_DOWN 和 多个  ACTION_MOVE 和一个 ACTION_UP 触发,这是快速滑动行为。
         *
         * @param 第一个 ACTION_DOWN 的 MotionEvent
         * @param 最后一个 ACTION_MOVE 的 MotionEvent
         * @param X 轴上的移动速度,像素/秒
         * @param Y 轴上的移动速度,像素/秒
         * @return 事件被消费,返回 ture,否则返回 false
         */
        boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
    }

    /**
     * 当双击或确认时调用接口
     */
    public interface OnDoubleTapListener {
        /**
         * 严格的单击行为
         * 注意,这里它和 onSingleTapUp() 的区别是,如果触发了 onSingleTapConfirmed(),那么后面
         * 不可能再紧跟着另一种单击行为,即这只可能是单击,而不可能是双击中的一次单击。
         */
        boolean onSingleTapConfirmed(MotionEvent e);

        /**
         * 双击
         * 由两次连续的单击组成,它不可能和 onSingleTapConfirmed 共存。
         */
        boolean onDoubleTap(MotionEvent e);

        /**
         * 双击行为
         * 在双击的期间,ACTION_DOWN、ACTION_MOVE、ACTION_UP 都会触发此回调。
         */
        boolean onDoubleTapEvent(MotionEvent e);
    }

    ...
}

    使用方法:

    1. 创建一个 GestureDetector 对象并实现 OnGestureListener 接口 或者 onDoubleTapListener 接口(不同的接口,实现不同的监听事件,根据需求选择),这里我们要注意,在 GestureDetector 构造函数中,除了 SimpleOnGestureListener 以外的其它两个构造函数都必须是 OnGestureListener 的实例。所以要想使用OnDoubleTapListener 的几个函数,就必须先实现 OnGestureListener。

mGestureDetector = new GestureDetector(this, new GestureListener());
mGestureDetector.setOnDoubleTapListener(new DoubleTapListener());

customView.setFocusable(true);
customView.setClickable(true);
customView.setLongClickable(true);

    2. 在目标 View 的 onTouch() 方法中,我们调用 GestureDetector 的 onTouchEvent() 方法,将捕捉到的 MotionEvent 交给 GestureDetector 来分析是否有合适的 callback 函数来处理用户的手势:

customView.setOnTouchListener(new View.OnTouchListener() {

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        return mGestureDetector.onTouchEvent(event);
    }
});

eg:

MainActivity.java:

package com.cfm.viewtest;

public class MainActivity extends AppCompatActivity {
    private GestureDetector mGestureDetector;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        CustomView customView = findViewById(R.id.custom_view);
        customView.setFocusable(true);
        customView.setClickable(true);
        customView.setLongClickable(true);
        mGestureDetector = new GestureDetector(this, new GestureListener());
        mGestureDetector.setOnDoubleTapListener(new DoubleTapListener());

        customView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                return mGestureDetector.onTouchEvent(event);
            }
        });
    }

    private class GestureListener implements GestureDetector.OnGestureListener{

        /**
         * 手指轻轻触摸屏幕的一瞬间,由一个 ACTION_DOWN 触发。
         */
        @Override
        public boolean onDown(MotionEvent e) {
            Log.d("cfmtest", "--- onDown() ---");
            return false;
        }

        /**
         * 用户轻触触摸屏,尚未松开或拖动,由一个 MotionEvent ACTION_DOWN 触发
         * 注意和 onDown() 的区别,强调的是没有松开或者拖动的状态
         *
         * 而 onDown() 也是由一个MotionEventACTION_DOWN触发的,但是他没有任何限制,
         * 也就是说当用户点击的时候,首先 MotionEventACTION_DOWN,onDown() 就会执行,
         * 如果在按下的瞬间没有松开或者是拖动的时候 onShowPress() 就会执行,如果是按下的时间超过瞬间
         * (这块我也不太清楚瞬间的时间差是多少,一般情况下都会执行onShowPress),拖动了,就不执行onShowPress。
         */
        @Override
        public void onShowPress(MotionEvent e) {
            Log.d("cfmtest", "--- onShowPress() ---");
        }

        /**
         * 用户(轻触触摸屏后)松开,由一个 MotionEvent.ACTION_UP 触发
         *  轻击一下屏幕,立刻抬起来,才会有这个触发
         *     从名子也可以看出,一次单独的轻击抬起操作,当然,如果除了 Down() 以外还有其它操作,
         *     那就不再算是 Single 操作了,所以这个事件 就不再响应
         */
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            Log.d("cfmtest", "--- onSingleTapUp() ---");
            return true;
        }

        /**
         * 手指按下并在屏幕上拖动,由一个 ACTION_DOWN 和 多个 ACTION_MOVE 触发,这是拖动行为。
         */
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            Log.d("cfmtest", "--- onScroll() ---");
            return true;
        }

        @Override
        public void onLongPress(MotionEvent e) {
            Log.d("cfmtest", "--- onLongPress() ---");
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            Log.d("cfmtest", "--- onFling() ---");
            return true;
        }
    }

    private class DoubleTapListener implements GestureDetector.OnDoubleTapListener{

        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            Log.d("cfmtest", "--- onSingleTapConfirmed() ---");
            return true;
        }

        @Override
        public boolean onDoubleTap(MotionEvent e) {
            Log.d("cfmtest", "--- onDoubleTap() ---");
            return true;
        }

        @Override
        public boolean onDoubleTapEvent(MotionEvent e) {
            Log.d("cfmtest", "--- onDoubleTapEvent() ---");
            return true;
        }
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值