Android进阶知识——View的事件体系

文章目录


本章我们将介绍Android中十分重要的一个概念:View,它的应用十分广泛。比如说自定义控件和解决滑动冲突等,因此学好Veiw的事件体系对于我们开发者而言是十分必要的。

1.View的基础知识

本节我们将主要介绍的内容有:View的位置参数、MotionEvent和TouchSlop对象、VelocityTracker、GestureDetector和Scroller对象。

1.1什么是View

首先View是Android中所有控件的基类,无论是Button还是ListView,它们的共同基类都是View。所以,View是一种界面层的控件的一种抽象,它代表了一个控件。而ViewGroup则是一个控件组,也就是说ViewGroup内部包含了许多控件,即一组View。

在Android的设计中,ViewGroup也继承了View,这就意味着View本身就可以是单控件也可以是由多个控件组成的一组控件,通过这种关系就形成了View树的结构。

1.2View的位置参数

View的位置主要由它的四个顶点来决定,分别对应于View的四个属性:top、left、right、bottom,其中top是左上角纵坐标,left是左上角横坐标,right是右下角横坐标,bottom是右下角纵坐标。(这些坐标都是相对于View的父容器来说的,因此它是一种相对坐标;在Android中,x轴和y轴的正方向分别为右和下)
Android进阶知识——View的事件体系
根据上述知识,我们很容易得到View的宽高和坐标的关系:

width = right - left
hight = bottom - top

上述四个参数的获取方式如下:

  • Left = getLeft();

  • Right = getRight();

  • Top = getTop();

  • Bottom = getBottom();

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

x = left + translationX
y = top + translationY

注意:View在平移过程中,top和left表示的是原始左上角的位置信息,并且值并不会发生改变,此时发生改变的是x、y、translationX和translationY这四个参数。

1.3MotionEvent和TouchSlop

1.MotionEvent

在手指接触屏幕后所产生的一系列事件中,典型的事件类型有如下几种:

  • ACTION_DOWN——手指刚接触屏幕(按下)

  • ACTION_MOVE——手指在屏幕上移动(滑动)

  • ACTION_UP——手指从屏幕上松开的一瞬间(松开)

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

  • 点击屏幕后松手离开,事件序列为DOWN->UP

  • 点击屏幕滑动一会再松开,事件序列为 DOWN->MOVE->…->MOVE->UP

我们还可以通过MotionEvent对象得到点击事件发生的x和y坐标。共有两组方法:getX/getY和getRawX/getRawY。(getX/getY返回的是相对于当前View左上角的x和y坐标,而getRawX/getRawY返回的是相对于手机屏幕左上角的x和y坐标)

2.TouchSlop

TouchSlop是系统所能识别出的被认为是滑动的最小距离,也就是说,当在手机屏幕上滑动时,如果两次滑动之间的距离小于这个常量,那么系统就不认为你是在进行滑动操作。

这个常量和设备有关系,在不同设备上这个值可能是不同的,我们可以通过如下方式获取这个常量:ViewConfiguration.get(getContext()).getScaledTouchSlop()。我们可以利用这个常量来做一些过滤,比如当两次滑动事件的滑动距离小于这个值,我们就可以认为它们不是滑动,这样做可以有更好的用户体验。

1.4VelocityTracker、GestureDetector和Scroller

1.VelocityTracker

速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。它的使用过程如下:

  • 首先,在View的onTouchEvent方法中追踪当前单击事件的速度
    VelocityTracker velocityTracker=VelocityTracker.obtain();
    velocityTracker.addMovement(event);
    
  • 接着,调用get…方法获取当前的速度
    velocityTracker.computeCurrentVelocity(1000);
    int xVelocity= (int) velocityTracker.getXVelocity();
    int yVelocity= (int) velocityTracker.getYVelocity();
    

注意:获取当前速度之前必须先计算速度,即getXVelocity和getYVelocity这两个方法的前面必须调用computeCurrentVelocity方法;这里的速度是指一段时间内手指所滑动的像素数,比如将时间间隔设置为1000ms时,在1s内,手指在水平方向从左向右滑动100像素,那么水平速度就是100。注意速度可以为负数,当手指从右往左滑动时,水平方向速度即为负值。
速度的计算公式如下:

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

另外,compeCurrentVelocity这个方法的参数表示的是一个时间单元或者说时间间隔,它的单位是毫秒(ms),计算速度时得到的速度就是在这个时间间隔内手指在水平或竖直方向上所滑动的像素点。

最后,当不需要使用它的时候,需要调用clear方法来重置并回收内存:

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

2.GestureDectector

手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。它的使用过程如下:

  • 首先,需要创建一个GestureDetector对象并实现OnGestureListener接口,根据需要我们可以实现OnDoubleTapListener从而能够监听双击行为

    GestureDetector mGestureDetector=new GestureDetector(new GestureDetector.OnGestureListener() {
    	//选择实现以下的方法
        @Override
        public boolean onDown(MotionEvent e) {
            return false;
        }
        @Override
        public void onShowPress(MotionEvent e) {
        }
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            return false;
        }
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            return false;
        }
        @Override
        public void onLongPress(MotionEvent e) {
        }
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            return false;
        }
    });
    //解决长按屏幕后无法拖动的现象
    mGestureDetector.setIsLongpressEnabled(false);
    
  • 接着,接管目标View的onTouchEvent方法,在待监听View的onTouchEvent方法中添加

    boolean consume=mGestureDetector.onTouchEvent(event);
    return consume;
    

接下来,我们来看看OnGetsureListener和OnDoubleTapListener中我们可以实现的方法:
Android进阶知识——View的事件体系
上表中的方法有很多,而我们比较常用的方法有:onSingleTapUp(单击)、onFling(快速滑动)、onScroll(拖动)、onLongPress(长按)和onDoubleTap(双击)。

另外,关于是使用onTouchEvent还是使用GestureDectector。这里给大家一个建议:如果是监听滑动相关的,建议自己在onTouchEvent中实现,如果要监听双击这种行为的话,那么就使用GestureDectector。

3.Scroller

弹性滑动对象,用于实现View的弹性滑动。当使用View的scrollTo/scrollBy方法进行滑动时,其过程是一瞬间完成的,但这个没有过渡效果的滑动用户体验不好。而Scroller可以用来实现有过渡效果的滑动,其过程不是瞬间完成的,而是在一定的时间间隔内完成。Scroller和View的computeScroll方法配合使用才能共同完成这一功能。使用方法如下:

//注:以下代码都是写在自定义控件中的
Scroller scroller=new Scroller(getContext());

public void smoothScrollTo(int destX,int destY){//缓慢滚动到指定位置
    int scrollX=getScrollX();
    int delta=destX-scrollX;
    scroller.startScroll(scrollX,0,delta,0,1000);
    invalidate();
}

@Override
public void computeScroll(){
    if(scroller.computeScrollOffset()){
        scrollTo(scroller.getCurrX(),scroller.getCurrY());
        postInvalidate();
    }
}

2.View的滑动

滑动在Android开发中具有很重要的作用,不管一些滑动效果多么绚丽,归根结底,它们都是由不同的滑动外加一些特效所组成的。因此,掌握滑动的方法是实现绚丽的自定义控件的基础。通过三种方式可以实现View的滑动:第一种是通过View本身提供的scrollTo/scrollBy方法来实现滑动;第二种是通过动画给View施加平移效果来实现滑动;第三种是通过改变View的LayoutParams使得View重新布局从而实现滑动。

2.1使用scrollTo/scrollBy

scrollBy本质上也是调用了scrollTo方法,它实现基于当前位置的相对滑动,而scrollTo则实现了基于所传递参数的绝对滑动。

scrollTo和scrollBy只能改变View内容的位置而不能改变View在布局中的位置;View边缘是指View的位置,由四个顶点组成,而View内容边缘是指View中的内容的边缘;mScrollX的值总是等于View左边缘和View内容左边缘在水平方向的距离,mScrollY的值总是等于View上边缘和View内容上边缘在竖直方向的距离;mScrollX和mScrollY的单位为像素;从左向右滑动,那么mScrollX为负值,反之为正值,从上往下滑动,那么mScrollY为负值,反之为正值。(正好和x、y轴的延伸方向相反)

2.2使用动画

通过动画我们能够让一个View进行平移,而平移就是一种滑动。使用动画来移动View,主要是操作View的translationX和translationY属性,既可以采用传统的View动画,也可以采用属性动画。

采用如下的View动画代码,可以在1000ms内将一个View从原始位置向右下角移动100个像素,代码如下:

//View动画
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true"
    android:zAdjustment="normal">

    <translate
        android:duration="1000"
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:interpolator="@android:anim/linear_interpolator"
        android:toXDelta="100"
        android:toYDelta="100" />

</set>
//给控件应用View动画
Button button=findViewById(R.id.myButton);
Animation myAnimator= AnimationUtils.loadAnimation(this,R.anim.my_animation);
button.startAnimation(myAnimator);

如果采用属性动画的话,就更简单了,以下代码可以将一个View在1000ms内从原始位置向右平移100像素。

ObjectAnimator.ofFloat(button,"translationX",0,100).setDuration(1000).start();

View动画是对View的影像做操作,它并不能真正改变View的位置参数,包括宽/高,并且如果希望动画后的状态得以保留还必须将fillAfter属性设置为true,否则动画完成后其动画结果会消失。而使用属性动画并不会存在上述问题。

上面提到View动画并不能真正改变View的位置,这会带来一个很严重的问题。比如我们通过View动画将一个Button向右移动100px,并且这个View设置的有单击事件,然后你会惊奇的发现,单击新位置无法触发onClick事件,而单击原始位置仍然可以触发onClick事件,尽管Button已经不再原始位置了。这个问题还是比较好理解的,因为Button的位置信息(四个顶点和宽高)并不会随着动画而改变,因此在系统眼里,这个Button并没有发生任何改变。

从Android3.0开始,使用属性动画可以解决上面的问题。而在Android3.0以下我们这里给出一个简单的解决方法。针对上述的View动画问题,我们可以在新位置预先创建一个和目标Button一模一样的Button,它们不但外观一样连onClick事件也一样。当目标Button完成平移动画后,就把目标Button隐藏,同时把预先创建好的Button显示出来,通过这种间接的方式我们解决了上面的问题。

2.3改变布局参数

改变布局参数,即改变LayoutParams。比如我们想要把一个Button向右平移100px,我们只需要将这个Button的LayoutParams里的marginLeft参数的值增加100px即可。

还有一种情形,为了达到移动Button的目的,我们可以在Button的左边放置一个空View,这个空View的默认宽度为0,当我们需要向右移动Button时,只需要重新设置空View的宽度即可,当空View的宽度增大时(假设Button的父容器是水平方向的LinearLayout),Button就自动被挤向右边,即实现了向右平移的效果。而该怎样重新设置一个View的LayoutParams呢?代码如下所示:

Button button=findViewById(R.id.myButton);
ViewGroup.MarginLayoutParams params=(ViewGroup.MarginLayoutParams) button.getLayoutParams();
params.leftMargin+=300;
button.setLayoutParams(params);
//或button.requestLayout();

2.4各种滑动方式的对比

上面我们共介绍了三种不同的滑动方式,它们都能实现View的滑动,接下来我们就来分析一下它们之间的差别。

  • scrollTo/scrollBy这种方式:它可以比较方便的实现滑动效果并且不影响内部元素的单击事件,但是它的缺点也是很明显的:它只能滑动View的内容,并不能滑动View本身。

  • 动画这种方式:如果是使用属性动画,那么这种方式没有明显的缺点;如果是使用View动画或者是在Android3.0以下使用属性动画,均不能改变View本身的属性。在实际使用中,如果动画元素不需要响应用户的交互,那么使用动画来做滑动是比较合适的,否则就不合适了。但是动画有一个很明显的优点,那就是一些复杂的效果必须要通过动画才能实现。

  • 改变布局这种方式:它除了用起来麻烦点以外,也没有明显的缺点。它的主要适用对象是一些具有交互性的View,因为这些View需要和用户交互,直接通过动画去实现会有问题。

针对上述分析,我们再来总结一下:

  • scrollTo/scrollBy:操作简单,适合对View内容的滑动;

  • 动画:操作简单,主要适用于没有交互的View和实现复杂的动画效果;

  • 改变布局参数:操作稍微复杂,适用于有交互的View。

上一篇:View的事件体系——View的基础知识


下一篇:从Activity创建到View呈现中间发生了什么?