ObservableScrollView之TouchInterceptionFrameLayout解析

之前打算学习material design一些相关的东西,因为之前工作里也一直没有用到,所以不是很了解,而现在的app越来越多用了material design,学一学还是很有必要。然后又碰巧在github上看到这个开源控件,决定研究一番。

一些风格类似sticky header,flexibleSpace,fillGap等等,如果里面的view是可滑动(如scrollview,listview,recyclerview等),那么需要处理滑动事件来实现我们所需要的内容。那么作者就做出了这个滑动处理的framelayout。

因为处理比较多的是滑动事件,如果对滑动事件不是很熟悉的话,建议看一看这篇文章 Android Touch事件传递机制(一) -- onInterceptTouchEvent & onTouchEvent

那么下面开始。

监听器

首先定义了一个监听,用来传递一些滑动事件,主要是当前控件需要拦截子控件的滑动事件的时候会调用,已经注释了挺详细,注释并非原版翻译过来,添加了自己的理解。

public interface TouchInterceptionListener {
        /**
         * 确定布局是否应该拦截这个事件。
         * @param ev     Motion event.
         * @param moving true如果当这个事件是移动的事件
         * @param diffX  如果moving = true,那么之前的X和当前X的差值。
         * @param diffY  如果moving = true,那么是之前的Y和当前的Y的差值
         * @return True if the layout should intercept.
         */
        //确定是否是要拦截事件,如果需要拦截,那么走onTouchEvent
        boolean shouldInterceptTouchEvent(MotionEvent ev, boolean moving, float diffX, float diffY);

        /**
         * 当拦截的时候发出的一个down事件,第一次会调用到这里,
         * 另外当从不需要拦截到需要拦截的move滑动事件也会调用到这个方法
         * @param ev Motion event.
         */
        void onDownMotionEvent(MotionEvent ev);

        /**
         * 如果拦截的时候走onTouchEvent,并会转发给已注册的监听
         * @param ev    Motion event.
         * @param diffX Difference between previous X and current X.
         * @param diffY Difference between previous Y and current Y.
         */
        // 滑动过程中需要拦截或者从不需要突然变成需要拦截
        void onMoveMotionEvent(MotionEvent ev, float diffX, float diffY);

        /**
         * 当拦截的时候onTouchEvent转发的up或者cancel事件
         * @param ev Motion event.
         */
        void onUpOrCancelMotionEvent(MotionEvent ev);
    }

字段属性

注释已经听明白了,都是个人理解,可能会有出入

    //是否拦截
    private boolean mIntercepting;
    // 是否需要创建一个down滑动事件,当拦截状态要变成不需要的时候,这个标识变成false,创建一个down事件并传递给子view
    private boolean mDownMotionEventPended;
    // 是否从down事件开始,滑动从不需要拦截到需要拦截的时候,这个标识变成true,调用监听器的onDownMotionEvent方法
    private boolean mBeganFromDownMotionEvent;
    private boolean mChildrenEventsCanceled;
    private PointF mInitialPoint;
    //记录一个down事件,后面用于复制,但是会改变 x,y,效果应该和当时获取的一个事件将action 设置为down是一样的。
    private MotionEvent mPendingDownMotionEvent;
    private TouchInterceptionListener mTouchInterceptionListener;

重点一:onInterceptTouchEvent

如果拦截了那么会直接调用onTouchEvent,如果返回true,onTouchEvent也返回true,那么下一次不会在走这里,直接走onTouchEvent
如果返回false,子view消耗了事件,那么下一次会还会走这里,子view不消耗,那么以后的事件不会再给这个控件
@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (mTouchInterceptionListener == null) {
            return false;
        }

        //  在这里,我们必须初始化触摸状态变量
        // 和确认我们是否应该拦截这个事件
        // 我们是否应该拦截,保存在以后的事件处理中。
        switch (ev.getActionMasked()) { //getActionMasked() 用来有多点触控?
            case MotionEvent.ACTION_DOWN:
                mInitialPoint = new PointF(ev.getX(), ev.getY());
                // 复制一个action down的事件
                mPendingDownMotionEvent = MotionEvent.obtainNoHistory(ev);
                // 赋值将要action down = true
                mDownMotionEventPended = true;
                //因为这里还没有开始滑动,所以传参为 false ,0 , 0
                mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, false, 0, 0);
                // move的时候是不是从down开始的,如果不是的话,需要给子view发送一个down事件
                mBeganFromDownMotionEvent = mIntercepting;
                mChildrenEventsCanceled = false;
                return mIntercepting;//这里拦截的话直接走onTouchEvent
            case MotionEvent.ACTION_MOVE:
                //(不拦截并且子控件消费了才会走这里)
                // ACTION_MOVE will be passed suddenly, so initialize to avoid exception.
                //acion_move在down后會馬上調用,因此初始化避免異常
                //子view消费了事件才会走这里
                if (mInitialPoint == null) {
                    mInitialPoint = new PointF(ev.getX(), ev.getY());
                }

                // diffX and diffY are the origin of the motion, and should be difference
                // from the position of the ACTION_DOWN event occurred.
                float diffX = ev.getX() - mInitialPoint.x;
                float diffY = ev.getY() - mInitialPoint.y;
                mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, true, diffX, diffY);
                Constant.Log(TAG+"onIntercept  "+mIntercepting);
                return mIntercepting;
        }
        return false;
    }

这里需要注意的是如果这里返回false,子viewonTouchEvent返回了false,那么之后的事件会继续走过这里
如果这里返回true,自身的onTouchEvent也返回true,那么以后的事件不会在经过这个方法。

重重重点二:onTouchEvent

因为主要功能的实现都在这个方法里面,所以这个方法是最终要的。
我们需要知道的是,如果一开始拦截了,那么就会走这个方法,如果滑动的中途突然不想拦截了,因为后续的事件会一直经过这个方法(此时onIntercept方法已经不走了),所以需要手动将事件传递给子view。

@Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (mTouchInterceptionListener != null) {
            switch (ev.getActionMasked()) {
                case MotionEvent.ACTION_DOWN:
                    if (mIntercepting) {
                        //如果拦截了,那么通知监听down事件
                        mTouchInterceptionListener.onDownMotionEvent(ev);
                        //这里拦截了为什么还要重新传递事件??注释了一下好像也没有问题
                        duplicateTouchEventForChildren(ev);
                        return true;
                    }
                    break;
                case MotionEvent.ACTION_MOVE:
                    // ACTION_MOVE will be passed suddenly, so initialize to avoid exception.
                    if (mInitialPoint == null) {
                        mInitialPoint = new PointF(ev.getX(), ev.getY());
                    }

                    // diffX and diffY are the origin of the motion, and should be difference
                    // from the position of the ACTION_DOWN event occurred.
                    float diffX = ev.getX() - mInitialPoint.x;
                    float diffY = ev.getY() - mInitialPoint.y;
                    mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, true, diffX, diffY);
                    if (mIntercepting) {
                        // 我们应该根据现有的位置创建一个 aciont_down 事件
                        // 其实就是滑动中从不需要拦截到需要拦截的时候变换的时刻的通知
                        if (!mBeganFromDownMotionEvent) {
                            mBeganFromDownMotionEvent = true;

                            MotionEvent event = MotionEvent.obtainNoHistory(mPendingDownMotionEvent);
                            event.setLocation(ev.getX(), ev.getY());
                            //如果没有的话需要创建一个down事件传递给监听
                            mTouchInterceptionListener.onDownMotionEvent(event);

                            mInitialPoint = new PointF(ev.getX(), ev.getY());
                            diffX = diffY = 0;
                        }

                        // 因为拦截了,所以不再发事件给子控件(可能之前子view还在滚动,然后需要拦截,但这里不是已经收不到了吗)
                        if (!mChildrenEventsCanceled) {
                            mChildrenEventsCanceled = true;
                            duplicateTouchEventForChildren(obtainMotionEvent(ev, MotionEvent.ACTION_CANCEL));
                        }

                        mTouchInterceptionListener.onMoveMotionEvent(ev, diffX, diffY);

                        // 如果下一个滑动事件不需要拦截了
                        // 那么我们应该创建一个假的 action_down 事件
                        // 因此我们设置一个pending标记 = true ,就是变成不需要拦截了,需要传递一个down事件给子view,不然会有bug
                        mDownMotionEventPended = true;

                        // 因为我们认定了这是已经消费了,所以返回true
                        return true;
                    } else {
                        // 如果到一半不需要拦截了,那么应该要将滑动事件转发给子view
                        if (mDownMotionEventPended) {
                            mDownMotionEventPended = false;
                            // 因为这是从move事件中突然不需要拦截的,因为从拦截到不拦截的过程,是不会在走OnIntercept的,所以还要重新传一次
                            //如果部分发down事件给字控件,那么子控件是不会有移动效果的,要有头有尾
                            MotionEvent event = MotionEvent.obtainNoHistory(mPendingDownMotionEvent);
                            event.setLocation(ev.getX(), ev.getY());
                            duplicateTouchEventForChildren(ev,event);
                            //下面这两句话是自己加的,效果试了下是一样的
                            //ev.setAction(MotionEvent.ACTION_DOWN);
                            //duplicateTouchEventForChildren(ev);
                        } else {
                            //如果已经发送了down事件,那么就直接分发给子view
                            duplicateTouchEventForChildren(ev);
                        }


                        // 如果下一个事件变成需要拦截了
                        // 那么我们需要创建一个假的action_down事件
                        // 因此设置这个标记为false
                        // 好像我们没有收到一个down的动作事件
                        // 这是滑到一半的时候不需要拦截了,所以不是从down开始的
                        mBeganFromDownMotionEvent = false;

                        // 子控件触摸事件取消
                        mChildrenEventsCanceled = false;
                    }
                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    mBeganFromDownMotionEvent = false;
                    if (mIntercepting) {
                        //滑动来滑动去,如果最后需要拦截了事件的话,那么直接通知。
                        mTouchInterceptionListener.onUpOrCancelMotionEvent(ev);
                    }


                    // 无论这个布局是否拦截了连续的滑动事件,都应该取消子view的滑动事件。
                    if (!mChildrenEventsCanceled) {
                        //需要给子view传递up或cancel事件,因为子view会在这up或cancel设置一些变量或逻辑
                        mChildrenEventsCanceled = true;
                        if (mDownMotionEventPended) {
                            mDownMotionEventPended = false;
                            MotionEvent event = MotionEvent.obtainNoHistory(mPendingDownMotionEvent);
                            event.setLocation(ev.getX(), ev.getY());
                            //分发down事件,有头有尾
                            duplicateTouchEventForChildren(ev, event);
                        } else {
                            duplicateTouchEventForChildren(ev);
                        }
                    }
                    return true;
            }
        }
        return super.onTouchEvent(ev);
    }

这里我有一个疑问,在down里已经拦截了,为什么还要向子view传递down事件呢,而且马上又会传递一个cancel事件。希望有明白的同学可以给我指导一下~~

你可能感兴趣的:(ObservableScrollView之TouchInterceptionFrameLayout解析)