学习笔记| (三)View的事件体系

一、基础知识

1.1什么是View

View是所有控件的基类,ViewGroup包含很多View,即一组View,而ViewGroup又继承View,所以View既可以使单个的控件,也可以是一组控件;


View的树形结构.png

1.2.View的位置参数

1.View的位置由它的四个顶点决定,它们分别对应left、top、right、bottom。left是左上角横坐标,top是左上角纵坐标,right是右下角横坐标,bottom是右下角纵坐标;这几个坐标都是相对于父控件而言的相对坐标

View的位置坐标.png

2.view的宽度和坐标的关系:

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

3.获取坐标:

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

4.从android3.0开始,新增了几个属性x,y,translationX,transelationY
x和y是view左上角的坐标,translationX和translationY是view左上角相对于父容器的偏移量;

view新增的几个属性.png

坐标关系:
x = left + transelationX;
y = right + translationY;
①left和right是view的初始坐标,view绘制完成后就不会改变;
②x和y是view移动后左上角的坐标,会实时改变;

1.3.触控系列

1.MotionEvent是手指接触屏幕后产生的一系列事件:
ACTION_DOWM:手指刚接触屏幕
ACTION_MOVE:手指在屏幕上移动
ACTION_UP:手指离开屏幕的一瞬间

事件列:从手指接触屏幕开始到离开屏幕这中间发生的一系列事件,都是从Down开始,Up结束,中间有无数个move事件;

通过MotionEvent可以得到当前点击事件发生的x、y坐标,getX()/getY()、getRawX()/getRawY();
getX()/getY()是相对于当前view的左上角坐标;
getRawX()/getRawY()是相对于手机屏幕的左上角坐标

MotionEvent触摸事件.jpg

2.TouchSlop

是指系统所能识别的滑动的最小距离,也就是说,滑动距离小于这个值,系统就不认为是在滑动;
这是一个数值,但是在不同手机上的值不同;
获取方式:
int small = ViewConfiguration.get(this).getScaledTouchSlop();//16

3.VelocityTracker

指速度追踪,手指在滑动过程中的速度,可以检测水平和垂直方向的速度,速度可以为负数(从右往左滑动的时候)

使用:
在onTouchEvent事件中:

      //创建VelocityTracker实例
        VelocityTracker velocityTracker = VelocityTracker.obtain();
        
        //监听滑动事件
        velocityTracker.addMovement(event);

        //计算当前的速度(参数是ms),单位时间内滑动的像素个数,比如当前滑动了100个像素点,也就是说1s滑动100个像素点
        velocityTracker.computeCurrentVelocity(1000);

        //获取水平速度
        int xVelocity = (int) velocityTracker.getXVelocity();
        
        //获取垂直速度
        int yVelocity = (int) velocityTracker.getYVelocity();

      //不用的时候要回收
        velocityTracker.clear();
        velocityTracker.recycle();

4.GetureDetecor

手势检测,用于监听单击、双击、长按、滑动事件

使用:

       //监听手势
        final GestureDetector gestureDetector = new GestureDetector(this);
        
        //监听双击事件
        gestureDetector.setOnDoubleTapListener(this);

       mButton.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {

                return gestureDetector.onTouchEvent(event);
            }
        });
方法名 描述 事件 接口
onDown 手指轻触屏幕的一瞬间 1个onDown
onShowPress 手指轻触屏幕,还没有松开或移动 1个ACTION_DOWN OnGestureListener
onDown 手指轻触屏幕的一瞬间 1个ACTION_DOWN OnGestureListener
onSingleTapUp 手指轻触屏幕后松开 1个ACTION_UP OnGestureListener
onScroll 手指按下屏幕后移动 n个ACTION_MOVE OnGestureListener
onFling 触摸屏幕,快速滑动后松开 1个ACTION_Down,n个ACTION_MOVE,1个ACTION_UP OnGestureListener
onLongPress 长按屏幕 1个onDown OnGestureListener
onSingleTapConfirmed 严格的单击事件,触发了它,后面就不可能再跟着一个单击事件 OnDoubleTapListener
onDoubleTap 双击事件,和onSingleTapConfirmed不共存 OnDoubleTapListener
onDoubleTapEvent 表示双击行为发生了 ACTION_DOWN、ACTION_MOVE、ACTION_UP都会回调这个方法 OnDoubleTapListener

建议:如果只是监听滑动相关,建议自己在onTouchEvent()中实现;如果要监听双击事件,就用OnGestureListener

5.Scroller
弹性滑动对象

二、View的滑动

2.1使用ScrollTo/ScrollBy

  • ScrollTo/ScrollBy只能改变View内容的位置,而不能改变View在布局中的位置;
  • ScrollBy的内部调用了ScrollTo
  • 二者的区别:ScrollBy是基于当前位置的相对滑动,ScrollTo是基于所传位置的绝对滑动;

mScrollX和mScrollY分别表示View在X、Y方向的滚动距离。mScrollX:View的左边缘减去View的内容的左边缘;
mScrollY:View的上边缘减去View的内容的上边缘。
从右向左滑动,mScrollX为正值,反之为负值;
从下往上滑动,mScrollY为正值,反之为负值。
(更直观感受:查看下一张照片或者查看长图时手指滑动方向为正)


scrollBy和ScrollTo.jpg

2.2使用动画

//方法1   使用View动画
mButton.startAnimation(AnimationUtils.loadAnimation(this,R.anim.move));

 //方法2:使用属性动画
ObjectAnimator.ofFloat(mButton,"translationX",0,100).setDuration(100).start();

区别:

  • View动画是对View的影像做操作,并不能真正改变View的位置参数和宽高,移动后就无法触发onClick事件,在原来的位置还可以触发;
  • 属性动画可以解决这个问题,但是3.0以下无法使用属性动画,可以使用nineoldandroids来实现属性动画,但是本质上仍然是View动画

2.3改变布局参数

ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mButton.getLayoutParams();
params.leftMargin += 100;

//mButton.requestLayout();
 mButton.setLayoutParams(params);

2.4三种方式的对比

  • ScrollTo和ScrollBy可以方便实现滑动,但是只能滑动View的内容,不能滑动View;
  • 动画:操作简单,适合于没有交互的View或者是复杂的动画
  • 改变布局参数:操作稍微复杂,适合有交互的View

三、弹性滑动

核心思想:将一个大的滑动,分成若干个小的滑动,并在一段时间内完成;

3.1使用Scroller

典型的使用方法:

   /**
     * 缓慢滑动
     * @param destX
     * @param destY
     */
    private void smoothScrollTo(int destX, int destY) {
        int scrollX = getScrollX();//view左边缘和view内容左边缘的距离
        int scrollY = getScrollX();//view上边缘和view内容上边缘的距离

        int deltaX = destX - scrollX;//x方向上的位移量
        int deltaY = destY - scrollY;//y方向上的位移量

        scroller.startScroll(scrollX,scrollY,deltaX,deltaY,1000);//开始滑动
        invalidate();//重绘View,刷新界面
    }

startScroll源码:
只是将我们传进去的数据进行保存,这里仍然是view内容的滑动

 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;//x滑动距离
        mDeltaY = dy;//y滑动距离
        mDurationReciprocal = 1.0f / (float) mDuration;
    }

使用场景:在ACTION_UP触发的时候会调用startScroll()进行数据的保存--->调用invalidate重绘view--->view的draw()方法会调用computeScroll(),这个方法要自己实现;
实现过程:先判断computeScrollOffset,若为true(表示滚动未结束),则执行scrollTo方法,它会再次调用postInvalidate,如此反复执行,直到返回值为false。

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

3.2使用动画

只能改变view的内容

        final int startX = 0;
        final int deltaX = 100;

        final ValueAnimator valueAnimator = ValueAnimator.ofInt(0,1).setDuration(1000);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //获取当前动画的执行进度
                float fraction = valueAnimator.getAnimatedFraction();
                Log.e("testslide","fraction:"+fraction);//0.008856356-->1.0
                mButton.scrollTo((int) (startX+deltaX*fraction),0);
            }
        });

        valueAnimator.start();

3.3使用延时策略

  • 使用handler
  • 使用sleep
private static final int MESSAGE_SLIDE = 0;
    private int allCount = 30;
    private int delayTIme = 33;
    private int count;
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
                case MESSAGE_SLIDE:
                    count++;

                    if (count < allCount){
                        //完成进度,注意要用float
                        float fracsaction = count/(float)allCount;
                      
                        mButton.scrollTo((int) (fracsaction*100),0);

                        //33ms后再发送一次消息
                        sendEmptyMessageDelayed(MESSAGE_SLIDE,delayTIme);
                    }
                    break;
                default:
                    break;
            }
        }
    };

    public void slideByDelay(View view) {
        mHandler.sendEmptyMessage(MESSAGE_SLIDE);
    }

四、事件分发机制

4.1点击传递规则

点击事件的事件分发,实质上就是MotionEvent事件的分发,当一个点击事件发生的时候,系统需要将这个事件传递给一个具体的View,这个过程就叫事件分发;

三个重要方法:
dispatchTouchEvent(MotionEvent event):
用于事件的分发,如果事件能传递给当前View,则这个方法一定会被调用,它的返回结果受当前view的onTouchEvent()和下级view的dispatchTouchEvent()的影响,表示是否消耗当前事件;
onInterceptTouchEvent(MotionEvent ev)
在dispatchTouchEvent中调用,该方法只在ViewGroup中有,表示是否拦截这个事件,如果拦截了,那么在同一个事件序列中,此方法不会再被调用,返回结果表示是否拦截了当前事件;
onTouchEvent(MotionEvent event)
在dispatchTouchEvent中调用,用于处理点击事件,返回结果表示是否消耗了当前事件,如果不消耗,那么在同一个事件序列中,当前view无法再次接收到事件;

   @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean consume;
        if (onInterceptTouchEvent(ev)){
            consume = onTouchEvent(ev);
        }else {
            consume = child.dispatchTouchEvent(ev);
        }
        
        return consume;
    }

传递规则:对于根ViewGroup而言,当一个点击事件发生的时候,首先会传递给它,它的dispatchTouchEvent会被调用,如果这个ViewGroup的onInterceptTouchEvent返回true,表示ViewGroup拦截了这个事件,会调用自己的onTouchEvent方法进行处理;否则,会调用子元素的dispatchTouchEvent,如此下去,知道该事件被消费;

注意

  • ①当一个View设置了setOnTouchListener,那么它的onTouch事件会被回调,这时候事件如何处理,要看onTouch的返回值,(返回值默认为false),返回值为true时,onTouchEvent()不会被调用;返回值为false,onTouchEvent()会被调用;在onTouchEvent()中,如果设置了onClickListener(),则它的onClick会被调用,也就是说onClickListener的优先级最低,处于事件传递的底端。
    优先级:onTouchListener > onTouchEvent() > onClickLisener()

    touch事件传递.png

  • ②当一个点击事件产生的时候,传递顺序是:
    Activity-->Window-->View,传到顶级View后,再按事件分发机制分发,如果这个View的onTouchEvent()返回false,那么它父容器的onTouchEvent()将会被调用,如果所有元素都不处理,那么最后将会传到Activity处,则Activity的onTouchEvent()会被调用。


    传递顺序.jpg

理解:我的上级接到了一个任务,他不想做,就分给我了,会调用我的onTouchEvent(),最后我实在解决不了了,只能返回false,然后让上级去解决,然后会调用上级的onTouchEvent();

结论:

  • 同一个事件序列,是指手指接触屏幕的一瞬间起,到离开屏幕的一刻为止,中间发生的一系列事件,以DOWN开始,中间经过n个MOVE事件,以UP结束;
  • ②某个view一旦决定拦截,那么这个事件序列都由他来处理,并且它的onInterceptTouchEvent()不会再被调用了;
  • ③正常情况下,一个事件序列只能被一个View拦截且消耗
  • ④某个View一旦开始处理事件,如果它不处理ACTION_DOWN事件(也就是说它的onTouchEvent()返回false),那么同一事件序列中的其他事件都不会再交于它处理,并且把事件交由它的父类处理
  • ⑤如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,同时不会调用父类的onTouchEvent()方法,后续还是会持续收到事件,这些消失的事件最后会传递给Activity处理
  • ⑥ViewGroup默认不拦截任何事件,onInterceptTouchEvent()默认返回false;
  • View没有onInterceptTouchEvent(),一旦有事件传过来,就会调用onTouchEvent();
  • View的onTouchEvent()默认都会消耗该事件(返回true),除非它是不可点击的(clickable和longClickable为false),View的longClickable默认为false,clickable要分情况:Button默认为true,TextView默认为false;
  • ⑨View的enable不影响onTouchEvent()的返回值,即便一个View enable为false,只要clickablelongClickable有一个为true,onTouchEvent()返回值就为true;
  • ⑩onClick发生的前提是当前view可点击,并且受到了down和up事件;
  • 11.事件传递是由外到内的,总是由父类分发给子view,通过requestDisallowInterceptTouchEvent()可以影响父元素的分发,但是ACTION_DOWN除外;

4.2源码分析

①当一个点击事件发生的时候,事件最先传递给Activity,Activity的dispatchTouchEvent()会调用进行事件分发:

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        //②交给附属的Window进行分发,如果返回true,则结束循环,否则,这个事件没人处理,所有view的onTouchEvent()返回false,就会交给Activity的onTouchEvent()处理;
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

window是一个抽象类,superDispatchTouchEvent也是一个抽象方法,它的实现类是PhoneWindow;

   @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

在PhoneWindow中,将事件传递给了mDecor;

// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;

那什么是DectorView呢?
通过((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0);可以获取当前Activity设置的View,那么getWindow().getDecorView()获取到的就是DectorView,Activity通过setContentView设置的View是DectorView的子View;

事件传递到DectorView后,查看DectorView的源码:

public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
  .....
}

DecorView 继承FrameLayout 且是父View,最终事件会传递给View。从这里开始事件已经传递到了顶级View(根View),即通过setContentView设置的View。

public class FrameLayout extends ViewGroup {
  ...
}

顶级View对点击事件的分发:
点击事件到达顶级View后,会调用ViewGroup的dispathchTouchEvent()方法,如果onInterceptTouchEvent返回true拦截了该事件,则事件交由ViewGroup处理;这时,如果ViewGroup设置了onTouchListener,会调用onTouch()方法,不会调用onTouchEvent()方法,否则,调用onTouchEvent()方法,如果设置了onClickListener事件,则onClick会被调用;如果ViewGroup没有拦截该事件,则会传给给所在点击事件链上的子View,这时,子View的dispatchTouchEvent()会被调用,一直循环,直到该事件被消耗;

在ViewGroup的DispatchTouchEvent()中有这样一段代码:

if (actionMasked == MotionEvent.ACTION_DOWN) {
    cancelAndClearTouchTargets(ev);
    resetTouchState();
}

// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {

    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action); // restore action in case it was changed
    } else {
        intercepted = false;
    }
    
} else {
    // There are no touch targets and this action is not an initial down
    // so this view group continues to intercept touches.
    intercepted = true;
}
/**
 * Resets all touch state in preparation for a new cycle.
 */
private void resetTouchState() {
    clearTouchTargets();
    resetCancelNextUpFlag(this);
    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    mNestedScrollAxes = SCROLL_AXIS_NONE;
}

从上面的二代码可以看到有两种情况下回判断是否拦截当前事件,actionMasked == MotionEvent.ACTION_DOWN或者mFirstTouchTarget != null;
从后面的代码可以知道当前事件由ViewGroup的子View处理时,mFirstTouchTarget !=null,也就是说,当ViewGroup自己处理的时候,mFirstTouchTarget =null

当ViewGroup拦截了当前事件,当MOVE事件到来的时候,if(true || false ){}会执行,当MOVE/UP事件再来的时候,if(false || false),则else中的内容会执行,onInterceptTouchEvent再不会被调用,所以证明了上面的那条结论,③正常情况下,一个事件序列只能被一个View拦截且消耗;

这里也有一种特殊情况,就是FLAG_DISALLOW_INTERCEPT这个标志位,它是在子View中通过requestDisallowInterceptTouchEvent(true);来设置的,它的源码是:

@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    ....

    if (disallowIntercept) {
        mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
    } else {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }
    ...
}

如果在子View中设置了requestDisallowInterceptTouchEvent,则ViewGoup只能拦截Down事件,因为在ViewGroup的onDispatchTouchEvent()中,Down事件到来的时候会调用resetTouchState();重置FLAG_DISALLOW_INTERCEPT标志位,也就是说,即便这时候子View设置了requestDisallowInterceptTouchEvent也无效,down事件来的时候总是会调用ViewGroup的onInterceptTouchEvent方法

①设置了true
mGroupFlags = mGroupFlags | FLAG_DISALLOW_INTERCEPT;

②down事件来的时候会重置标志位:
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
-->mGroupFlags = mGroupFlags &( ~FLAG_DISALLOW_INTERCEPT);
-->mGroupFlags = mGroupFlags | FLAG_DISALLOW_INTERCEPT &( ~FLAG_DISALLOW_INTERCEPT);
-->boolean disallowIntercept = (mGroupFlags | FLAG_DISALLOW_INTERCEPT &( ~FLAG_DISALLOW_INTERCEPT) & FLAG_DISALLOW_INTERCEPT)

假设mGroupFlags一开始为000,FLAG_DISALLOW_INTERCEPT为111
则boolean disallowIntercept = (000 | 111 & (~111) & 111) = 0 

所以down事件来的时候,disallowIntercept = false-->onInterceptTouchEvent()

③UP或MOVE事件来的时候,不会重置标志位:
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
-->mGroupFlags = mGroupFlags | FLAG_DISALLOW_INTERCEPT;

boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) 
-->boolean disallowIntercept = (mGroupFlags | FLAG_DISALLOW_INTERCEPT & FLAG_DISALLOW_INTERCEPT) 

假设mGroupFlags一开始为000,FLAG_DISALLOW_INTERCEPT为111
则boolean disallowIntercept = (000 | 111 & 111) = 1 -->true
-->
intercepted = false;

根据上面的源码可以得出结论:
①当ViewGroup决定拦截该直接后,后续的点击事件都会交给它处理;
②FLAG_DISALLOW_INTERCEPT是为了让ViewGroup不再拦截该事件,前提是 不拦截down事件

当ViewGroup不拦截这个事件的时候,会交给它的子View进行处理

if (!canceled && !intercepted){
    .....
for (int i = childrenCount - 1; i >= 0; i--) {
    final int childIndex = getAndVerifyPreorderedIndex(
            childrenCount, i, customOrder);
    final View child = getAndVerifyPreorderedView(
            preorderedList, children, childIndex);

    // If there is a view that has accessibility focus we want it
    // to get the event first and if not handled we will perform a
    // normal dispatch. We may do a double iteration but this is
    // safer given the timeframe.
    if (childWithAccessibilityFocus != null) {
        if (childWithAccessibilityFocus != child) {
            continue;
        }
        childWithAccessibilityFocus = null;
        i = childrenCount - 1;
    }

    if (!canViewReceivePointerEvents(child)
            || !isTransformedTouchPointInView(x, y, child, null)) {
        ev.setTargetAccessibilityFocus(false);
        continue;
    }

    newTouchTarget = getTouchTarget(child);
    if (newTouchTarget != null) {
        // Child is already receiving touch within its bounds.
        // Give it the new pointer in addition to the ones it is handling.
        newTouchTarget.pointerIdBits |= idBitsToAssign;
        break;
    }

    resetCancelNextUpFlag(child);
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
        // Child wants to receive touch within its bounds.
        mLastTouchDownTime = ev.getDownTime();
        if (preorderedList != null) {
            // childIndex points into presorted list, find original index
            for (int j = 0; j < childrenCount; j++) {
                if (children[childIndex] == mChildren[j]) {
                    mLastTouchDownIndex = j;
                    break;
                }
            }
        } else {
            mLastTouchDownIndex = childIndex;
        }
        mLastTouchDownX = ev.getX();
        mLastTouchDownY = ev.getY();
        newTouchTarget = addTouchTarget(child, idBitsToAssign);
        alreadyDispatchedToNewTouchTarget = true;
        break;
    }

    // The accessibility focus didn't handle the event, so clear
    // the flag and do a normal dispatch to all children.
    ev.setTargetAccessibilityFocus(false);
}
.....
}

根据代码可知,会先遍历ViewGroup的子View,如果子元素在播放动画,并且触摸点在子元素的范围内,则子元素能够接收这个事件,然后调用dispatchTransformedTouchEvent(),在它的内部有这样一段代码:

if (child == null) {
    handled = super.dispatchTouchEvent(event);
} else {
    handled = child.dispatchTouchEvent(event);
}

可以知道,dispatchTransformedTouchEvent()内部其实调用的是dispatchTouchEvent,如果dispatchTransformedTouchEvent()返回值为true,这会执行以下这段代码:

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
    ....
    newTouchTarget = addTouchTarget(child, idBitsToAssign);
    alreadyDispatchedToNewTouchTarget = true;
    break;
    ...
}
      |  |
      |  |
      ↓  ↓
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}

根据上面的代码可以知道,这时候mFirstTouchTarget 就被赋值了,同时会跳出for循环(不再执行下面的子View了),如果dispatchTransformedTouchEvent()返回值false,就会把事件分给下一个子元素(如果有的话);

如果遍历了所有的子元素后都没有被合适的处理这个事件,那么有两种情况:
①这个ViewGroup没有子View
dispatchTransformedTouchEvent()返回false,这一般都是子View的onTouchEvent()返回false;

if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
}

  ↓  ↓  ↓

if (child == null) {
    handled = super.dispatchTouchEvent(event);
} else {
    handled = child.dispatchTouchEvent(event);
}

上面的这段代码就证明了:

④某个View一旦开始处理事件,如果它不处理ACTION_DOWN事件(也就是说它的onTouchEvent()返回false),那么同一事件序列中的其他事件都不会再交于它处理,并且把事件交由它的父类处理;
说完顶级View的事件分发之后,再来看看View对点击事件的处理过程:

注意:这里的View不包括ViewGroup
先分析它的dispatchTouchEvent()方法:

public boolean dispatchTouchEvent(MotionEvent event) {
....

boolean result = false;

....

final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
    // Defensive cleanup for new gesture
    stopNestedScroll();
}

if (onFilterTouchEventForSecurity(event)) {
    if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
        result = true;
    }
    //noinspection SimplifiableIfStatement
    ListenerInfo li = mListenerInfo;
   
    //①
    if (li != null && li.mOnTouchListener != null
            && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnTouchListener.onTouch(this, event)) {
        result = true;
    }

   //②
    if (!result && onTouchEvent(event)) {
        result = true;
    }
}

....
return result;
}

因为View是一个单一的元素,他没有子元素可以向下传递,所以事件传到他这的时候只能自己处理;根据①②可知,会先判断View是否设置了onTouchListener事件,如果设置了,并且onTouch()返回true,则②中的 onTouchEvent(event)不会执行,这里注意&&中只要前者为false,后面的就不会再执行

结论:onTouch的优先级高于onTouchEvent

接下来看看View的onTouchEvent事件,先看看不可点击的情况下:

if ((viewFlags & ENABLED_MASK) == DISABLED) {
    if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
        setPressed(false);
    }
    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    // A disabled view that is clickable still consumes the touch
    // events, it just doesn't respond to them.
    return clickable;
}

根据上面的代码可以看到,虽然此时View不可点击,但是它仍然会消耗点击事件(仍然走了onTouchEvent);

if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
        return true;
    }
}

当View设置了代理的时候,会执行代理的onTouchEvent()事件,和onTouchListener一样;

下面看看View对具体的事件是如何处理的:

/**
 * <p>Indicates this view can display a tooltip on hover or long press.</p>
 * {@hide}
 */
static final int TOOLTIP = 0x40000000;

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
    switch (action) {
        case MotionEvent.ACTION_UP:
            ...
           
            if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                ...

                if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                    // This is a tap, so remove the longpress check
                    removeLongPressCallback();

                    // Only perform take click actions if we were in the pressed state
                    if (!focusTaken) {
                        // Use a Runnable and post this rather than calling
                        // performClick directly. This lets other visual state
                        // of the view update before click actions start.
                        if (mPerformClick == null) {
                            mPerformClick = new PerformClick();
                        }
                        if (!post(mPerformClick)) {
                            performClick();
                        }
                    }
                }
                ....
            }
          
            break;

        ....
    }
    return true;
}

从上面的代码可以看出,只要是点击或长按事件中的一个为true,都会
消耗这个事件,onTouchEvent()都会返回true,不管它设置的是不是disable状态,也就证明了下面的几个结论:

⑧View的onTouchEvent()默认都会消耗该事件(返回true),除非它是不可点击的(clickable和longClickable为false),View的longClickable默认为false,clickable要分情况:Button默认为true,TextView默认为false;
⑨View的enable不影响onTouchEvent()的返回值,即便一个View enable为false,只要clickable或longClickable有一个为true,onTouchEvent()返回值就为true;
⑩onClick发生的前提是当前view可点击,并且受到了down和up事件

在上面的代码中,可以看到,当UP事件发生的时候,会执行PerformClick

private final class PerformClick implements Runnable {
    @Override
    public void run() {
        performClick();
    }
}

public boolean performClick() {
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

    notifyEnterOrExitForAutoFillIfNeeded(true);

    return result;
}

从上面代码可以看到,当设置了onClickListener事件的时候,会执行onClick方法;

View的LONG_CLICKABLE默认为false,CLICKABLE的值要根据具体的View来定,比如TextView是不可点击的,Button是可点击的;

public void setOnClickListener(@Nullable OnClickListener l) {
   if (!isClickable()) {
       setClickable(true);
   }
   getListenerInfo().mOnClickListener = l;
}

public void setOnLongClickListener(@Nullable OnLongClickListener l) {
   if (!isLongClickable()) {
       setLongClickable(true);
   }
   getListenerInfo().mOnLongClickListener = l;
}

通过源码可以看到,可以通过这两个方法设置点击可长按事件,并且在设置的时候,会自动将LONG_CLICKABLE和CLICKABLE设置为true

五、滑动冲突

5.1常见的冲突场景

①外部滑动和内部滑动的方向不一致

②外部滑动和内部滑动的方向一致

③上面两种情况嵌套

5.2处理规则

①对于场景1,左右滑动的时候,由内部控件处理;上下滑动的时候由外部控件处理;至于如何判断是水平滑动还是垂直滑动,可以计算水平滑动差和竖直滑动差;
②场景2和场景3都比较复杂,不能根据角度和滑动的方向来判断谁拦截,只能根据具体的业务需求来定;

5.3处理方式

①对于场景1有两种方式处理:

  • 外部拦截法:
    即在外部控件判断是否拦截
外部拦截:
在父容器中重写onInterceptTouchEvent()方法:

伪代码如下:
Public boolean onInterceptTouchEvent(Event event){
    boolean intercepted = false;

     //获取开始的相对坐标
    int x = (int) event.getX();
    int y = (int) event.getY();

    switch(event.getAction()){
        case down:
             intercepted = false;//如果为true,那么后续的事件就都会交给父容器处理了
             break;
        case move:
            //水平差
            float detX = mLastXIntercept - x;
            //垂直差
            float detY = mLastYIntercept - y;

             if(Math.abs(detX) > Math.abs(detY)){
                intercepted = true;
             }else{
                intercepted = false;
             }
             break;
        case up:
              //这里必须返回false,因为这里没有太大的意义
              intercepted = false;
              break;
    }

        mLastXIntercept = x;
        mLastYIntercept = y;
    return intercepted;

}

问题:如果在up时返回了true会怎么样?
答:up时返回true,也就是说这个点击事件还是会交给父容器处理,子元素是接收不到这个事件的,它的onClick()也就执行不了了。
  • 内部拦截法:
    即父容器全部都不拦截,都交由子View,如果子View需要处理这事件,就消耗;
重写子View的dispatchTouchEvent():
伪代码如下:
public boolean dispatchTouchEvent(MotionEvent event) {
    int x = (int) ev.getX();
    int y = (int) ev.getY();

    switch (event.getAction()){
        case MotionEvent.ACTION_DOWN:
            //不拦截,因为对于父容器来说,设置了也无效
            getParent().requestDisallowInterceptTouchEvent(false);
            break;
        case MotionEvent.ACTION_MOVE:
            //水平差
            int deltaX = x - mInterceptX;
            //垂直差
            int deltaY = y - mInterceptY;
            if (Math.abs(deltaX) > Math.abs(deltaY)){
                getParent().requestDisallowInterceptTouchEvent(false);
            }
            break;
        case MotionEvent.ACTION_UP:
            break;
    }

     mInterceptX = x;
     mInterceptY = y;

    return super.dispatchTouchEvent(event);
}

父容器中要这样处理:
public boolean onInterceptTouchEvent(MotionEvent ev) {
    int event = ev.getAction();
    if (event == MotionEvent.ACTION_DOWN){
        //如果是down事件不拦截
        return false;
    }else {
        return true;
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容