View的事件体系(基础+滑动+弹性滑动+事件分发+滑动冲突)《Android开发艺术探索》超详细笔记

1 View的基础知识

1.1什么是View

View是所有控件的基类,View是一种界面层的控件的一种抽象,它代表了一个控件。ViewGroup是一个控件组,内部包含多个View,同时ViewGroup也是继承自View,这样就形成了View树结构。Button就是一个View,而LinearLayout是一个View,同时还是一个ViewGroup。ViewGroup内部可以是单个View,也可以是一个ViewGroup。

1.2位置参数

View的位置参数包括,top,bottom,left,right,它们分别代表的是与父容器之间的相对距离。值得一提的是,x轴是向右,y轴是向下的。

  • left: View 左上顶点相对于父容器的横坐标
  • top: View 左上顶点相对于父容器的纵坐标
  • right: View右下顶点相对于父容器的横坐标
  • bottom: View 右下顶点相对于父容器的纵坐标
    在这里插入图片描述
    View同时还具有x,y,translationX,translationY,x和y是View左上角的坐标(相对于父容器),但往往x和y可能不等于left与top,原因就是translationX与translationY,这个是View左上角相对于父容器的偏移量。
x = translationX+left
y = translationY+top

下图就可以很好的体现出translation的作用,原本的父容器相当于是黑框,而有了translationX、Y之后的父容器相当于变成了绿框,而我们的View与绿框之间的距离不变,仍然为left与top。可以从图中看出下列式子。

x = translationX+left
y = translationY+top

在这里插入图片描述
因此我们看出在每次平移(有translation的时候),top与left都不会改变,而xy会相应发生变化。

1.3 MotionEvent与Touchslop

1.MotionEvent
手指滑动包含三个状态:手指接触屏幕,手指在屏幕移动,手指移出屏幕
ACTION_DOWN:手指接触屏幕
ACTION_MOVE:手指在屏幕移动
ACTION_UP:手指移出屏幕
包含两种情况:
1)DOWN->UP
2)DOWN->MOVE->MOVE->MOVE->UP
通过MotionEvent对象可以获取点击事件的坐标:getX与getY(相对于当前View左上角的x与y坐标);getRawX与getRawY(相对于手机屏幕左上角的x与y坐标)
2.Touchslop
可以通过两次点击之间的距离的大小与Touchslop比较来判断是否是滑动,Touchslop就是最小滑动距离。
代码中通过下述方法获取。

ViewConfiguration.get(getContext()).getScaledTouchSlope()

1.4 VelocityTracker、GestureDetector和Scroller

1.VelocityTracker

用来追踪手指滑动的速度,包括水平速度与垂直速度。

调用方法:
在View的onTouchEvent中使用。

//初始化
VelocityTracker mVelocityTracker = VelocityTracker.obtain();

//在onTouchEvent方法中
mVelocityTracker.addMovement(event);

//获取速度
mVelocityTracker.computeCurrentVelocity(1000);

int xVelocity = mVelocityTracker.getXVelocity();
int yVelocity = mVelocityTracker.getYVelocity();
//重置和回收

mVelocityTracker.clear(); //一般在MotionEvent.ACTION_UP的时候调用

mVelocityTracker.recycle(); //一般在onDetachedFromWindow中调用

使用总结

(1)滑动速度可以为负,当我们向坐标轴反向滑动就为负

(2)当我们获取速度的时候,一定要先使用computeCurrentVelocity()方法,在使用getXVelocity()方法。代码中的1000是1000ms,假设在1000ms内水平向右滑动了100像素,那么水平速度就为100(像素/1s)。当传入的是100ms,那么水平速度就为10(像素/100ms)。因此公式可以总结如下:
速度 = (终点位置 - 起点位置) / 时间段,这个时间段是由我们传入的参数决定。

当我们不使用了需要将其回收。

//重置和回收

mVelocityTracker.clear(); //一般在MotionEvent.ACTION_UP的时候调用

mVelocityTracker.recycle(); //一般在onDetachedFromWindow中调用

2.GestureDetector

手势检测,用于辅助检测单击,滑动,长按,双击等手势。

需要首先创建一个GestureDetector的对象,并实现OnGestureListener的接口,为了需要还可以实现OnDoubleTapListener用于监听双击事件。

在日常开发中,比较常用的有:onSingleTapUp(单击)、onFling(快速滑动)、onScroll(拖动)、onLongPress(长按)、onDoubleTap(双击),建议:如果只是监听滑动相关的事件在onTouchEvent中实现;如果要监听双击这种行为的话,那么就使用GestureDetector。

3.Scroller

弹性滑动对象,(由于View自身的ScrollTo和scrollBy方法进行滑动,是一瞬间完成,用户体验较差)。因此使用Scroller用于实现View的弹性滑动。Scroller本身无法让View弹性滑动,它需要和View的computeScroll方法配合使用才能共同完成这个功能。

2.View的滑动

大概有三种方式:
1.通过View自带的ScrollTo与ScrollBy方法
2.通过动画给View施加平移效果
3.通过改变View的LayoutParams使得View重新布局从而实现滑动

1.通过View自带的ScrollTo与ScrollBy方法

1.使用scrollTo/scrollBy scrollTo和scrollBy方法只能改变view内容的位置而不能改变view在布局中的位置。
2. scrollBy是基于当前位置的相对滑动(如果传入的位置和当前位置相同就不动),而scrollTo是基于所传参数的绝对滑动(调用scrollBy并传入(当前位置+所传参数))。
在这里插入图片描述
3.mScrollx与mScrollY可以通过View的getScrollX和getScrollY方法可以得到,以mScrollX为例,它是由View的左边缘到View中内容的左边缘的距离决定的。边缘就是指四个顶点。当内容的左边缘在View左边缘的左边时,mScrollX大于0,反之小于0。

这里也可以看出由原始状态到第二个状态需要scrollTo(100,0),也就是说如果想要让View中的内容左移,就要将传入大于0的x,其他三个方向同理。
在这里插入图片描述
简单的尝试下上述方法。由于只能移动View中的内容,这里思维不要局限,单个View的内容就是里面的内容,比如TextView的内容,而ViewGroup的内容就是其中的View。我现在有一个ConstraintLayout,其中包含三个Button,那么我对Constraintlayout使用scrollTo,更改的就是三个Button的位置。

 ConstraintLayout constraintLayout = findViewById(R.id.layout);
        constraintLayout.scrollTo(-100,-100);

在这里插入图片描述
在这里插入图片描述
可以看到三个Button(ViewGroup中的内容)向右下移动。

2.通过动画给View施加平移效果

使用动画来移动View主要是更改View的translationX与translationY,以更改View左上角相对于父容器之间的偏移量。既可以使用传统的view动画,也可以使用属性动画。使用后者需要考虑兼容性问题,如果要兼容Android3.0以下版本系统的话推荐使用nineoldandroids。

使用动画还存在一个交互问题:在android3.0以前的系统上,view动画和属性动画,新位置均无法触发点击事件,同时,老位置仍然可以触发单击事件。从3.0开始,属性动画的单击事件触发位置为移动后的位置,view动画仍然在原位置。

3.通过改变View的LayoutParams使得View重新布局从而实现滑动

这种方法简单易懂,例如我们要将Button向右移100px,只需要将其marginleft+100px即可。我们还可以在Button左边放一个width为0的view,如果想要右移100pxButton,就将View的width设为100px。

ViewGroup.MarginLayoutParams mp = (ViewGroup.MarginLayoutParams) mybutton.getLayoutParams();
        mp.leftMargin+=100;
        mybutton.setLayoutParams(mp);

在这里插入图片描述在这里插入图片描述

4.三种方式的比较(重要)

1.scrollTo/scrollBy方式:适合于View内容的滑动,因为View无法滑动
2.动画方式:3.0版本以上采用这种方式没有明显缺陷,3.0以下版本,使用属性动画和View动画均不能改变View的属性,影响用户交互。(新位置无法触发点击事件,而老位置还可以触发点击事件)。
3.通过更改View的layoutparams使得View重新布局:操作复杂,适合于需要与用户交互

5.跟手滑动

这里是使用更改其layoutparams的参数来滑动,每一次回调OnTouchEvent时都记录触摸点的位置(这里记录的是getX和getY,相对于View本身的坐标)

当用户的动作是按下时,更新上一次的位置;当用户动作是滑动的时候,计算上次的位置与当前触摸点位置的差值,并将差值与View当前所在位置的值加和,更新View的位置。

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        //每次回调onTouchEvent的时候,我们获取触摸点的代码
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 记录触摸点坐标
                mLastX = x;
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                // 计算偏移量
                int offsetX = x - mLastX;
                int offsetY = y - mLastY;
                // 在当前left、top、right、bottom的基础上加上偏移量
                layout(getLeft() + offsetX,
                        getTop() + offsetY,
                        getRight() + offsetX,
                        getBottom() + offsetY);
                break;
        }
        return true;
    }

3.弹性滑动(重要)

1.使用Scroller

核心思想:使用Scroller可以将View中的内容缓慢的移动,而非一下子到达指定位置。不过仅仅使用Scroller无法完成功能,需要与View的computeScroll函数相结合,才能完成弹性滑动。它不断地让view重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔Scroller就可以得出view的当前的滑动位置,知道了滑动位置就可以通过scrollTo方法来完成view的滑动。就这样,view的每一次重绘都会导致view进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动,这就是Scroller的工作原理

主要代码如下。首先创建Scroller对象,然后调用smoothScrollTo函数,并传入滑动到的指定位置。


    Scroller scroller = new Scroller(getContext());

    private void smootthScrollTo(int destX,int destY){
        int scrollX = getScrollX();
        int deltaX = destX - scrollX;
        //1000ms内滑向destX,效果是慢慢滑动
        scroller.startScroll(scrollX,0,deltaX,0,1000);
        invalidate();
    }

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

startScroll方法做到的就是保存参数,其中比较重要的是mDurationReciprocal,代表的是单位延迟。其他比较好理解,start的都是初始位置,d的都是最终滑动的终点,duration代表滑动时长。

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

滑动功能在于invalidate方法。在该方法中会重新绘制View,会调用computeScroll函数。我们的computeScroll函数中会不断的将当前View中的内容向当前的ScrollX与ScrollY滑动,并继续调用postIncalidate方法来进行第二次重绘,第二次重绘重复第一重绘的过程,不断的递归,直到滑动截至。其实一个缓慢滑动就是多次滑动的结果。

接下来看computeScrollOffset方法。首先他有个boolean变量来判断是否返回false来终止递归。主要代码中通过判断当前距离开始时间走过的时间是否小于滑动总时间,小于就需要继续滑动,否则就不用继续滑动了。

如果处于小于的情况下,计算当前走过的时间对应总时间的百分比,进而计算出距离需要改变的百分比并与初始位置加和后分别赋给mCurrx与mCurry,并返回true。

    /**
     * Call this when you want to know the new location.  If it returns true,
     * the animation is not yet finished.
     */ 
    public boolean computeScrollOffset() {
        if (mFinished) {
            return false;
        }

        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;
           ...
        else {
            mCurrX = mFinalX;
            mCurrY = mFinalY;
            mFinished = true;
        }
        return true;
    }

2.使用动画

我们可以在动画的每一帧到来时获取动画完成的比例,然后再根据这个比例计算出当前View所要滑动的距离,并通过scrollTo来移动。同时可以通过onAnimationUpdate方法做更多的事情。

    final int startX = 0;
        final int startY = 100;
        final  int deltaX = 0;
        final ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                float fraction = animator.getAnimatedFraction();
                mybutton.scrollTo(startX + (int)(deltaX * fraction),0);
            }
        });

3.使用延时策略

它的核心思想是通过发送一系列延时消息从而达到一种渐近式的效果,可以使用Handler或者View的postDelayed方法,也可以使用线程的sleep方法。

以postDelayed为例,我们可以通过它来延时发送一个消息,然后在消息中来进行View的滑动,如果接连不断地发送这种延时消息,那么就可以实现弹性滑动的效果,Handler与其相似。

代码以Handler为例。

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);
                    mButton1.scrollTo(scrollX, 0);
                    mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
                }
                break;
            }

            default:
                break;
            }
        };
    };

Button的点击函数

 @Override
    public void onClick(View v) {
        if (v == mButton1) {
            mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
        }
    }

整体流程:

点击button后,会发送一个具有延迟的Message,handler在消息处理时首先count++,然后会根据当前count值,来判断应该滑动的距离,滑动之后,继续发送相同的具有延迟的Message以达到递归效果,直至count大于阈值,终止递归,直接跳出,完成整个滑动过程。

4.View的事件分发(重要)

4.1点击事件的传递规则

1.三个重要函数(dispatchTouchEvent、onInterceptTouchEvent与onTouchEvent)

首先,要明确我们分析的对象是MotionEvent(点击事件)。点击事件的事件分发,其实就是对MotionEvent事件的分发过程,即当一个MotionEvent发生后,系统要传递给一个具体的View,这个传递过程就是分发过程。其中包含三个很重要的函数。

1.public boolean dispatchTouchEvent(MotionEvent ev)
描述:用来进行事件的分发。如果事件能够传递给当前的View,那么此方法一定会被调用。
返回值:受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。返回true表示事件被消费,本次的事件终止。返回false表示View以及子View均没有消费事件,将调用父View的onTouchEvent方法

2.public boolean onInterceptTouchEvent(MotionEvent event)
描述:在上述方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用。(因为理论上只要View拦截了一个事件,系统就会把整个事件序列都分发给这个View
返回值:返回结果表示是否拦截当前事件。返回false表示不做拦截,事件将向下分发到子View的dispatchTouchEvent方法。

3.public boolean onTouchEvent(MotionEvent event)
描述:在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前的事件
返回值:返回true表示事件被消费,本次的事件终止。返回false表示事件没有被消费,将调用父View的onTouchEvent方法

三个方法的关系可以如下所示:

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

总流程:对于一个根ViewGroup,点击事件发生后,会先传递给它,它的dispatchTouchEvent就会被调用,如果它要拦截这个事件,onInterceptTouchEvent就会返回true,这个事件就会交给当前的ViewGroup处理,也就是去调用onTouchEvent方法;如果不拦截这个事件,onInterCepterTouchEvent会返回false,进而传递给ViewGroup的子View去处理,也就是调用子View的dispatchTouchEvent。持续上述过程,直到该点击事件被处理。

2.onTouchListener的优先级大于OnTouchEvent

当一个View要处理事件,同时设置了OnTouchListenr,那么OnTouchListener中的OnTouch方法就会回调,如果返回false则会去继续调用OnTouchEvent,否则不调用。在onTouchEvent方法中,如果当前view设置了OnClickListener,那么它的onClick方法会被调用,所以OnClickListener的优先级最低

3.点击事件的传递过程:Activity->window->View

如果一个View的onTouchEvent方法返回false,那么它的父容器的onTouchEvent方法将会被调用,依次类推,如果所有的元素都不处理这个事件,那么这个事件将会最终传递给Activity处理(调用Activity的onTouchEvent方法)

4.总结(重要)

1.一个事件序列是由一个Action_down,一个Action_up与多个Action_Move组成。

2.正常情况下,一个事件序列只能被一个View所拦截与消耗。(当View一旦决定拦截事件,整个事件序列都将交给它来处理,并且它的onInterceptTouchEvent不会再被调用。)

3.某个View开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回false),那么整个事件序列剩下的事件也不会给它处理,会给它的父View处理(调用父View的onTouchEvent)。如果它只不消耗ACTION_DOWN事件,但消耗剩余的事件,这个点击事件(ACTION_DOWN)就会消失,View可以持续收到剩下的事件,而这个点击事件最终会被Activity处理。

4.ViewGroup默认不拦截事件,它的onInterceptTouchEvent默认返回false,View没有onInterceptTouchEvent方法,只要事件传递给它,就会默认调用onTouchEvent。

5.View的onTouchEvent默认会返回true,除非不可点击(clickable和Longclickable都为false)。一般View的Longclickable为false,而clickable分不同View,比如Button默认为true,而TextView为false

6.View的enable(可用性)不影响onTouchEvent的返回值。即使View是不可用的(disable),只要有一个属性(clickable和longclickable)为true,onTouchEvent就返回true。

7.onclick会发生的前提实际当前的View是可点击的,并且他收到了down和up的事件。

8.事件传递的过程总是先发给父元素,再由父元素发给子View。

流程图(复习的时候可以画一画这个流程图)

来自一文读懂Android View事件分发机制

1.View的结构图
主要是树形结构图,同时涉及包括了点击事件的产生:Activity->Window->顶级View->分发到子View
在这里插入图片描述
2. View事件分发流程
主要是一个整体的分发过程,第一层是根ViewGroup,第二层时子ViewGroup,第三层是子View,其中包括区分了View与ViewGroup在事件分发中的不同(View没有onIntercepterTouchEvent方法,会直接调用View的onTouchEvent)
在这里插入图片描述

4.2源码分析

1.Activity对点击事件的分发过程

点击事件最先是传递到Activity,因此首先看Activity的dispatchTouchEvent方法

Class Activitypublic boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {//事件分发并返回结果
            return true;//事件被消费
        }
        return onTouchEvent(ev);//没有View可以处理,调用Activity onTouchEvent方法
    }

可以看到事件传递到Activity后,会传递至预期绑定的window,如果最终返回false,则说明没有一个View可以消费这个事件,就返回Acitivity的onTouchEvent,交由Activity处理。

2.Window对点击事件的分发过程

我们继续跟进getWindow.superDispatchTouchEvent()方法,会发现这是一个抽象方法。

Window类说明
/**
 * Abstract base class for a top-level window look and behavior policy.  An
 * instance of this class should be used as the top-level view added to the
 * window manager. It provides standard UI policies such as a background, title
 * area, default key processing, etc.
 *
 * <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */
Class Window:
//抽象方法,需要看PhoneWindow的实现
public abstract boolean superDispatchTouchEvent(MotionEvent event);

Window是一个抽象类,而唯一实现的类就是phonewindow类,继续跟进phonewindow类

class PhoneWindow
    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

可以看到我们将事件传递到DecorView,而这个DecorView是Window的顶级View,我们通过setContentView设置的View是它的子View。

此时事件已经传递至顶级View,也就是我们设置的View。

3.View对点击事件的分发

顶级View一般是ViewGroup,我们首先去看ViewGroup的dispatchTouchEvent方法。

ViewGroup会首先判断是否拦截该事件

mFirstTouchTarget变量是一个单向的链式节点,当ViewGroup自己不处理而选择给子View处理时,会将这个变量指向子View。

   class ViewGroup:
    public boolean dispatchTouchEvent(MotionEvent ev) {
     	// Handle an initial 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();
            }
        // Check for interception.
        final boolean intercepted;//是否拦截事件
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            //FLAG_DISALLOW_INTERCEPT是子View通过
            //requestDisallowInterceptTouchEvent方法进行设置的
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                //调用onInterceptTouchEvent方法判断是否需要拦截
                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;
        }
        ...
    }

(1)当事件为Action_down或者mFirstTouchTarget不为空时,会选择去判断是否拦截这个事件。
注意,子View可以通过requestDisallowInterceptTouchEvent方法去设置FLAG_DISALLOW_INTERCEPT,来阻止ViewGroup去拦截除了Action_down以外的事件。

//FLAG_DISALLOW_INTERCEPT是子View通过
            //requestDisallowInterceptTouchEvent方法进行设置的
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

而当动作为ACTION_DOWN的时候,则无论子View设置与否都会去判断是否拦截。如下述代码,当点击事件为ActionDown,会重置FLAG_DISALLOW_INTERCEPT与mFirstTouchTarget。因此,当点击事件为Action_down,无论子View是否设置FLAG_DISALLOW_INTERCEPT都无法阻止ViewGroup去拦截。

        // Handle an initial 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();
            }

(2)如果事件不为ACTION_DOWN 且事件为ViewGroup本身处理(即mFirstTouchTarget ==null)那么intercepted = true,很显然事件已经交给自己处理根本没必要再调用onInterceptTouchEvent去判断是否拦截

结论(重要)

(1)如果View选择自身处理这个事件,则事件序列中其他事件也不需要判断是否拦截。直接会默认为拦截。

(2)子View可以使用requestDisallowInterceptTouchEvent方法去设置 FLAG_DISALLOW_INTERCEPT来阻止ViewGroup拦截除了ACTION_DOWN以外的其他事件。

(3)Action_Down事件不受子View设置标志位与否的影响,都会去判断是否需要拦截。

ViewGroup选择不拦截这个事件并分发给子View

核心思路是遍历ViewGroup中的子元素,判断子元素是否能够接收到事件(子元素是否在播动画和点击时按的坐标是否落在子元素的区域内),接下来的重要函数是dispatchTransformedTouchEvent函数。

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

在dispatchTransformedTouchEvent方法中有一个判断,判断传入的child是否为空。最后该函数会返回一个boolean型变量,如果返回true,就会将mFirstTouchTarget(单向链式节点)赋值同时跳出for循环。

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

当ViewGroup的子View全部被遍历后,会执行以下判断,最后如果mFirstTouchTarget变量为空,就会让ViewGroup自己处理。具体处理方法是使用dispatchTransformedTouchEvent方法,其中第三个参数传入null

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

注意,由于传入的child为null,如前文代码可知,这时候在dispatchTransformedTouchEvent方法中就会执行super.dispatchTouchEvent(event),也就是让ViewGroup自己处理。

结论(重要)

1.ViewGroup会遍历所有子View去寻找能够处理点击事件的子View(可见,没有播放动画,点击事件坐标落在子View内部),最终会调用子View的dispatchTouchEvent方法处理事件。

2.如果ViewGroup中有一个View可以处理事件,就会将mFirstTouchTarget赋值,并停止ViewGroup中对View的寻找。

3.如果ViewGroup遍历了所有子View都无法处理事件,就会让ViewGroup自己处理事件。

4.ViewGroup对事件的分发,是通过对View(子View或本身)调用dispatchTouchEvent来处理。

4.View对事件的处理

注意:这里的View不包含ViewGroup。
View不包含子View,因此无法向下传递事件,只能自己处理事件。观察下述代码可以看到一个先后顺序,首先判断是否有OnTouchLinstener的onTouch函数,如果返回的是true,就不调用onTouchEvent函数了。

    public boolean dispatchTouchEvent(MotionEvent event) {
        boolean result = false;
   		...
        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;
    }

继续看onTouchEvent中对事件的处理,可以看到只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么他就会消耗这个事件,即onTouchEvent返回true,不管他是不是DISABLE状态。

public boolean onTouchEvent(MotionEvent event) {
		...
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
  						...
                        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();
                                }
                            }
                        }
                 black;
             }
        ....
    return true;
    }
    return false;
 }

如果事件是ACTION_UP,且当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);
        return result;
    }

View的LONG_CLICKABLE属性默认为false,而CLICKABLE属性是否为false和具体的View有关,确切的说是可点击的View其CLICKABLE为true,不可点击的为false。比如button是可点击的,textview是不可点击的,通过setonclik或者longclik都是可以改变状态的,这点我们看源码:

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

总结

1.View对事件的处理中具有优先级。如果设置了onTouchListener,就会首先调用onTouch,返回false后,会调用onTouchEvent。

2.在onTouchEvent中,不考虑View是否可用,只要Clickable和Longclickable有一个为true,就可以消耗事件,而Longclickable默认为false,clickable根据不同的View有不同(TextView为false,Button为true)

3.如果设置了onClickListener,onClick就会在onTouchEvent中的performClick()函数中被调用。

整体脑图

在这里插入图片描述

5.滑动冲突

5.1外部拦截法

点击事件都经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,该方法只需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可,伪代码如下:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        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;
        }
        mLastXIntercept = x;
        mLastYIntercept = x;
        return intercepted;
    }

在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事件就无法触发。

5.2内部拦截法

父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就由父容器进行处理,这种方法和Android中的事件分发机制不一样,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX =  x - mLastX;
                int deltaY =  x - mLastY;
                if("父容器的点击事件"){
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:

                break;
        }
        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
    }

上述代码就是内部拦截法的典型代码,当面对不同的滑动策略只需要修改里面的条件即可,其他不需要做改动,除了子元素需要处理之外,父元素默认也要拦截除ACTION_DOWN之外的其他事件,这样当子元素调用getParent().requestDisallowInterceptTouchEvent(true)方法时,父元素才能继续拦截所需要的事件

为什么父容器不能拦截ACTION_DOWN事件呢?那是因为ACTION_DOWN事件并不受FLAG_DISALLOW_DOWN这个标记位的控制,所以一旦父容器拦截,那么所有的事件都无法传递到子元素中,这样不拦截就无法起作用了,父元素要做的如下修改

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        if(action == MotionEvent.ACTION_DOWN){
            return false;
        }else {
            return true;
        }
    }

发布了55 篇原创文章 · 获赞 28 · 访问量 9235

猜你喜欢

转载自blog.csdn.net/weixin_41796401/article/details/104021206