View的事件体系

View虽然不是四大组件,但是他的作用堪比四大组件,甚至比Receiver和Provider的重要性都大.在Android中Activity承担着可视化的功能,同时Android系统提供了很多基础控件,常见的有TextView,Button等.但是很多时候使用系统提供的基础控件是不可以满足需求的,这就需要我们进行自定义控件,但是如果自定义控件,就需要对Android的View体系有一定的了解

View 基础知识

View的位置参数,MotionEvent和TouchSlop对象,VelocityTracker,GestureDetector和Scroller对象.

什么是View

View是所有控件的基类,不管是Button,TextView还是RelativeLayout和ListView,他们的共同基类都是View,除了View,还有ViewGroup,从名字来看,它可以被翻译为控件组,也就是说,它包含了很多View.

View的位置参数

View的位置主要由它的四个顶点决定.分别对应于View的四个属性,top,left,right和bottom.这些坐标都是相对于控件的父容器来说,因为它是一种相对坐标,如图所示.

这里写图片描述
那么View的宽高的计算关系就是

width=right-left;
height=bottom-top;

至于View的这四个参数获取方式也很简单

left=getLeft();
right=getRight();
top=getTop();
bottom=getBottom();

MotionEvent和TouchSlop

MotionEvent

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

  • ACTION_DOWN—–手指刚接触屏幕的时候
  • ACTION_MOVE—–手指在屏幕移动的时候
  • ACTION_UP—–手指从屏幕上松开的一瞬间

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

  • 点击屏幕离开,事件顺序为—DOWN—>UP
  • 点击屏幕滑动离开,事件顺序为—DOWN—>MOVE—>MOVE—>UP
    同事MotionEvent对象给我们提供了点击事件发生的x和y坐标.为此系统提供了两组方法,getX()和getY()以及getRawX()和getRaw().它们的区别很简单.
    getX()和getY()获取的是当前view位于父布局的x和y的距离,而getRawX()和getRawY()获取的是当前view距离屏幕的x和y的坐标
TouchSlop

TouchSlop是系统所能识别出的被认为是滑动的最小距离,换句话说,当手指在屏幕滑动的时候,当两次滑动的距离小于这个常量,那么系统就不认为你是在滑动操作.原因很简单.滑动的距离太短,系统不认为是在滑动.它是一个常量,跟设备有关.在不同的设备上,这个值可能会不同的,通过以下方法可以获得这个常量

      ViewConfiguration.get(this).getScaledTouchSlop();

VelocityTracker,GestureDetector和Scroller

VelocityTracker

速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度,它的使用过程很简单.首先,在View的onTouchEvent方法中追踪当前单机事件的速度.

VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
velocityTracker.computeCurrentVelocity(1000);//计算事件间隔是1秒
int xVelocity = (int) velocityTracker.getXVelocity();//x轴方向的速度
int yVelocity = (int) velocityTracker.getYVelocity();//y轴方向的速度

这里要注意两点,第一点,在获取速度之前必须先计算速度,也就是getXVelocity()和getYVelocity(),这两个方法之前必须调动computeCurrentVelocity(1000)这个方法.第二点,这里的速度是指一段时间内手指滑动的像素数,例如上边的代码把时间间隔设置为1000ms也就是1s,当手指在水平方向从左到右,划过100像素,那么水平速度就是100,注意这里速度可能是负数,因为如果手指从右向左滑动,就是负的.计算公式就是

速度=(起始位置-结束位置)/时间

根据上边的公式再加上Android的坐标系,可以知道手指逆着坐标系的正反向滑动.所产生的速度就是负值.另外,computeCurrentVelocity这个方法的参数表示的是.一个时间单元,或者是时间间隔,它的单位是毫秒(ms),计算速度时得到的速度就是在这个时间段内手指在水平或者垂直方向上滑动的像素数.
最后要在不需要它的时候,释放资源.

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

手势检测,用于辅助检测用户的单击,滑动,长按和双击等行为.使用起来也不复杂.

首先创建GestureDetector对象并实现OnGestureListener接口

/**
         * 第一个参数是上下文
         * 第二个参数是设置监听
         */
         GestureDetector gestureDetector=new GestureDetector(AActivity.this,this);

接着在目标的View的onTouchEvent()方法,在待监听View的onTouchEvent方法中添加以下代码:

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

做完上面的两步,我们就可以有选择的实现OnGestureListener和OnDoubleTapListener中的方法了,这两个方法介绍如下.
这里写图片描述
这里写图片描述
里边的方法很多,但是并不是所有的方法都会时常用到,在日常开发中,比较常用的有,onSingleTapUp(单击),onFling(快速滑动),onScroll(拖动),onLongPress(长按)和onDoubleTap(双击).

Scroller

弹性滑动对象,用于实现View的弹性滑动.我们知道,当我们使用View的scrollTo/scrollBy方法来进行滑动时,其过程是瞬间完成的,这个没有过度效果用户体验不好.这个时候就可以使用Scroller来实现有过度效果的滑动,其过程不是瞬间完成的,而是在时间间隔内完成的.Scroller本身无法让View弹性滑动,它需要和View的coumputerScroll方法配合使用才能共同完成功能,那么如何使用Scroller呢,如示例代码:

 Scroller scroller = new Scroller(context);

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

    /**
     * 移动到某一个位置
     *
     * @param x
     * @param y
     */
    public void smoothScrollTo(int x, int y) {
        int scrollx = getScrollX();//获取控件已经移动的距离
        int distance = x - scrollx;//获得需要移动的距离
        //1000ms滑动distance的距离
        scroller.startScroll(scrollx, 0, distance, 0, 1000);
    }

View的滑动

使用ScrollTo/ScrollBy

为了实现View的滑动,View提供了专门的方法,那就是ScrollTo和ScrollBy.
ScrollBy实际上也是调用ScrollTo方法,他实现了基于当前位置的相对滑动.而ScrollTo则实现了基于所传递参数的绝对滑动,这个不难理解.利用ScrollTo和ScrollBy来实现View的滑动,这不是一件很难的事情.

使用动画

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

改变布局参数

第三种改变View的位置就是改变布局参数,即改变LayoutParams.

各种滑动对比

以上三种方法都可以进行滑动,但是有什么区别呢?

先看scrollTo/scrollBy这种方式.它是View提供的原生方法,其作用是专门用于View的滑动,它可以比较简单的实现滑动效果,并且不影响内部元素的单击事件.但是它的缺点也是很显然的,它只能滑动View的内容,不能滑动View本身.

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

最后看一下改变布局的这种方式,它除了使用起来麻烦点之外,也没有明显的缺点,它的主要使用对象是一些具有交互性的View,因为这些View需要和用户交互,直接通过动画实现会有问题.所以这个时候我们需要使用直接改变布局参数的方法去实现.
  • scrollTo/scrollBy:操作简单,适合对View内容的滑动.
  • 动画:操作简单,主要使用于没有交互的View和实现复杂的动画效果.
  • 改变布局参数:操作稍微复杂,适用于有交互的View;

弹性滑动

知道了View的滑动,我们还要知道如何实现View的弹性滑动,比较生硬的滑动过去,这种方式给用户的体验太差了,因此我们需要渐进式的滑动,那么如何实现弹性滑动呢?其实实现方法有很多,但是他们都有一个共同思想:将一次大的滑动分成若干次小的滑动,并在一个时间段内完成,弹性滑动的具体实现方式有很多,比如通过Scroller,Handler#postDelayed以及Thread#sleep等.

使用Scroller

使用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滑动的呢?其实答案就是 invalidate();方法.因为它的调用会导致View的重绘,在View的draw方法中又回去调用computeScroll方法,computeScroll方法在View中是一个空实现,因此需要我们自己去实现.正是因为computeScroll方法,才让VIew实现了滑动.
原理是这样的:当View重绘后会在draw()方法中调用computeScroll方法,而computeScroll方法又会向Scroller获取当前的scrollX和scrollY;然后通过scrollTo方法实现滑动;接着调用 postInvalidate();进行第二次重绘….如此重复直到整个滑动结束.

 /**
     * Call this when you want to know the new location.  If it returns true,
     * the animation is not yet finished.
     */ 
    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和ScrollY的值.计算方法也很简单,就是按照时间流逝占总的时间的百分比来算出scrollX和ScrollY改变的百分比计算当前的值.这个方法的返回值也很重要,返回true表示滑动还没有结束,false表示滑动已经结束,因此当这个方法返回true时.我们要继续进行View的滑动.

总结:scroller本身并不能使View滑动,它需要配合View的computescroll方法才能完成弹性效果的滑动,它不断的让View重绘,而每一次重绘距离滑动起始时间会有一个间隔,通过这个时间间隔就可以得出View当前的hua’dong滑动位置,知道了滑动位置就可以通过scrollTo方法来完成View的滑动.就这样.View的每一次重绘都会导致View进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动,这就是Scroller的工作机制.

通过动画

动画本身就是一种渐进的过程,因此通过它来滑动,天然就有弹性的效果

  final int startX=0;
        final int endX=100;
        ValueAnimator valueAnimator=ValueAnimator.ofInt(0,1).setDuration(1000);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float animatedFraction = valueAnimator.getAnimatedFraction();
                scrollTo((int) (startX+endX*animatedFraction),0);
            }
        });
        valueAnimator.start();  

我们的动画本质并没有作用在任何对象上,它只是在1000ms内完成了动画.利用这个特性,我们可以在每次动画到来时,获取到动画完成的比例.然后根据这个比例计算出,要移动的距离,这里的滑动针对的是View的内容不是View本身,这个方法思想其实跟Scooller类似,都是通过改变一个百分比配合ScrollTo方法来完成View的滑动.说明一下,动画不仅仅是可以实现这样的效果,也可以实现其他的,可以在onAnimationUpdate做我们想做的可以做的操作.

使用延时策略

第三种实现弹性滑动的方法是延时策略.它的思想是通过发送一系列的延时消息从而达到一种渐进式的效果,具体来说可以使用Handler或者View的postDellayed方法,也可以使用线程的sleep方法.对于postDelayed方法来说,我们可以通过它来延时发送一个消息,然后在消息中来进行View的滑动,如果接连不断的发送这种延时消息,那么就可以是实现弹性滑动的效果.

View的事件分发机制

点击事件的传递规则

我们今天分析的对象就是MotionEvent,即点击事件.所谓的点击事件分发.其实是对MotionEvent的分发过程,即当一个MotionEvent产生之后,系统需要把事件传递给一个具体的View,而这个过程就是分发过程.点击事件的分发过程由三个很重要的方法,共同完成,dispatchTouchEvent();onTouchEvent();onInterceptTouchEvent();

public boolean dispatchTouchEvent(MotionEvent event);

用来进行事件的分发,如果事件能传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级的dispatchTouchEvent()方法影响,表示是否消耗当前事件,.

public boolean onTouchEvent(MotionEvent event) ;

在dispatchTouchEvent()方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一事件序列中,当前View无法再接收到事件.

public boolean onInterceptTouchEvent(MotionEvent ev) ;

用来判断是否判断某事件,如果当前View拦截了某事件,那么在同一事件序列中,此方法不会再被调用,返回结果表示是否拦截当前事件.

那么这三个方法有什么区别呢?它们是什么关系呢?

  @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        boolean isholdup = false;
        /**
         * 查看当前是否拦截事件传递
         */
        if (onInterceptTouchEvent(event)) {
            /**
             * 当前拦截事件
             */
            isholdup = onTouchEvent(event);
        } else {
            /**
             * 当前不拦截事件,进行下边子控件的传递
             */
            isholdup = child.dispatchTouchEvent(event);
        }
        return isholdup;
    }

通过上边的代码可以看出,对于一个viewGroup来说,点击事件产生后,首先传递给它,这时它的dispatchTouchEvent()方法就会被调用,如果它的onInterceptTouchEvent()方法返回false,表示它不拦截此事件,这时当前事件就会传递给它的子元素,接着子元素的dispatchTouchEvent()方法就会被调用,如此反复,直到事件最终被处理.

当一个VIew需要处理事件时,如果它设置了onTouchListener,那么OnTouchListener中的onTouch方法会被回调.这时事件如何处理还要看onTouch的返回值,如果返回false,则当前View的onTouchEvent方法会被调用;如果返回true,那么onTouchEvent方法不会被调用;由此可见,给View设置的onTouchListener,其优先级比onTouchEvent要高.在onTouchEvent方法中,如果当前设置的有onClicklistener,那么它的onclick方法会被调用,可以看出,平常我们常用的OnClickListener,其优先级最低,即处于事件传递的最末端.

当一个点击事件产生后,它的传递过程遵循如下顺序:Activity–>Window–>View,即事件总是先传递给Activity,Activity再传递给Window,最后Window传递给View.顶级View接收到事件后,就会按照事件分发机制去分发事件.有一种情况,如果View的onTouchEvent方法返回false,那么它的父容器的onTouchEvent将会被调用,以此类推,如果所有的元素都不处理这个事件,那么这个事件将会传递给Activity进行处理,即Activity的onTouchEvent方法将会被调用.

关于事件传递机制,总结一下

  • 同一事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最后个以up事件结束.
  • 正常情况下,一个事件序列只能被一个View拦截消耗,因为一旦一个元素拦截了某次事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一事件序列中不能分别由两个View同时处理,但是通过特殊手段可以做到,比如一个View讲本该自己处理的事情通过onTouchEvEvent强行传递给其他View处理.
  • 某一个View一旦决定拦截, 那么这一个事件序列只能由他来处理,并且它的onInterceptTouchEvent方法不会被调用,就是说当一个View决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给他来处理,因此就不会调用这个view的onInterceptTouchEvent方法询问是否处理拦截这个事件.
  • 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent方法返回false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent方法将会被调用.意思就是说事件一旦交给一个View处理,那么它就必须消耗掉,否则同一事件序列的其他事件就不会再交给他处理.
  • 如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件就会消失,此时父元素的onTouchEvent方法并不会被调用,并且当前View可以持续接收到后续的事件,最终这些消失的点击事件会传递给Activity处理.
  • ViewGroup默认不拦截任何事件.Android源码中的ViewGroup的onInterceptTouchEvent方法默认返回false.
  • View没有onInterceptTouchEvent方法,一旦点击事件传递给它,那么它的onTouchEvent方法就会被调用.
  • View的onTouchEvent方法默认都会消耗事件,除非它是不可点击的(clickable和longclickable同时为false).View的longClickable属性默认都是false,clickable属性要分情况,比如button的clickable属性默认为true,而textview的clickable属性默认为false.
  • View的enable属性不影响onTouchEvent的默认返回值,哪怕一个View是disable状态,只要她的clickable或者longclickable有一个为true,那么它的onTouchEvent就返回true.
  • onClick会发生的前提是当前view是可点击的,并且它收到了down和up事件.
  • 事件传递过程是由外向内的,即事件总是先传递给父元素,再由父元素,分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干扰父元素的事件分发过程,但是ACTION_DOWN事件除外.

    View的滑动冲突

//未完待续…


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值