View的事件体系

2018-12-05  本文已影响0人  发光的老金

参考资料《Android开发艺术探索》

什么是View?

View是android中所有控件的基类,是一种界面层的控件的一种抽象,它代表了一个控件。ViewGroup继承了View,以为这View本身可以是单控件也可以是由多个控件组成的一组控件,通过这种关系形成了View树的结构。

View的位置参数

View的位置主要有它的四个顶点来决定,分别对应着View的四个属性:top,left,right,buttom,其中top是左上角的纵坐标,left是左上角的横坐标,right是右下角的横坐标,bottom是右下角的纵坐标。这些坐标都是针对View的父容器来说,因此他是一种相对坐标。那么怎么得到这四个参数呢?
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也为它们提供了set/get方法。需要注意的是,View在平移的过程中,top和left表示的是原始左上角的位置信息,其值不会发生改变,此时发生改变的是x,y,translationX,translationY这四个参数。

MotionEvent和TouchuSlop

MotionEvent

在手指接触屏幕后所产生的一系列事件中,典型的事件有如下几种
ACTION_DOWN 手指刚接触屏幕
ACTION_MOVE 手指在屏幕上移动
ACTION_UP 手指从屏幕上松开的一瞬间
通过MotionEvent对象,我们可以得到点击事件发生的x和y坐标。为此,系统提供了两组方法:getX/getY和getRawX/getRawY,它们的区别其实很简单,getX/getY返回的是相对于当前的view左上角的x和y坐标,getRawX/getRawY返回的是相对于手机屏幕左上角的x和y坐标。

TouchSlop

TouchSlop是系统所能识别出的被认为是滑动的最小距离,也就是说,当手指在屏幕上滑动的时候,如果两次滑动之间的距离小于这个常量,那么系统就不认为你是在进行滑动操作。这个常量跟设备有关,可通过如下方法获得这个常量:ViewConfiguration.get(this).getScaledTouchSlop();可以利用这个常量做一些操作,好比两次滑动事件的距离小于这个值,我们就可以认为未达到滑动距离的临界值,就认为他不是滑动操作。

VelocityTracker,GestureDetector和Scroller

VelocityTracker

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

VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);

接着,当我们想知道当前的滑动速度时,这个时候可以采用如下方法来获得当前的速度:

velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();

在这一步有亮点需要注意,第一点,获取速度之前必须先计算速度,即getXVelocity和getYVelocity这两个方法之前必须要调用computeCurrentVelocity方法。第二点,这里的速度是指一段时间内手指所滑过的像素数,比如将时间间隔设为1000ms,在1s内,手指在水平方向从左向右滑过100像素,那么水平速度就是100。速度可以为负数,从右向左滑时,水平方向速度即为负值。

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

computeCurrentVelocity这个方法的参数表示的是一个时间单元或者说时间间隔,单位是毫秒(ms),计算速度时得到的速度就是在这个时间间隔内手指在水平或竖直方向上所滑动的像素数。
最后,当不需要使用它的时候,需要调用clear方法来重置并回收内存.

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

GestureDetector

手势检测,用于辅助检测用户的单击,滑动,长按,双击等行为。要是用GestureDetector,参考以下过程:
首先,需要创建一个GestureDetector对象并实现OnGestureListener接口,根据需要我们还可以实现OnDoubleTapListener从而能够监听双击行为:

        GestureDetector gestureDetector = 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;
            }
        });
        //解决长按屏幕后无法拖动的现象
        gestureDetector.setIsLongpressEnabled(false);

接着,接管目标view的onTouchEvent方法,在待监听的onTouchEvent方法中添加如下实现:

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

做完上面两步,就可以有选择性的实现OnGestureListener和OnDoubleTapListener中的方法了。
当然,实际开发中,也可以不使用GestureDetector,完全可以自己在view的onTouchEvent方法中实现所需的监听。

Scroller

弹性滑动对象,用于实现view的弹性滑动。我们知道scrollTo/scrollBy方法来进行滑动是,其过程是瞬间完成的,没有这个过渡效果的滑动用户体验很不好。这时候就可以使用Scroller来实现有过渡效果的滑动,其过程不是瞬间完成的,而是在一个时间间隔内完成的。Scroller本身无法让view弹性滑动,它需要和view的computeScroll方法配合才能共同完成这个功能。

View的滑动

掌握滑动是自定义控件的基础,通过三种方法可以实现滑动,分别说一下

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内部的两个属性mScrollX和mScrollY的改变规则。在滑动过程中,mScrollX的值总是等于view的左边缘和view内容的左边缘在水平方向上的距离,而mScrollY的值等于总是等于view的上边缘和view内容上边缘在竖直方向的距离。view边缘是值view的位置,由四个顶点组成,而view内容边缘是指view中的内容的边缘,scrollTo和scrollBy只能改变view内容的位置而不能改变view在布局中的位置,mScrollX和mScrollY的单位为像素,并且当view左边缘在view内容左边缘的右边时,mScrollX为正值,反之为负;当view上边缘在view内容上边缘的下边时,mScrollY为正值,反之为负。

使用动画

使用动画能够使一个view进行平移,而平移本身就是一种滑动。主要是操作view的translationX和translationY属性

 ObjectAnimator translationX = ObjectAnimator.ofFloat(
                imageView,
                "translationX",
                300
        );

        translationX.setDuration(1000);
        translationX.start();

表示1000ms单位内在水平方向移动了300个像素。

改变布局参数

即改变LayoutParams,这个比较好理解,就是我们想把一个view向右平移100px,我们只需要将这个view的LayoutParams里面的marginLeft的参数值增加100px即可

        ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) button.getLayoutParams();
        layoutParams.leftMargin +=100;
        button.setLayoutParams(layoutParams);

各种滑动方式的对比

scrollTo/scrollBy

操作简单,适合对view内容的滑动

动画

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

改变布局参数

操作稍微复杂,适用于有交互效果的view

View的事件分发机制

点击事件的传递规则

点击事件分析的对象实际就是MotionEvent,所以说点击事件的事件分发,其实就是对MotionEvent事件的分发过程。点击事件的事件分发由三个很重要的方法来共同完成dispatchTouchEvent(MotionEvent event), onInterceptTouchEvent(MotionEvent ev),onTouchEvent(MotionEvent event)

dispatchTouchEvent(MotionEvent event)

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

onInterceptTouchEvent(MotionEvent ev)

在上述方法内部调用,用来判断是否拦截某个事件,如果当前view拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。

onTouchEvent(MotionEvent event)

在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前view无法再次接受到事件。
三者关系是什么呢?书中给到一段伪代码

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

这样,大致可以了解点击事件的传递规则:对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,即它的onTouchEvent方法会被调用;如果这个ViewGroup的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方法会被调用。

结论:

1.同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个时间序列以down开始,中间含有数量不定的move,最终以up结束。
2.正常情况下,一个事件序列只能被一个view拦截且消耗。参考3,一个元素拦截了事件,那么同一事件序列内的所有事件都会交给他处理,因此同一个时间序列中的事件不能分别由两个view同时处理。但通过特殊手段可以做到,比如一个view将本该自己处理的事件通过onTouchEvent强行传递给其他view处理。
3.某个view一旦决定拦截,那么这一事件序列都只能由它来处理,并且它的onInterceptTouchEvent不会再被调用。
4.某个view一旦开始处理事件,如果它不消耗ACTION_DOWN事件,那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。
5.如果view不消耗ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前view可以持续收到后续的事件,最终这些消失的点击事件会传递给activity处理。
6.viewgroup默认不拦截任何事件。
7.view没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。
8.view的onTouchEvent默认都会消耗事件,除非它是不可点击的。
9.view的enable属性不影响onTouchEvent的默认返回值。哪怕一个view是disabled状态的,只要它的clickable或者longclick有一个为true,那么它的onTouchEvent就返回true。
10.onclick会发生的前提是当前view是可点击的,并且它收到了down和up事件。
11.事件传递过程是由外向内的,即事件总是先传递给父元素,然后在由父元素分发给子view,通过requestDisallowInterceptTouchEvent()方法可以在子元素中干预父元素的事件分发过程,ACTION_DOWN除外。

事件分发的源码解析

activity对点击事件的分发过程

点击事件用MotionEvent来表示,当一个点击操作发生时,事件最先传递给当前activity,由activity的dispatchTouchEvent来进行事件分发,具体工作是由activity的window完成的。window会讲事件传递给decorview,而decorview一般就是当前界面的底层容器,通过getWindow().getDecorView()可以获得。先从activity的dispatchTouchEvent开始分析

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

首先事件开始交给activity所附属的window进行分发,如果返回true,整个事件循环就结束了,返回false意味着事件没人处理,所有的view的onTouchEvent都返回了false,那么activity的onTouchEvent就会被调用。
接下来看window是如何将事件传递给viewgroup的。window是个抽象类,而window的superDispatchTouchEvent也是个抽象方法

public abstract boolean superDispatchTouchEvent(MotionEvent event);

window类可以控制顶级view的外观和行为策略,它的唯一实现位于android.policy.PhoneWindow这个类,当你要实例化这个window类的时候,你并不知道它的细节,因为这个类会被重构,只有一个工厂方法可以使用。

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

PhoneWindow将事件直接传递给了DecorView,由于DecorView继承自FrameLayout且是父view,所以最终事件会传递给view。

顶级view对点击事件的分发过程

首先看viewgroup对点击事件的分发过程,其主要实现在viewgroup的dispatchTouchEvent方法中,先看这段

  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;
            }

viewgroup在如下两种情况会判断是否要拦截当前事件。事件类型为ACTION_DOWN或者mFirstTouchTarget != null,mFirstTouchTarget != null是什么意思呢?从后面的代码逻辑中可以看出,当事件由viewgroup的子元素成功处理时,mFirstTouchTarget会被赋值并指向子元素。也就是说,viewgroup不拦截事件并将事件交由子元素处理时,mFirstTouchTarget != null。反过来,一旦事件由当前viewgroup拦截时,mFirstTouchTarget != null就不成立。那么当ACTION_DOWN和ACTION_UP事件到来时,由于(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)这个条件为false,将导致viewgroup的onInterceptTouchEvent不会在被调用,并且同一序列中的其他事件都会默认交给它处理。
有一种特殊情况,那就是FLAG_DISALLOW_INTERCEPT标记位,这个标记位是通过requestDisallowInterceptTouchEvent()方法来设置的,一般用于子view中。FLAG_DISALLOW_INTERCEPT一旦设置后,viewgroup将无法拦截除了ACTION_DOWN以外的其他点击事件(viewgroup在分发事件时,如果是ACTION_DOWN就会重置FLAG_DISALLOW_INTERCEPT这个标记位,将导致子view中设置的这个标记位无效)。因此,当面对ACTION_DOWN这个事件时,viewgroup总是会调用onInterceptTouchEvent方法来询问自己是否要拦截事件。在下面的代码中,viewgroup会在ACTION_DOWN事件到来时做重置状态的操作,而在 resetTouchState()方法中会对FLAG_DISALLOW_INTERCEPT进行重置,因此子view调用requestDisallowInterceptTouchEvent()方法并不能影响viewgroup对ACTION_DOWN事件的处理。

 if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

当viewgroup决定拦截事件,那么后续的点击事件将会默认交给它处理并且不在调用它的onInterceptTouchEvent方法。FLAG_DISALLOW_INTERCEPT这个标志的作用是让viewgroup不在拦截事件,当然前提是viewgroup不拦截ACTION_DOWN事件。

总结

1.onInterceptTouchEvent不会每次都调用,所以想提前处理所有点击事件,要使用dispatchTouchEvent方法
2.FLAG_DISALLOW_INTERCEPT标记位的作用给我们提供了一个思路,当面对滑动冲突时,可以考虑用这种方法来解决问题。

接着当viewgroup不拦截事件的时候,事件会向下分发交由它的子view进行处理

final View[] children = mChildren;
          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;
                            }

上面这段代码的逻辑也很清晰,首先遍历viewgroup的所有子元素,然后判断子元素是否能够接收到点击事件。是否能够接收到点击事件主要由两点来衡量:子元素是否在播动画和点击事件的坐标是否落在子元素的区域内。如果某个子元素满足这两个条件,那么事件就会传递给它来处理。可以看到,dispatchTransformedTouchEvent实际上就是调用子元素的dispatchTouchEvent,在它的内部有如下一段内容,而在上面的代码中child传递的不是null,因此它会直接调用子元素的dispatchTouchEvent方法 ,这样事件就交给了子元素处理,从而完成了一轮事件分发。

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

如果子元素的dispatchTouchEvent返回true,这时我们暂时不用考虑事件在子元素内部是怎么分发的,那么mFirstTouchTarget就会被赋值同时跳出for循环

newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;

这几行代码完成了mFirstTouchTarget的赋值并终止对子元素的遍历。如果子元素的dispatchTouchEvent返回false,viewgroup就会把事件分发给下一个子元素。
其实mFirstTouchTarget真正的赋值过程是addTouchTarget内部完成的,从下面addTouchTarget方法内部可以看出,mFirstTouchTarget其实是一种单链表结构。mFirstTouchTarget是否被赋值,将直接影响到viewgroup对事件的拦截策略,如果mFirstTouchTarget为null,那么viewgroup就默认拦截接下来同一序列中所有的点击事件。

    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

如果遍历所有的子元素后并没有被合适的处理,这包含两种情况。第一种是viewgroup没有子元素,第二种是子元素处理了点击事件,但是在dispatchTouchEvent中返回了false,这一般是因为在子元素在onTouchEvent中返回了false。这两种情况下,viewgroup会处理自己的点击事件。

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

上面这段代码,第三个参数child为null,从前面分析可知,他会调用super. dispatchTouchEvent(event),很显然,这里就转到了view的dispatchTouchEvent方法,即点击事件开始交由view来处理。

view对点击事件的处理过程

view对点击事件的处理过程稍微简单一些,注意这里的view不包含viewgroup,先看它的dispatchTouchEvent方法

 public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }

        boolean result = false;

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        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;
            }
        }

        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

view是一个单独的元素(不包含viewgroup),它没有子元素向下传递的事件,所以他只能自己处理事件。从上面的源码可以看出view对点击事件的处理过程,首先会判断有没有设置OnTouchListener,如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent方法就不会被调用,可以看见OnTouchListener的优先级高于onTouchEvent,这样做的好处是方便在外界处理点击事件。
接着再分析onTouchEvent的实现。先看view处于不可用状态下点击事件的处理过程,很显然,不可用状态下的view照样会消耗点击事件,尽管看起来不可用

final int action = event.getAction();

        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        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设置有代理,那么还会执行TouchDelegate的onTouchEvevt方法,这个onTouchEvevt的工作机制跟OnTouchListener类似,就不说了


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

再看一下onTouchEvent中对点击事件的具体处理,如下

final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                        }

                        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();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;

                    if (!clickable) {
                        checkForLongClick(0, x, y);
                        break;
                    }

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();

                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0, x, y);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    if (clickable) {
                        setPressed(false);
                    }
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    break;

                case MotionEvent.ACTION_MOVE:
                    if (clickable) {
                        drawableHotspotChanged(x, y);
                    }

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        // Remove any future long press/tap checks
                        removeTapCallback();
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }
                    break;
            }

            return true;
        }

从上面代码看,只要view的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗掉这个事件,即onTouchEvent返回true,不管他是不是DISABLE状态。然后是当ACTION_UP事件发生时,会触发performClick()方法,如果view设置了onClickListener,那么performClick()方法内部会调用它的onClick方法

    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;
    }

view的LONG_CLICKABLE属性默认为false,而CLICKABLE属性是否为false和具体的view有关。通过setClickable和setLongClickable可以分别改变view的CLICKABLE和LONG_CLICKABLE属性。另外,setOnClickListener会自动将view的CLICKABLE设置为true,setOnLongClickListener则会自动将view的LONG_CLICKABLE设为true。

    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;
    }

View的滑动冲突

常见的滑动冲突场景

1.外部滑动方向和内部滑动方向不一致
2.外部滑动方向和内部滑动方向一致
3.上面两种情况的嵌套

滑动冲突的处理规则

对于场景1,它的处理规则是:当用户左右滑动时,需要让外部的view拦截点击事件,当用户上下滑动时,需要让内部view拦截点击事件。这个时候我们就可以根据它们的特征来解决滑动冲突。具体来说是:根据滑动是水平滑动还是竖直滑动来判断到底由谁来拦截事件。可以根据水平方向跟竖直方向的距离差来判断,也可以根据速度来判断。
对于场景2,比较特殊,它无法根据滑动的角度,距离差以及速度差来做判断,但这个时候一般都能在业务上找到突破点,比如业务上规定:当处于某种状态时需要外部view响应用户滑动,而处于另一种状态时则需要内部view来响应view的滑动,根据这种业务上的需求我们就能得出相应的处理规则,有了处理规则就可以进行下一步的处理。
对于场景3,它的滑动规则就更复杂了,跟场景2一样,它也无法根据滑动的角度,距离差以及速度差来做判断,同样还是在业务上找突破点,具体方法跟2一样。

滑动冲突的解决方式

1.外部拦截法

所谓的外部拦截发是指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制。外部拦截法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可。伪代码如下

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int mLastXIntercept;
        int mLastYIntercept;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                intercepted = false;
                break;
            case MotionEvent.ACTION_MOVE:
                
                if(父容器需要当前点击事件){
                    intercepted = true;
                }else {
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
            default:
                break;
        }
        mLastXIntercept = x;
        mLastYIntercept = y;
        return

上述代码是外部拦截法的典型逻辑,针对不同的滑动冲突,只需要修改父容器需要当前点击事件这个条件即可,其他均不需要做修改也不能修改。在onInterceptTouchEvent这个方法中,首先是ACTION_DOWN这个事件,父容器返回false,即不拦截ACTION_DOWN事件,这是因为一旦父容器拦截了ACTION_DOWN,那么后续的ACTION_MOVE和ACTION_UP事件都会直接交给父容器处理,这个时候就没办法在传递给子元素了;其次是ACTION_MOVE事件,这个事件可以根据需要来决定是否拦截,如果父容器需要就返回true,否则返回false;最后是ACTION_UP事件,这里必须返回false,以为ACTION_UP事件本身没有太多意义。
考虑一种情况,假设事件交由子元素处,如果父容器在ACTION_UP时返回了true,就会导致子元素无法接收到ACTION_UP事件,这个时候子元素中的onClick事件就无法触发,但是父容器比较特殊,一旦它开始拦截任何一个事件,那么后续的事件都会交给它来处理,而ACTION_UP作为最后一个事件也必定可以传递给父容器,即便父容器的onInterceptTouchEvent方法在ACTION_UP时返回了false。

2.内部拦截法

内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理,这种方式和android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起来较外部拦截法稍显复杂。伪代码如下

    public boolean dispatchTouchEvent(MotionEvent ev) {
        int mLastXIntercept = 0;
        int mLastYIntercept = 0;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                this.getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastXIntercept;
                int deltaY = y - mLastYIntercept;
                if (父容器需要当前点击事件) {
                    this.getParent().requestDisallowInterceptTouchEvent(false);
                } 
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }
        mLastXIntercept = x;
        mLastYIntercept = y;
        return super.dispatchTouchEvent(ev);
    }

上述代码是内部拦截法的典型逻辑,针对不同的滑动冲突,只需要修改父容器需要当前点击事件这个条件即可,其他均不需要做修改也不能修改。除了子元素需要做处理之外,父元素也要默认拦截除了ACTION_DOWN以外的其他事件,这样子元素在调用this.getParent().requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需的事件。

上一篇下一篇

猜你喜欢

热点阅读