Android开发艺术(3)——View的事件体系

2017-11-13  本文已影响12人  X_Sation

View的基础

View的位置参数

表示位置的几种参数

width = right - left;
x = left + translationX;

注意:View的平移其实只是改变了x/y和translationX/translationY,left/right/top/bottom还是不变。

x改变,如果left不变,translationX也会改变

MotionEvent和TouchSlop

VelocityTracker、GestureDetector和Scroller

VelocityTracker

可以称其为速度追踪器,就是获取手指滑动的速度的。当然在手机上,指的是一定时间内,手指划过的像素

使用:

  1. 首先需要在onTouchEvent中调用它,这样才能采集到手指在屏幕上的坐标变化信息
  2. 获取速度前需要先计算,传入时间,表示需要计算的时间长度
  3. 获取2传入的时长内滑过的像素,即使速度,不过这个速度的定义跟以往不同,只是像素长度而已
  4. 使用完后要释放

代码:

//初始化
    VelocityTracker velocityTracker = VelocityTracker.obtain();
//添加MotionEvent(在onTouchEvent中)
    velocityTracker.addMovement(event);
//计算速度 
    velocityTracker.computeCurrentVelocity(30);
//获取速度
    velocityTracker.getXVelocity();
//释放
    velocityTracker.clear();
    velocityTracker.recycle();

GestureDetector

手势监控,使用方式十分简单

//1.创建GestureDetector,注意只能在Looper Thread中使用,看源码可知,内部使用了handler
gestureDetector = new GestureDetector(Context,GestureDetector.OnGestureListener);
gestureDetector.setIsLongpressEnabled(false);//是否开启长按,如果开了,可以捕获到长按事件,但是长按后不可以捕获到滑动事件
//2.在view的onThouchEvent中让它接管事件,然后根据他的返回值决定是否消费(return true)
boolean resume = gestureDetector.onTouchEvent(event);
return resume;
//3.在OnGestureListener接口的各种方法就会被调用

这个类只是辅助类,帮助我们更容易的捕获到手势(双击、长按等,当然也可以自己写,也就是各种onTouchEvent中的处理)

Scroller

注意,Scroller实质是不断的调用scrollTo方法,所以就要了解scrollTo方法

scrollTo移动的是View的内容,所以,Scroller会给View的内容增加滚动效果

使用

//1.创建Scroller
scroller = new Scroller(context);
//2.重写View(需要滚动的东西所在的View,因为scrollTo滚动的是View的内容)的computeScroll方法 
public void computeScroll() {
    if (scroller.computeScrollOffset()) {//该次滚动是否执行完
    scrollTo(scroller.getCurrX(),scroller.getCurrY());
    //postInvalidate();//这里书上写错了,不需要调用他,因为scrollTo之后就会自动重绘了
    }
}
//3.调用Scroller的startScroller方法
scroller.startScroll(0,0,-10,-10,1000);
invalidate();//注意调用之后还需要调用这个方法,让view重绘,具体原因之后Scroller原理的方法

View的滑动

View滑动的三种方法:

ScrollTo/ScrollBy

scrollBy内部调用的是scrollTo,scrollTo其实就是修改mScrollX、mScrollY的值,然后调用invalidateParentCaches实现View的内容的位置改变

mScrollX:View的左边缘距离内容的左边缘的距离,内容边缘在右边,mScrollX为负,反之为正(正好相反)

三种对比

弹性滑动

Scroller

原理:

View的事件分发机制

以前是看博客,这次自己来搞,下载2.3的源码(先用的7.0、4.0的源码,实在看不懂,2.3确实简单点了),搞起

可以对着源码来看

ViewGroup:

public boolean dispatchTouchEvent(MotionEvent ev) {
    //过滤一些“错误”的事件,直接return false,
    if (!onFilterTouchEventForSecurity(ev)) {
            return false;
    }
    
    //FLAG_DISALLOW_INTERCEPT(一个标记,表示是否需要拦截事件,
    //这个是由childView来设置,通过它可以使chilidView拥有控制parentView
    //是否拦截事件的权利。7.0的源码中,这个值会在ACTION_DOWN的时候重置,2.3
    //的源码中暂未找到。这个标记一般在onTouchEvent的ACTION_DOWN之后的事件
    //中设置,所以即使重置也无影响)
    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

    //针对down做一些处理
    if (action == MotionEvent.ACTION_DOWN) {
        if (mMotionTarget != null) {
                // mMotionTarget是事件作用的View,down的时候,应该还没有它,
                // 这里不为空,所以是特殊情况,直接给他置空
                // this is weird, we got a pen down, but we thought it was
                // already down!
                // XXX: We should probably send an ACTION_UP to the current
                // target.
                mMotionTarget = null;
        }
        //childView不让parentView拦截  或者 自己的onInterceptTouchEvent返回false
        if(禁止拦截 或 !onInterceptTouchEvent(ev)){
            //不拦截,那就找合适的childView(位置之类的满足条件的),把事件分发给他们
            for(遍历childView){
                if(childView可接受事件——处于事件所在的坐标等条件满足){
                    //分发给childView(childView可以为viewgroup也可以为View)
                    //如果是viewGroup,就和现在分析的这套代码相同,否则稍后分析
                    if (child.dispatchTouchEvent(ev))  {
                        //这个孩子处理了事件了(可能是他自己处理的,也可能是他的孩子处理的)
                        // Event handled, we have a target now.
                        //给mMotionTarget赋值,表示事件将作用于它(这个它其实是与其他的子View)
                        //做区分的,因为事件可能最终被它的几重孙子消费,但一定是这个孩子的子孙
                        //不会是它的兄弟们
                        mMotionTarget = child;
                        //既然处理了,作为父亲,也return true,跟上行注释类似,他也告诉
                        //他的父亲,他处理了事件了(其实这里是他的孩子处理的)
                        return true;
                    }
                    //这个孩子(或者孩子的孩子)没处理,那就分发给下一个孩子,只要有一个
                    //后代处理了,上面就会return true,当前我们所分析的这个View的任务就完成了
                }
            }
            //所有的孩子、孙子都没有处理,自己来处理
        } 
    }
    
    //下面这两行代码&看不太懂,反正就是改变了mGroupFlags,而mGroupFlags在前面获取disallowIntercept
    //的时候用到过,再结合下面那个Note,再结合7.0源码中ACTION_DOWN后会重置FLAG_DISALLOW_INTERCEPT,
    //所以这里的意思大致应该是:如果UP活着CANCEL,就设置mGroupFlags,这将导致下次DOWN后
    //FLAG_DISALLOW_INTERCEPT变为“flase”,功能类似于7.0的重置
    //(*^__^*) 爽啊,从源码中找到答案的感觉真爽。。。
    boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
                (action == MotionEvent.ACTION_CANCEL);
    if (isUpOrCancel) {
        // Note, we've already copied the previous state to our local
        // variable, so this takes effect on the next event
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }


    //能走到这里有以下几种情况:
    //1.是DOWN且target==null(是DOWN且child不处理)
    //2.不是DOWN且target==null(自己处理)
    //3.不是DOWN,target!=null(DOWN已经由child处理,这个事件不是DOWN,孩子、自己都可以处理)
    

    //如果target==null,说明没有childView要处理事件,交由自己处理
    //如果target!=null,说明这个不是ACTION_DOWN且DOWN已经被child处理,这是一个非DOWN
    final View target = mMotionTarget;
    if(target == null){//没有childView处理
        //。。。
        //调用父类的dispatchTouchEvent(也就是这个ViewGroup当做普通View处理,自己处理事件)
        return super.dispatchTouchEvent(ev);
    }
    
    
    //能走到这里,根据前面那三条,不是DOWN,而且target不为空,也就是DOWN已经被孩子处理
    //这里的第一个参数,子View调用requestDisallowIntercept一般就是为了在这里出效果
    if (!disallowIntercept && onInterceptTouchEvent(ev)) {//要拦截了
    //这个if一般是这样的:当子View上正在触发非DOWN(比如MOVE)事件,然后把该View,也就是这个
    //子View的父亲的disallowIntercept改变了,就走到这里了,父亲就开始拦截,然后就像下面那样,
    //给孩子分发一个CANCEL的事件,然后把mMotionTarget置为空,并且return true,当下个事件来到
    //的时候,在上面那一步,target已经等于null了,这样这个ViewGroup就会自己去处理事件了,然后就
    //走到onTouchEvent了,所以在这里if里面不用考虑onTouchEvent
        ev.setAction(MotionEvent.ACTION_CANCEL);
        if (!target.dispatchTouchEvent(ev)) {
            //这里是空的
            //这里主要作用是:我要拦截了,那么我就给我的孩子分发一个事件,ACTION是ACTION_CANCEL
        }
        //拦截了,以后不会让孩子去处理了,把mMotionTarget清空 
        mMotionTarget = null;
        return true;
    }
    
    
    if (isUpOrCancel) {
        mMotionTarget = null;
    }
    
    //把事件分发给孩子(如果return false,就会调用它父亲的这一行,一直往上,都是return false,
    //最后就到了Activity了,下一段代码分析)
    return target.dispatchTouchEvent(ev);   
}

前面说了,如果dispatchTouchEvent返回了false,会一直往上return,直到被Activity消费,上代码

Activity:
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    //window的superDispatchTouchEvent 如果return false,就往下走,调用了activity的onTouchEvent
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

PhoneWindow:
public boolean superDispatchTouchEvent(MotionEvent event) {
    //调了mDecor.superDispatchTouchEvent(event)
    //其实就是ViewGroup的dispatchTouchEvent,回归到上一个代码分析了
    return mDecor.superDispatchTouchEvent(event);
}

综上,如果dispatchTouchEvent返回false,最终就会走到activity的onTouchEvent中

接下来分析View的dispatchTouchEvent

View:

//看着很简单
public boolean dispatchTouchEvent(MotionEvent event) {
    if (!onFilterTouchEventForSecurity(event)) {
        return false;
    }
    //有了onTouchListener,就执行onTouch,没有,或者return false,才会走onTouchEvent
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
            mOnTouchListener.onTouch(this, event)) {
        return true;
    }
    //其实onClick是在onTouchEvent中的,所以onTouch->onTouchEvent->onClick/onLongClick
    return onTouchEvent(event);
}

都说事件分发机制搞清楚三个方法就ok了

onTouchEvent:

//这个类只存在于View中,用来处理事件(ViewGroup一般会把事件分发给孩子,让孩子来处理,如果自己处理
//就通过super class来处理,也就是View,所以还是它)
public boolean onTouchEvent(MotionEvent event) {
    final int viewFlags = mViewFlags;

    //如果view是DISABLED,那么这个View不可以响应事件,即不可以对事件作出回应,但是他是会
    //消耗事件的:1.CLICKABLE 2.LONG_CLICKABLE,这里直接根据这两个条件return,不论true or false
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        return (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
    }

    //委托给另一个View去处理?没用过
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }

    //对事件处理,其实就是click和longclick(当然我们可以重写这个方法,但是对于view,系统
    //只帮忙处理这两个,scrollview等都是重写的)
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                //手指抬起,各种校验,最后决定调用点击、长按方法
                break;

            case MotionEvent.ACTION_DOWN:
                //按下
                if (mPendingCheckForTap == null) {
                    mPendingCheckForTap = new CheckForTap();
                }
                mPrivateFlags |= PREPRESSED;
                mHasPerformedLongPress = false;
                postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                break;

            case MotionEvent.ACTION_CANCEL:
                //事件取消,就会remveXXCallBack(),还会
                //refreshDrawableState();
                mPrivateFlags &= ~PRESSED;
                refreshDrawableState();
                removeTapCallback();
                break;

            case MotionEvent.ACTION_MOVE:
                //手指移出去(View的可点击范围),就会remveXXCallBack(),还会
                //refreshDrawableState();
                //...
                if ((x < 0 - slop) || (x >= getWidth() + slop) ||
                        (y < 0 - slop) || (y >= getHeight() + slop)) {
                    // Outside button
                    removeTapCallback();
                    if ((mPrivateFlags & PRESSED) != 0) {
                        // Remove any future long press/tap checks
                        removeLongPressCallback();
                        // Need to switch from pressed to not pressed
                        refreshDrawableState();
                    }
                }
                break;
        }
        return true;
    }
    //不是DISABLE,没有被委托的View处理,且不可点击(click 和 longclick),直接return false
    return false;
}

事件拦截

外部拦截法

在父容器的onInterceptTouchEvent中控制,一般return false,需要拦截的时候return true,一旦return true,子View就无法再接收到事件,所以在ACTION_DOWN中最好不要return true,而是在ACTION_MOVE中根据情况返回。在ACTION_UP中return false,因为它如果return true,子View就没法接收UP事件,click就会失效

内部拦截法

子View中通过requestDisallowInterceptTouchEvent来控制父View是否拦截,这个方法调用之后,父View只是“是否允许拦截”,还要看父View的onInterceptTouchEvent,所以为了让子View可以通过这个方法去控制父View的拦截,父View应该让onInterceptTouchEvent拦截非DOWN的事件。为什么不能拦截DOWN?因为拦截之后,事件就传不到子View了,内部拦截法也就无意义了

综上:内部拦截法更麻烦点,需要修改父View和子View的代码,所以一般采用外部拦截法

上一篇下一篇

猜你喜欢

热点阅读