什么是 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;
}
}
}