一个写代码的地方

Android View 基础知识

转载请附原文链接:Android View 基础知识读

内容源于Android 开发艺术探索第 3 章 3.1 View 基础知识读书笔记:View 的位置参数、MotionEvent 和 TouchSlop对象、VelocityTracker, GestureDetector 和 Scroller对象。

1 什么是 View

View 是Android中所有控件的基类,例如Button和TextView、ViewGroup 等常见控件他们的基类都是View,View是一种界面层的控件的一种抽象,代表了一个控件。View 本身可以是单个控件也可以是由多个控件组成的一组控件,通过这种关系就形成了View 树的结构,与Web前端中的DOM树概念相似。

1.1 View的位置参数

View 的位置主要有四个顶点决定的, top(左上角纵坐标)、left(左上角横坐标)、right(右下角横坐标)、bottom(右下角纵坐标),这些顶点的坐标是相对于 View 的父容器来说,是一种相对坐标。

View_ XYZ

根据上图View的坐标位置关系可以得出以下view的宽度(width)和高度(height):

View的宽高和坐标关系:width = right - left,height = top - bottom。

view 源码中他们对应于mLeft、mRight、mTop、mBottom这四个成员变量,获取方式如下:

Left=getLeft() 、Right=getRight() 、Top=getTop() 、Bottom=getBottom()

从Android 3.0开始,view增加了:x、y、translationX、translationY 四个参数,这几个参数也是相对于父容器的坐标。x和y是左上角的坐标(偏移前或者偏移后左上角坐标,即当前坐标),而translationX和translationY是view左上角相对于父容器的偏移量(偏移坐标),默认值都是0。View 也提供了相应的方法,通过这几个坐标可以得到如下换算关系:

x = left + translationX 其中:x为左上角偏移后当前横坐标,left为view原始位置横坐标即可以通过getLeft()获取,translationX 为view相对于父容器偏移横坐标。

y = top + translationY 其中y为左上角偏移后当前纵坐标,left为view原始位置纵坐标可以通过getTop()获取,translationY 为view相对于父容器偏移纵坐标。
view_x_y

注意:在 View 平移的过程中, top 和 left 表示的是原始左上角的位置信息, 其值并不会发生变化, 改变的是 x, y, traslationX 和 translation Y 。

2 MotionEvent 和 TouchSlop

  1. MotionEvent 是指用户手指触摸屏幕产生的一系列事件 分为 ACTION_DOWN(手指刚接触屏幕), ACTION_MOVE(手指在屏幕上滑动), ACTION_UP(手指从屏幕上松开瞬间)。
  2. 点击屏幕后松开,事件序列 DOWN->UP点击屏幕滑动一会再松开,事件序列为 DOWN->MOVE->…->MOVE->UP。
  3. getX/getY获取相对当前View左上角的x和y坐标;getRawX/getRawY获取相对手机屏幕左上角的x和y坐标。
  4. TouchSlop 是系统能识别滑动的最小距离,是系统常量,与设备有关,不同设备,值可能不同,当手指在屏幕上滑动,小于这个距离,系统不认为你在进行滑动操作;可通过ViewConfiguration.get(getContext()).getScaledTouchSlop()方法来获取;可以利用此参数进行一些滑动过滤,当未达到此值的时候可以认为未达到滑距离的临界值。

3 VelocityTracker 、GestureDetector 和Scroller

  1. VelocityTracker 用于追踪手指在滑动过程中的速度。如在View的onTouchEvent方法中追踪当前单击事件的速度方法如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //获取VelocityTracker对象
    VelocityTracker velocityTracker = VelocityTracker.obtain();
    //将事件event添加进去
    velocityTracker.addMovement(event);
    //获取当前滑动速度计算方法
    //这里的速度是指一段时间内手指滑动过的像素数
    //获取速度前需输入计算速度参数时间间隔单位ms,即计算在1000ms内手指滑动过的像素数
    velocityTracker.computeCurrentVelocity(1000);
    //获取x轴速度
    int xVelocity = (int)velocityTracker.getXVelocity();
    //获取y轴速度
    int yVelocity = (int)velocityTracker.getYVelocity();

    注意:这里的速度可以为负数,手指从右向左滑动,水平方向速度即为负数,计算公式为:

    速度 = (终点位置 - 起点位置)/ 时间段

    计算得到的速度即为在给定时间间隔内水平方向或者竖直方向所滑动的像素数。

    最后如果不在使用,需要调用clear进行清除和回收:

    1
    2
    3
    //重置和回收
    mVelocityTracker.clear(); //一般在MotionEvent.ACTION_UP的时候调用
    mVelocityTracker.recycle(); //一般在onDetachedFromWindow中调用
  2. GestureDetector

    用于辅助检测用户的单击、滑动、长按、双击等行为;下面笔者写一个小的 demo 如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    public class GestureActivity extends AppCompatActivity implements GestureDetector.OnGestureListener,GestureDetector.OnDoubleTapListener{
    private static final String TAG ="GestureActivity" ;
    private GestureDetector gestureDetector;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    //创建GestureDetector对象,并设置OnGestureListener监听
    //第一个this参数为context,第二个参数为OnGestureListener
    gestureDetector =new GestureDetector(this,this);
    //设置双击监听
    gestureDetector.setOnDoubleTapListener(this);
    //解决长按屏幕无法拖拽的现象
    gestureDetector.setIsLongpressEnabled(false);
    }
    //重写Activity 的onTouchEvent将MotionEvent事件设置给gestureDetector
    @Override
    public boolean onTouchEvent(MotionEvent event) {
    //接管目标 View 的 onTouchEvent 方法
    gestureDetector.onTouchEvent(event);
    //返回事件处理结果
    return super.onTouchEvent(event);
    }
    @Override
    public boolean onDown(MotionEvent e) {
    //手指轻轻触摸屏幕的瞬间,根据需要处理相应事件
    Log.d(TAG,"onDown"+e.toString());
    return false;
    }
    @Override
    public void onShowPress(MotionEvent e) {
    //手指轻触屏幕,尚未松开或者拖动,根据需要处理相应事件
    Log.d(TAG,"onShowPress"+e.toString());
    }
    @Override
    public boolean onSingleTapUp(MotionEvent e) {
    //手指松开,根据需要处理相应事件
    Log.d(TAG,"onSingleTapUp"+e.toString());
    return false;
    }
    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
    //手指按下屏幕并拖动,根据需要处理相应事件
    Log.d(TAG,"onScroll");
    return false;
    }
    @Override
    public void onLongPress(MotionEvent e) {
    //手指长按,根据需要处理相应事件
    Log.d(TAG,"onLongPress"+e.toString());
    }
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
    // 用户按下触摸屏,快速移动后松开,根据需要处理相应事件
    Log.d(TAG,"onFling");
    return false;
    }
    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) {
    //单击行为;根据需要处理事件
    return false;
    }
    @Override
    public boolean onDoubleTap(MotionEvent e) {
    //表示双击行为,在双击的期间,根据需要处理事件
    return false;
    }
    @Override
    public boolean onDoubleTapEvent(MotionEvent e) {
    // 双击,由两次连续的单击组成,根据需要处理事件
    return false;
    }
    }

    本例子中是将手势监听设置给了整个Activity,你也可以根据实际需求设置给某个view,同样是在onTouchEvent方法中设置。

    OnGestureListener 接口的方法

    onDown : 手指轻轻触摸屏幕的瞬间,由一个 ACTION_DOWN 触发;

    onShowPress : 手指轻触屏幕,尚未松开或者拖动, 由一个 ACTION_DOWN 触发;

    onSingTapUp : 手指松开,由 ACTION_DOWN 触发, 这是单击行为;

    onScroll: 手指按下屏幕并拖动,由一个 ACTION_DOWN , 多个 ACTION_MOVE 触发, 这是拖动行为;

    onLongPress: 用户长久地按着屏幕不放,即长按;

    onFling: 用户按下触摸屏,快速移动后松开,由一个 ACTION_DWON、多个 ACTION_MOVE 和一个 ACTION_UP 触发,快速滑动行为;

    OnDoubleTapListener 接口中的方法

    onDoubleTap: 双击,由两次连续的单击组成,不能和 onSingleTapConfirmed 共存;

    onSingleTapConfirmed: 单击行为;

    onDoubleEvent: 表示双击行为,在双击的期间, ACTION_DOWN、ACTION_MOVE、ACTION_UP 都不会触发此回调。

    在实际开发中可以不使用 GestureDetector ,完全可以在View的 onTouchEvent 方法中根据MotionEvent类型以及滑动速度等条件实现所需的监听,作者建议:如果只是监听滑动相关的推荐在onTouchEvent中实现,如果需要监听双击,使用GeststureDetector。

3.Scroller

用来实现View的弹性滑动,View的scrollTo/scrollBy是瞬间完成的,Scroller本身并不能实现view的滑动,使用Scroller配合View的computeScroll方法配合使用达到弹性滑动的效果,它不断地让view重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔Scroller就可以得出view的当前的滑动位置,知道了滑动位置就可以通过scrollTo方法来完成view的滑动。就这样,view的每一次重绘都会导致view进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动,这就是Scroller的工作原理,下面3.2节会详细介绍,典型固定代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Scroller mScroller = new Scroller(mContext);
// 缓慢滚动到指定的位置
private void smoothScrollTo(int destX, int destY){
int scrollX = getScrollX();
int deltaX = destX - scrollX;
// 以 1000ms 内滑向 destX, 效果是慢慢滑动
mScroller.startScroll(scrollX, destY, deltaX , 0, 1000);
// View 的重绘
invalidate();
}
@Override
public void computeScroll() {
// 重写 computeScroll 方法,并在内部完成平滑滚动的逻辑
if (mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
// 再次进行重绘
postInvalidate();
}
}

##4 View 触摸事件的监听小结:

第一类: 系统已经提供了控件触摸事件相关的监听接口,我们直接去调用即可。

这种类型的,比如就是简单的想去监听 view.setOnClickListener(OnClickListener),我们可以直接调用系统提供的 api 去实现即可。

第二类: 系统提供 api 无法满足我们想要的控件触摸事件相关的监听效果。

1、如果你要监听的 View 是一个 ViewGroup 类型,比如 RecyclerView 就没有提供 ItemView 点击事件监听 api,那么我们可以在 Adapter 中的 onBind 方法中去给 RecyclerView 的 ItemView 所包含的那个子 View 设定监听事件,通过给这个子 View 设置监听达到交互响应目的,当你点击 RecyclerView 的ItemView 时候虽然 RecyclerView 的 ItemView 没有监听,但是事件会传递到它的子 View ,此时它的子 View 是有监听事件的,此时当你去点击 RecyclerView 的时候就会产生响应交互,给用户的感觉是点击在 RecyclerView 上面产生的行为,其实是它的子 View 产生的行为(欺骗用户眼睛的行为,这种思路在开发中会经常用到,可以绕开很多难题)。

2、如果你要监听的 View 是一个 ViewGroup 类型,而在监听的过程中 ViewGroup 会与它的子 View 产生事件的冲突,那么也可以在 ViewGroup 的 onInterceptTouchEvent(MotionEvent event) 中,根据 UP,DOWM,MOVE 等事件类型、手势滑动方向、滑动速度的综合条件去实现我们想要的监听事件类型,决定拦截事件,示例代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 解决滑动冲突
* @param event 点击事件
* @return 表明事件是否被拦截
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}

3、在 onTouchEvent(MotionEvent event) 方法中,通过 GestureDetector 这个类和想要的 Listener 例如 SimpleOnGestureListener (也可以自己根据 UP,DOWM,MOVE 等事件类型、手势滑动方向、滑动速度的综合条件去实现)来进行判断手势行为来触发我们想要的事件,参考代码: GestureDetector

1
2