Android View的事件体系(上)

本章将介绍Android中十分重要的一个概念View

文章目录:

View.png

3.1 View的基础知识

1. 什么是View :

内容 含义
View 一个单一的控件,如Button、TextView
ViewGroup 一个控件组,如RelativeLayout、LinearLayout

View的视图结构:


View的视图结构
  • View的位置参数:
    View的宽高和坐标的关系

View的宽高和坐标的关系:

width = right - left
height = bottom - top

位置获取方式

View的位置是通过view.getxxx()函数进行获取:(以Top为例)
// 获取Top位置

public final int getTop() {  
    return mTop;  
}  

// 其余如下:
  getLeft();      //获取子View左上角距父View左侧的距离
  getBottom();    //获取子View右下角距父View顶部的距离
  getRight();     //获取子View右下角距父View左侧的距离

2. MotionEvent和TouchSlop

  • MotionEvent
    在手指接触屏幕后所产生的事件:
常量 含义
ACTION_DOWN 手指接触屏幕(按下)
ACTION_MOVE 手指在屏幕上移动(滑动)
ACTION_UP 手指从屏幕上松开的一瞬间(离开)

上述三种情况是典型的时间序列,同时通过 MontionEvent 对象我们可以得到点击事件发生的 x 和 y 坐标。系统提供两组方法:getX / getY 和 getRawX / getRawY。

get() 和 getRaw() 的区别

具体代码:

@Override
    public boolean onTouchEvent(MotionEvent event) {

        //get() :触摸点相对于其所在组件坐标系的坐标
        event.getX();
        event.getY();

        //getRaw() :触摸点相对于屏幕默认坐标系的坐标
        event.getRawX();
        event.getRawY();
        return super.onTouchEvent(event);
    }
  • TouchSlop
    TouchSlop 是系统所能识别出的被认为是滑动的最小距离。换句话说,当手指在屏幕上滑动时,如果两次滑动之间的距离小于这个常量,那么系统就不认为你是在进行滑动操作。原因很简单滑动的距离太短,系统不认为是滑动。这是一个常量和设备有关,不同的设备上这个值可能会不同。获取方式:
     ViewConfiguration.get(this).getScaledTouchSlop(); 

4. VelocityTracher、GestureDetector和Scroller

  • VelocityTracher
    速度追踪,用于追踪手指在滑动过程中的速度,包括水平方向和竖直方向的速度。
 @Override
    public boolean onTouchEvent(MotionEvent event) {

        VelocityTracker velocityTracker = VelocityTracker.obtain();
        velocityTracker.addMovement(event);
        velocityTracker.computeCurrentVelocity(1000);//设置时间间隔为1000
        int xVelocity = (int) velocityTracker.getXVelocity();
        int yVelocity = (int) velocityTracker.getYVelocity();
        Log.e("event", "xVelocity ;" + xVelocity + "  yVelocity ;" + yVelocity);
        //当我们结束的时候,需要调用 clear 方法来重置并且回收内存。
        velocityTracker.clear();
        velocityTracker.recycle();
        return super.onTouchEvent(event);
    }

这里需要注意,第一点,获取速度之前必须先计算速度,即 getXVelocity() 和 getYVelocity() 必须在 computeCurrentVelocity 的后面,第二点,这里的速度是指一段时间内手指所划过的像素数,比如将时间间隔设置有 1000ms 时,在1s 内手指在水平方向划过100像素,那么水平速度就是 100 ,当手指从右向左滑动时,速度为负数,公式:
<div align = center>速度 = (终点位置 - 起始位置)/ 时间段</div>
不要管时间间隔是传统含义,这里只要根据公式来计算即可。

  • GestureDetector
    手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。首先创建一个MianActivity 实现GestureDetector.OnGestureListener ,OnDoubleTapListener接口 ,
public class MainActivity extends AppCompatActivity implements GestureDetector.OnGestureListener,
       GestureDetector.OnDoubleTapListener{
private GestureDetector mGestureDetector;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mGestureDetector = new GestureDetector(this,this);
        //解决长按屏幕后无法拖动的现象
        mGestureDetector.setIsLongpressEnabled(false);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {return mGestureDetector.onTouchEvent(event);}
    //手指轻轻触摸屏幕的一瞬间,由一个 ACTION_DOWN 触发。
    @Override
    public boolean onDown(MotionEvent e) { return false;}
    //手指轻轻触摸屏幕,尚未松开或拖动,由一个 ACTION_DOWN 触发。*注意和 onDown 的区别,它强调的是没有松开或者拖动的状态*
    @Override
    public void onShowPress(MotionEvent e) {}
    //手指(轻轻触摸屏幕后)松开,伴随着一个 MontionEvent ACTION_UP 而触发,这是单击行为。
    @Override
    public boolean onSingleTapUp(MotionEvent e) {return false; }
    //手指按下屏幕并拖动,由 1 个 ACTION_DOWN,多个 ACTION_MOVE 触发,这是拖动行为。
    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {return false;}
    //用户长久地按着屏幕不放,即长按。
    @Override
    public void onLongPress(MotionEvent e) { }
    //用户按下触摸屏、快速滑动后松开,由 1 个 ACTION_DOWN 、多个 ACTION_MOVE 和 ACTION_UP 触发,这是快速滑动行为
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return false; }
    //双击,由 2 次联系的单击组成,它不可能和 onSingleTapConfirmed 共存。
    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) {return false; }
    //严格的单击行为
    //*注意它和 onSingleTapUp的区别,如果触发了onSingleTapConfirmed,那么后面不可能再紧跟着另一个单击行为,即这只可能是单击,而不可能是双击中的一次单击*
    @Override
    public boolean onDoubleTap(MotionEvent e) {return false;}
    //表示发生了双击行为,在双击的期间,ACTION_DOWN 、ACTION_MOVE 、ACTION_UP 都会触发此回调。 
    @Override
    public boolean onDoubleTapEvent(MotionEvent e) {return false;}
}

这点可能会奇怪 setIsLongpressEnabled(false)参数要为false,经过我测试,setIsLongpressEnabled(true)的时候 ,长按屏幕触发 onLongPress 会直接拦截掉其他的触摸
down、move、up 事件,为 false 的时候,onLongPress 则不会触发,其他正常。

方法很多,但是并不是所有的方法都会被时常用到,在日常开发中,比较常用的onSingleTapUp(单击),onFling(快速滑动),onScroll(推动),onLongPress(长按)和onDoubleTap(双击),另外要说明的是,在实际开发中可以不使用 GestureDetector,完全可以自己在view中的onTouchEvent中去实现。

  • Scroller
    弹性的滑动对象,用于实现View的弹性滑动。

3.2 View 的滑动

常见的的三种 View 滑动实现方式:

  • 通过 View 本身提供的 scrollTo / scrollBy 方法来实现滑动
  • 通过动画给 View 施加平移想过
  • 通过改变 View 的LayoutParams 使得 View 重新布局从而实现滑动。
3.2.1 使用 scrollTo/scrollBy

  为了实现 View 的滑动, View 提供了 scrollTo 和 scrollBy,如下所示。

 public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }

    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

从上面的源码可以看出,scrollBy 实际上也是调用了scrollTo 方法,我们需要知道 scrollTo 智能改变 View 内容的位置而不能改变 View 在布局中的位置。mScrollX 和 mScrollY 单位为像素。

变换规律示意图(单位:像素)

完整代码地址
列出部分代码:

public class ScollerView extends View implements View.OnClickListener {
...
 public ScollerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mScroller = new Scroller(context);
        this.setOnClickListener(this);
}
private void smoothScrollTo(int destX, int destY) {
        int scrollX = getScrollX();
        int delta = destX - scrollX;
        int scrollY = getScrollY();
        int deltY = scrollY + destY;
        //100ms 内滑向 destX ,效果就是慢慢滑动
        mScroller.startScroll(scrollX, 0, delta, deltY, 1000);
        invalidate();
    }

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

3.2.2 使用动画

使用动画来移动 View ,主要操作 View 的 translatuonX 和 translationY 属性,既可以采用传统的 View 动画,也可以采用属性动画。书中的3.0兼容就不介绍了,现在也基本用不到。
  采用 View 动画的代码,如下所示,此动画可以在100ms 内将一个 View 从原来位置像右下角移动 100 个像素。

GIF.gif

在 res 目录中创建一个 anim 目录,在新建一个 set :

<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true" android:zAdjustment="normal"
    >
    <translate
        android:duration="100"
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:interpolator="@android:anim/linear_interpolator"
        android:toXDelta="100"
        android:toYDelta="100"/>
</set>

在 Activity 中使用改方法,就可以移动mButton

  ObjectAnimator.ofFloat(mButton,"translationX",0,100).setDuration(2000).start();

View 动画是对 View 的影像做操作,它并不能真正改变 View 的位置参数,包括宽高。并且如果希望动画后的状态得以保存还必须将 fillAfter 设为 true,为 false 时动画结束后 View 会恢复原状。

3.2.3 改变布局参数

第三种实现 View 滑动的方法,那就是改变布局参数,即改变 LayoutParams。改变 View 的位置,将LayoutParams 中的位置关系设置一下即可。实现方法很简单:

        ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) 
        mButton.getLayoutParams();
        params.width += 100;
        params.leftMargin += 100;
        mButton.requestLayout();
       //或者mButton.setLayoutParams(params);

3.2.4 各种滑动方式的对比

  • scrollTo/scrollBy:操作简单,适合对 View 内容的滑动;
  • 动画:操作简单,主要适用于没有交互的 View 和实现复杂的动画效果;
  • 改变布局参数:操作稍微复杂,适用于有交互的 View;
GIF.gif

GIF.gif

自定义一个 View 继承 Button:

...
@Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getRawX();
        int y = (int) event.getRawY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                int translationX = (int) (getTranslationX() + deltaX);
                int translationY = (int) (getTranslationY() + deltaY);
                setTranslationX(translationX);
                setTranslationY(translationY);
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }
![GIF.gif](http://upload-images.jianshu.io/upload_images/5531940-42d068be5205a447.gif?imageMogr2/auto-orient/strip)

        mLastX = x;
        mLastY = y;
        return true;
    }

通过上述代码可以看出,这一全屏滑动的效果实现起来相当简单。首先我们通过 getRawX 和 getRawY 方法来获取手指当前的坐标,注意这里不能使用 getX 和 getY 方法,getRawX 是获取全屏坐标,getX 是获取 View 的相对坐标(前面有讲到)。

3.3 弹性滑动

知道了 View 的滑动,还要知道如何实现 View 的弹性滑动。

3.3.1 使用 Scroller

之前使用过Scroller,现在来分析一下它的源码,探究一下为什么它能实现 View 的弹性滑动。

private void smoothScrollTo(int destX, int destY) {
        int scrollX = getScrollX();
        int delta = destX - scrollX;
        int scrollY = getScrollY();
        int deltY = scrollY + destY;
        //100ms 内滑向 destX ,效果就是慢慢滑动
        mScroller.startScroll(scrollX, 0, delta, deltY, 1000);
        invalidate();
    }

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

上面是 Scroller 的典型的使用方法,这里先描述它的工作原理:当我们构造一个Scroller 对象并且调用它的 startScroll 方法时,Scroller 内部其实上面也没做,它只是保存了,我们传递的几个参数。这几个参数从 startScroll 的原型上就可以看出来,如下所示。

 public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        mMode = SCROLL_MODE;
        mFinished = false;
        mDuration = duration;
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
        mStartX = startX;
        mStartY = startY;
        mFinalX = startX + dx;
        mFinalY = startY + dy;
        mDeltaX = dx;
        mDeltaY = dy;
        mDurationReciprocal = 1.0f / (float) mDuration;
    }

这个方法的参数含义很清楚, startX 和 startY 表示的是滑动的七点,dx 和 dy 表示的是要滑动的距离,而 duration 表示的是滑动时间,即整个滑动过程完成所需要的时间,注意这里的滑动是指 View 滑动内容的滑动而非 View 本身的位置的改变,可以看到仅仅是调用 startScroll 方法是无法让 View 滑动的,因为它内部并没有做滑动相关的事,那么 Scroller 到底是如何让 View 弹性滑动的呢 ?答案就是 startScroll 方法下面的 invalidate 方法,虽然有点不可思议,但是的确是这样的。invalidate 方法会导致 View 重绘, 在 View 的 draw 方法中又会去调用 computeScroll 方法, computeScroll 方法在View 中是一个空实现,因此需要我们自己去实现,方面的代码已经实现了 computeScroll 方法。正是因为这个 computeScroll 方法,View 才能实现弹性滑动。这看起来还是很抽象,其实是这样的:当 View 重绘后在 draw 方法中调用 computeScroll ,而 computeScroll 又回去向 Scroller 获取当前的 scrollX 和 scrollY,然后通过 scrollTo 方法实现滑动;接着又调用 postInvalidate 方法来进行第二次重绘,这一次重绘的过程和第一次重绘一样,还是会导致 computeScroll 方法被调用了;然后继续向 Scroller 获取当前的 scrollX 和 scrollY。并通过 scrollTo 方法滑动到新的位置,如此反复,知道整个滑动的过程结束。
  我们再看一下 Scroller 的 computeScrollOffset 方法的实现,如下所示:

public boolean computeScrollOffset() {
        ...

        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
    
        if (timePassed < mDuration) {
            switch (mMode) {
            case SCROLL_MODE:
                final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
                mCurrX = mStartX + Math.round(x * mDeltaX);
                mCurrY = mStartY + Math.round(x * mDeltaY);
                break;
            ...
        }
        return true;
    }

是不是突然就明白了?这个方法会根据时间的流逝来计算当前的scrollX和Y的值,计算方法也很简单,大意就是根据时间流逝的百分比来计算scrollX和Y改变的百分比并计算出当前的值,这个过程相当于动画的插值器的概念,这里我们先不去深究这个具体的过程,这个方法的返回值也很重要,他返回true表示滑动还未结束,false表示结束,因此这个方法返回true的时候,我们继续让View滑动

通过上面的分析,我相信大家应该都已经明白了Scroller的滑动原理了,这里做一个概括,他本身并不会滑动,需要配合computeScroll方法才能完成弹性滑动的效果,不断的让View重绘,而每次都有一些时间间隔,通过这个事件间隔就能得到他的滑动位置,这样就可以用ScrollTo方法来完成View的滑动了,就这样,View的每一次重绘都会导致View进行小幅度的滑动,而多次的小幅度滑动形成了弹性滑动,整个过程他对于View没有丝毫的引用,甚至在他内部连计时器都没有。

3.3.2 通过动画

一位大神的 View 系列 传送门:http://blog.csdn.net/harvic880925/article/details/50995268
  动画本身就是一种渐进的过程,因此通过它来实现的滑动天然就具有弹性效果。

GIF.gif

代码:

ValueAnimator animator = ValueAnimator.ofInt(0, 10, 60, 200).setDuration(2000);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float fraction = animation.getAnimatedFraction();
                int animatedValue = (int) animation.getAnimatedValue();
                mButton.scrollTo(animatedValue, 0);//根据动画本身移动200px
                mButton2.scrollTo((int) (fraction * 100), 0);//自定义移动100px
            }
        });
        animator.start();

在上述代码中,mButton 动画移动距离 200,我们的动画本质上没有作用于任何对象上,只是在 2000ms 内完成了整个动画过程。利用这一特性,我们就可以在动画的每一帧到来时获取动画完成的比例 fraction ,然后再根据这个比例计算出当前 View 所要滑动的距离。mButton2 通过改变百分比 fraction 来完成 View 的滑动,采用这种方法除了能够完成弹性滑动以外,还可以实现其他动画效果,我们完全可以在 onAnimationUpdate 方法中加上我们的其他操作。

3.3.3 使用延时策略

另一种实现弹性滑动的方法,延时策略。核心思想是使用 Handler 或 View 的 postDelayed 方法,也可以使用线程的 sleep 方法。

mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
private static final int MESSAGE_SCROLL_TO = 1;
    private static final int FRAME_COUNT = 30;
    private static final int DELAYED_TIME = 30;
    private int mCount = 0;
    @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler() {
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MESSAGE_SCROLL_TO: {
                    mCount++;
                    if (mCount <= FRAME_COUNT) {
                        float fraction = mCount / (float) FRAME_COUNT;
                        int scrollX = (int) (fraction * 100);
                        mButton.scrollTo(scrollX, 0);
                        mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
                    }
                    break;
                }
                default:
                    break;
            }
        };
    };

推荐阅读更多精彩内容