触摸事件分发机制(有用的小技巧:事件二次分发)

存在的问题

在上文的下拉刷新控件中,有两个问题

  1. 在下拉到ScrollView顶部时候,继续往下拉时,并不会直接把头布局拉下来,而是需要把手松开后,再次下拉才会拉下头布局,为什么?

上文说过,onInterceptTouchEvent方法虽然不是每次都被调用,但是如果子view在处理事件的时候,onInterceptTouchEvent是一直会调用的,因为他要等待子View不想消费事件的时机出现时,交给自己处理触摸事件,按理说这个问题里,子View不想处理事件的时机已经到了,为什么父View没马上接受呢?

  1. 当刷新头已经出现了,手指上滑 ,当把刷新头完全隐藏了,继续上滑,此时由于外层布局拦截了事件,会导致把整个外层布局往上滑,而我们想要的是此时让内部的scrollview响应事件,滑动的是scrollview。

我们知道事件分发机制,一旦父view拦截了事件后,就不会把他交给子View了,所以从此之后,scrollview是不会再次有处理事件的机会的,那有什么办法,改良一下呢?

其实这些问题都指向了一个方法onInterceptTouchEvent,他的调用时机到底是什么时候。我们只看伪代码,因为伪代码是思路,是看完源码后的精华,其实这个问题在 之前博客 说过了,但是他太重要了,所以还要总结一次。

事件分发精华伪代码

先看事件分发的入口 dispatchTouchEvent伪代码:
  public boolean dispatchTouchEvent(MotionEvent ev) {
        Boolean consume = false;
        if (【当前ViewGroup要不要拦截事件(ev)】) {
        //如果本ViewGroup拦截事件,那么调用本ViewGroup的onTouchEvent
            consume = onTouchEvent(ev);
        } else {
        //否则,调用子View的dispatchTouchEvent,
        //如果子View还是一个ViewGroup的话,dispatchTouchEvent逻辑是一样的,会迭代此逻辑
        //如果子View是一个View,那他的dispatchTouchEvent是不同的(稍后给出)
            consume = child.dispatchTouchEvent(ev);
        return consume;//返回true,表示事件被消耗了,
        // 如果是最里层级的view或ViewGroup的dispatchTouchEvent返回true,表示由本ViewGroup来处理以后的事件

    }

注意【当前ViewGroup要不要拦截事件(ev)】是一个方法,方法名我写成了中文名字,这个方法内部的逻辑,才是重点。这个方法可不等同于直接调用onInterceptTouchEvent哟,而是围绕onInterceptTouchEvent展开的一系列逻辑

【当前ViewGroup要不要拦截事件(ev)】伪代码★相当重要:
public boolean 【当前ViewGroup要不要拦截事件(ev)】{
	 boolean intercepted;
	   if(DOWN事件 |或| 他的子View正在消费触摸事件) {

	        if (子View请求父容器拦截) {
		      //子View请求父容器拦截就是指,子view通过parent.requestDisallowInterceptTouchEvent(false)
		      //★★★注意只有此种情况父容器的onInterceptTouchEvent才会被调用
	  		     intercepted = onInterceptTouchEvent(ev);
	        } else {
	            //如果子View请求不拦截,那么直接返回false,根本不需要走父容器的onInterceptTouchEvent方法
	            intercepted = false; 
	                }
	    } else {
	        //当这个事件不是ACTION_DOWN,并且当前的ViewGroup也没有子ViewGroup(view)可以处理事件,那么就由本ViewGroup直接拦截这个事件,也不需要走父容器的onInterceptTouchEvent方法
	        intercepted = true;
	    }
	    return intercepted;
	}
简单说下requestDisallowInterceptTouchEvent(boolean)

注意是在子view里通过,getParent().requestDisallowInterceptTouchEvent(boolean)来使用

- requestDisallowInterceptTouchEvent(false):
子view请求拦截,让父view去询问下自己的onInterceptTouchEvent方法,看看要不要拦截

- requestDisallowInterceptTouchEvent(true):
子view请求不要拦截,那就直接返回false,不去拦截
   
总结:

关键问题在于:【当前ViewGroup要不要拦截事件(ev)】的逻辑,虽然每次触摸事件都会通过dispatchTouchEvent来调用到【当前ViewGroup要不要拦截事件(ev)】这个方法,但是这并不意味着onInterceptTouchEvent每次都会被调用
这个关键逻辑,我们能得出以下重要结论
onInterceptTouchEvent被调用的前提条件是:

  1. Down事件&&子View请求父View不要拦截
  2. 他的子View正在消费触摸事件&&子View请求父View不要拦截

现在看看最上面抛出的问题:

  1. 在下拉到ScrollView顶部时候,继续往下拉时,并不会直接把头布局拉下来,而是需要把手松开后,再次下拉才会拉下头布局,为什么?

上文说过,onInterceptTouchEvent方法虽然不是每次都被调用,但是如果子view在处理事件的时候,onInterceptTouchEvent是一直会调用的,因为他要等待子View不想消费事件的时机出现时,交给自己处理触摸事件,按理说这个问题里,子View不想处理事件的时机已经到了,为什么父View没马上接受呢?

为什么父View没马上接受呢?因为在本例子里,内部的ScrollView在某一个时机,调用了 requestDisallowInterceptTouchEvent(false)方法,即请求父view不要拦截,那么根本不去走onInterceptTouchEvent方法,直接intercepted=false了,所以此时下拉不会落下刷新头,而是在第二次下拉时,才会访问自己的onInterceptTouchEvent,发现满足条件,就把刷新头拉下来了(此时ScrollView还没来得及调用requestDisallowInterceptTouchEvent(false)方法)

所以解决方法很简单
在父容器里重写requestDisallowInterceptTouchEvent,让

    @Override
    public void requestDisallowInterceptTouchEvent(boolean b) {
        // 去掉默认行为,使得每个事件都会经过走一下这个布局
    }

  1. 当刷新头已经出现了,手指上滑 ,当把刷新头完全隐藏了,继续上滑,此时由于外层布局拦截了事件,会导致把整个外层布局往上滑,而我们想要的是此时让内部的scrollview响应事件,滑动的是scrollview。

我们知道事件分发机制,一旦父view拦截了事件后,就不会把他交给子View了,所以从此之后,scrollview是不会再次有处理事件的机会的,那有什么办法,改良一下呢?

解决方法,就是在合适的时机,手动代码造一个Down事件,并且分发此事件,因为,这样就突破了在一系列触摸事件中,父容器拦截事件后,子View就没机会再次处理事件的问题,因为我们手动造出了第二次触摸事件,一切从Down开始

    if (mLp.topMargin <= -mHeaderHeight && deltaY <0) {
        // 重新dispatch一次down事件,使得ScrollView可以继续滚动
        int oldAction = event.getAction();
        event.setAction(MotionEvent.ACTION_DOWN);
        dispatchTouchEvent(event);
        event.setAction(oldAction);
    }

在这里插入图片描述

改良后的源码

package com.view.custom.dosometest.view;

import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.ScrollView;

/**
 * 描述当前版本功能
 *
 * @Project: DoSomeTest
 * @author: cjx
 * @date: 2019-12-01 10:06  星期日
 */
public class RefreshView extends LinearLayout {


    private ScrollView mScrollView;
    private View mHeader;
    private int mHeaderHeight;
    private MarginLayoutParams mLp;

    public RefreshView(Context context) {
        super(context);
        init(context);
    }

    public RefreshView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public RefreshView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }


    private void init(Context context) {
        setBackgroundColor(Color.GRAY);

        post(new Runnable() {
            @Override
            public void run() {
                initView();// 因为涉及到获取控件宽高的问题,所以写到post里
            }
        });

    }

    private void initView() {

        if (getChildCount() > 2) {

            // 给刷新头设置负高度的margin,让他隐藏
            mHeader = getChildAt(0);
            mHeaderHeight = mHeader.getMeasuredHeight();
            mLp = (MarginLayoutParams) mHeader.getLayoutParams();
            mLp.topMargin = -mHeaderHeight;
            mHeader.setLayoutParams(mLp);

            // 得到第二个view,scrollView
            View child1 = getChildAt(1);
            if (child1 instanceof ScrollView) {
                mScrollView = (ScrollView) child1;
            }

        }
    }


    float mLastY;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        boolean intercept = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercept = false;
                break;

            case MotionEvent.ACTION_MOVE:
                int deltaY = (int) (y - mLastY);
                if (needIntercept(deltaY)) {//外部拦截的模板代码,只要重写needIntercept方法逻辑就行
                    //注意当前ViewGroup一旦拦截,一次事件序列中就再也不会调用onInterceptTouchEvent了,
                    // 所以子View再也不会得到事件处理的机会了
                    // 为了解决这个问题,就引出了《嵌套滑动》这个新的事物,见下文
                    intercept = true;
                } else {
                    intercept = false;
                }

                break;

            case MotionEvent.ACTION_UP:

                intercept = false;

                break;
            default:
                break;
        }

        mLastY = y;
        return intercept;
    }

    private boolean needIntercept(int deltaInteceptY) {
        // mScrollView已经下拉到最顶部&&你还在下来,那么父容器拦截
        if (!mScrollView.canScrollVertically(-1) && deltaInteceptY > 0) {
            Log.e("ccc", "不能再往下拉了&&你还在往下拉,父布局拦截,开始拉出刷新头");
            return true;
        }
        if (mLp.topMargin>-mHeaderHeight) {
            Log.e("ccc", "只要顶部刷新头,显示着,就让父布局拦截");
            return true;
        }

        return false;
    }

    @Override
    public void requestDisallowInterceptTouchEvent(boolean b) {
        // 去掉默认行为,使得每个事件都会经过这个Layout
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        float y = event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:

                break;

            case MotionEvent.ACTION_MOVE:


                float deltaY = y - mLastY;

                // 防止刷新头被无限制下拉,限定个高度

                if (mLp.topMargin + deltaY > mHeaderHeight) {
                    deltaY = mHeaderHeight - mLp.topMargin;
                }
                // 动态改变刷新头的topMargin
                mLp.topMargin += (int) deltaY;
                Log.e("ccc", "y:" + y + "mLastY:" + mLastY + "deltaY:" + deltaY + "mLp.topMargin:" + mLp.topMargin);
                mHeader.setLayoutParams(mLp);

                if (mLp.topMargin <= -mHeaderHeight && deltaY <0) {
                    // 重新dispatch一次down事件,使得列表可以继续滚动
                    int oldAction = event.getAction();
                    event.setAction(MotionEvent.ACTION_DOWN);
                    dispatchTouchEvent(event);
                    event.setAction(oldAction);
                }
                break;


            case MotionEvent.ACTION_UP:
                //松手后,看位置,如果过半,刷新头全部显示,没过半,刷新头全部隐藏
                if (mLp.topMargin > -mHeaderHeight / 2) {
                    smoothChangeTopMargin(mLp.topMargin, 0);
                } else {
                    smoothChangeTopMargin(mLp.topMargin, -mHeaderHeight);
                }

                break;
        }

        mLastY = y;

        return true;
    }

    /**
     * 使用属性动画平滑地过度topMargin
     *
     * @param start
     * @param end
     */
    private void smoothChangeTopMargin(int start, int end) {
        ValueAnimator valueAnimator = ValueAnimator.ofInt(start, end);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mLp.topMargin = (int) animation.getAnimatedValue();
                mHeader.setLayoutParams(mLp);

            }
        });
        valueAnimator.setDuration(300);
        valueAnimator.start();

    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android 事件分发机制是指在用户与Android设备进行交互时,Android系统如何接收并分发这些事件的过程。事件分发机制包括三个阶段:分发、拦截和处理。 1. 分发阶段:事件从Android设备的底层硬件驱动程序开始,通过InputEvent分发给View层。在View层中,事件分为两类:MotionEvent和KeyEvent。MotionEvent表示触摸事件,包括按下、移动、抬起等操作;KeyEvent表示按键事件,包括按下和抬起。 2. 拦截阶段:在事件分发到View层后,会从最上层的View开始进行事件分发,直到有View对事件进行拦截。如果有View对事件进行了拦截,则事件不会继续向下分发,而是由该View进行处理。View是否拦截事件的判断由onInterceptTouchEvent方法完成,如果该方法返回true则表示拦截事件。 3. 处理阶段:如果事件没有被拦截,则会被传递到最底层的View进行处理。在View中,事件处理由onTouchEvent方法完成。如果该方法返回true,则表示事件已经被处理,不再需要继续向下分发;如果返回false,则会继续向上分发直到有View对事件进行拦截。 Android事件分发机制的流程如下: ![image.png](attachment:image.png) 需要注意的是,事件分发机制是一个逆向分发的过程,即从底层向上分发,而不是从顶层向下分发。这是因为底层的View需要先处理事件,如果底层的View不拦截事件事件才能向上分发

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值