Android 事件分发实例之综合篇(三)

前言

Android 事件分发实例之右滑结束Activity(一)
Android 事件分发实例之右滑结束Activity(二)

前两篇主要是介绍通过处理滑动事件实现右滑结束Activity,功能简单,并且存在诸多不足之处。在思考之后,遂想实现在不同方向上的滑动,并且加入背景渐变、背景缩放、组合方向上滑动、支持多触点、沉浸式系统状态栏等。通过自定义属性,设置不同的方向组合、背景属性等实现不同方向组合的滑动,通过组合总有15种类型的组合滑动方式,可通过在XML中设置属性或者动态设置属性的方式,组合不同的属性,实现不同的效果。遂起名为SuperSlideLayout,其内部依然是通过Scroller实现内容滑动,通过判断滑动条件以及滑动状态,重写拦截和消费事件以及解决事件冲突问题,处理滑动事件,实现滑动效果。所有特效都是使用APP过程发现的,SuperSlideLayout还可实现的更多的效果,下列的特效均为市面APP常见效果,之后还会陆续实现更多的特效。
事件分发实例之SuperSlideLayout

实现效果

侧滑结束Activity

可自由组合实现多种侧滑结束Activity的效果,适配系统多种ViewGroup

各种系统ViewGroup.png

上下滑动图集

此效果模仿今日头条的图集浏览功能,刚工作那会就感觉今日头条的图集浏览功能真赞。右滑过程中,window背景渐变为透明,当左右上滑过程,window背景设置为透明,Activity中除了Viewpager部分内容(不是背景)渐变透明。效果如下:


右滑过程.jpg

上下滑动.jpg

底部弹出框

很多应用都会有底部可拖拽弹出框,基本都是采用BottomSheetDialog来实现的,效果极佳,但是也有不足之处,就是默认情况下如果子视图过多可滚到顶部,有时候可能并不需要滚到顶部,但是系统并没有直接设置的方法,不过已经总结解决方案,具体解决请参考上一篇。Android 修改BottomSheetDialog不滚动到顶部。因此本文也会介绍使用封装的SuperSlideLayout实现如同BottomSheetDialog的效果。另外还可以通过实现设置滑动边缘,实现今日头条评论列表弹出框效果,具体请参考demo,如需要实现特效或者其他方向弹出等,需自己修改个别属性。

底部弹出评论框.png

全屏评论框

模仿今日头条全屏评论框,看名称肯定会觉得实现是使用上一步中的底部弹出框,其实不然,两者没有任何关系,除了都使用SuperSlideLayout之外。
主要是通过设置两个SuperSlideLayout,设置不同的方向上滑动的属性,并且外层的添加系统栏颜色,加以区分,里层添加列表数据,效果上如同弹出框。里层下拉过程中,需要外层透明并且系统栏渐变透明,具体参考demo。


右滑过程.png
下拉拖拽过程.png

可拖拽共享图集

Android5.0之后出了过渡动画,效果也是非常好,特别是共享元素,使Activity的跳转更平滑。在图集的基础上添加可拖拽结束,并且加入共享元素功能。效果如下:


网格图集列表.jpg

左右滑动过程.jpg
拖拽过程.jpg

可拖拽视频窗口

此效果模仿皮皮虾APP的视频详情页效果,下方评论列表可滑动,滑动顶部,向下拖拽可使背景渐变透明,视频窗口大小不改变(可通过参数改变大小),视频窗口随着手势改变,并且已经实现共享元素,超过阀值,视频窗口会自动回归到主页列表。


上滑列表状态.jpg
下拉拖拽状态.jpg

属性介绍

自定义属性

boolean mSlideEnable:是否支持滚动 
int mSlideEdge:从哪个边缘可滑动(是支持全屏,准确点是方向)
float mSlideThresholdRate:阈值比率
boolean mCheckThreshold:是否需要判断阈值
boolean mAlphaEnable:是否支持背景透明度变化
float mAlphaRate:透明度变化比率
float mMinAlpha:最小背景透明度
boolean mScaleEnable:是否支持缩放
float mScaleRate:缩放比率
float mMinScale:最小缩放比例
boolean mOverflowParent:滑动是否可越过父类边界
boolean mSingleDirection:滑动是否是单一方向
boolean mMultiPointerEnable:是否支持多点触摸
int mScrollTime:总滑动时间

默认属性

  • 支持四个方向组合方式的滑动,组合方式总有15种情况,通过 “ | ” 组合。
public static final int EDGE_LEFT = 1 << 0;
public static final int EDGE_RIGHT = 1 << 1;
public static final int EDGE_TOP = 1 << 2;
public static final int EDGE_BOTTOM = 1 << 3;
public static final int EDGE_ALL = EDGE_LEFT | EDGE_TOP | EDGE_RIGHT | EDGE_BOTTOM;
  • 滑动方向分为水平、垂直、以及水平加垂直,三种方式
public static final int DIRECTION_HORIZONTAL = 1 << 0;
public static final int DIRECTION_VERTICAL = 1 << 1;
public static final int DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL;
  • 根据滑动过程变化分为三种滑动状态,闲置、拖拽、释放
public static final int STATE_IDLE = 1 << 0;
public static final int STATE_DRAG = 1 << 1;
public static final int STATE_RELEASE = 1 << 2;
public int mCurrentState = STATE_IDLE;

成员变量介绍

Scroller mScroller:实现滑动
Activity mActivity:需要关闭的activity
boolean mOverThreshold:是否超越阀值
boolean mIsBeingDragged:是否拦截或者可拖拽
int mDirection:方向(指滑动水平、垂直或者组合方式)
float mDownX, mDownY:触点位置(不一定是按下位置)
boolean mPositiveX, mPositiveY:X轴、Y轴正方向向量
int mMeasuredWidth, mMeasuredHeight:测量的宽高
View mChildRootView:直属子视图(注:只能有一个直属子视图,同ScrollView)
Drawable mBackground:背景(滑动直属子视图后面的背景)
Drawable mForeground:前景(直属子视图的背景)
OnSlideListener mOnSlideListener:滑动监听器

//下面是触点
final int INVALID_POINTER = -1;
int mActivePointerId = INVALID_POINTER;
boolean mCheckTouchInChild;//触点是否在子类中

//系统状态栏
WindowInsetsCompat mLastInsets
boolean mDrawStatusBarBackground
Drawable mStatusBarBackground

处理滑动事件

本文主要采用Scroller实现其内部子视图滑动,滑动的核心内容还是重写ViewGroup的onInterceptTouchEvent和onTouchEvent方法。

拦截事件

分析拦截条件:

  • 由于采用Scroller滑动,因此必须需要其内部包含至少一个子类
  • 触点必须落在其子视图中才能拦截
  • 触点所在的子视图无法实现自身的滑动
  • 必须在指定的mSlideEdge滑动
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        final int action = event.getActionMasked();
        int pointerIndex;

        //第一步:监测是否含子类
        boolean checkNullChild = checkNullChild();
        if (!mSlideEnable || checkNullChild) {
            return super.onInterceptTouchEvent(event);
        }

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                //判断触点是否在子view中
                mDownX = event.getX();
                mDownY = event.getY();
                mActivePointerId = event.getPointerId(0);
                mCheckTouchInChild = checkTouchInChild(mChildRootView, mDownX, mDownY);

                //判断是否触点是否在子类外部
                if (!mCheckTouchInChild) {
                    if (mOnSlideListener != null) {
                        mOnSlideListener.onTouchOutside(this, mCheckTouchInChild);
                    }
                    return super.onInterceptTouchEvent(event);
                }

                mScroller.computeScrollOffset();
                if (mCurrentState != STATE_IDLE
                        && Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
                    mScroller.abortAnimation();
                    mIsBeingDragged = true;
                    disallowInterceptTouchEvent();
                } else {
                    mIsBeingDragged = false;
                }

                break;
            case MotionEvent.ACTION_MOVE:
                //计算移动距离 判定是否滑动
                pointerIndex = event.findPointerIndex(mActivePointerId);
                if (pointerIndex == INVALID_POINTER) {
                    break;
                }

                float dx = event.getX(pointerIndex) - mDownX;
                float dy = event.getY(pointerIndex) - mDownY;
                mIsBeingDragged = chechkCanDrag(dx, dy);
                if (mIsBeingDragged) {
                    performDrag(event, dx, dy, pointerIndex);
                }

                break;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
               //如果在可拖拽情况下复位
                if (mIsBeingDragged) {
                    revertOriginalState(getScrollY(), getScrollY(), false);
                }
                mIsBeingDragged = false;
                mActivePointerId = INVALID_POINTER;

            case MotionEvent.ACTION_POINTER_UP:
                onSecondaryPointerUp(event);
                break;
        }
        return mIsBeingDragged;
    }

说明:使用event.getActionMasked()判断事件的类型,主要是为了多触点,这个知识点可自行查知,本文不做特殊介绍。

解决分析第一点:
//第一步:监测是否含子类
        boolean checkNullChild = checkNullChild();
        if (!mSlideEnable || checkNullChild) {
            return super.onInterceptTouchEvent(event);
        }
/**
     * 监测是否有子类
     * 无子视图禁止拖动
     *
     * @return
     */
    private boolean checkNullChild() {
        mChildRootView = getChildAt(0);
        return getChildCount() == 0;
    }

解决分析第二点:
/**
     * 检测触点是否在当前view中
     */
    private boolean checkTouchInChild(View childView, float x, float y) {
        if (childView != null) {
            int scrollX = getScrollX();
            int scrollY = childView.getScrollY();
            //需要加上已经滑动的距离
            float left = childView.getLeft() - scrollX;
            float right = childView.getRight() - scrollX;
            float top = childView.getTop() - scrollY;
            float bottom = childView.getBottom() - scrollY;
            if (y >= top && y <= bottom && x >= left
                    && x <= right) {
                return true;
            }
        }
        return false;
    }

说明:
1、使用getScrollX()而不是childView.getScrollX()的原因,需要了解Scroller,其实现内部内容滑动,因此应该获取SuperSlideLayout的值,而不是其子视图的getScrollX()值
2、计算触点为什么要加上getScrollX()的值,目的是滑动过程中多触点判断,滑动过程中,子视图的坐标点位置不会改变,子视图内容改变,为了做到视觉上触点的位置是否在子视图中,因此需要加上getScrollX()的值。举个例子:如果从左向右滑动过程中,滑动了100px,getScrollX()的值是负数(-100px),如果直属子视图宽度是等于父类的话,那么getLeft()的值是不会随着滑动改变的,因此一直是0,此时直属子视图的左边距应当判定为100px,其他的计算同理。

解决分析后两点:

最后两点包括,监测边缘和方向上是否可滑动,还有一点就是外加的,关于方向上优先级处理

/**
     * 检测是否可以拖拽
     *
     * @param dx
     * @param dy
     * @return
     */
    private boolean chechkCanDrag(float dx, float dy) {
        boolean mMinTouchSlop = checkEdgeAndTouchSlop(dx, dy);
        boolean chcekScrollPriority = chcekScrollPriority(dx, dy);
        boolean checkCanScrolly = checkCanScrolly(dx, dy);
        return mMinTouchSlop && chcekScrollPriority && !checkCanScrolly;
    }

下面这个方法主要是根据设置的滑动边缘判断方向

/**
     * 边缘滚动
     *
     * @param dx
     * @param dy
     * @return
     */
    private boolean checkEdgeAndTouchSlop(float dx, float dy) {
        boolean mMinTouch = false;
        if (mSlideEdge == EDGE_LEFT) {
            mDirection = DIRECTION_HORIZONTAL;
            mPositiveX = dx > 0;
            mMinTouch = mPositiveX;
        } else if (mSlideEdge == EDGE_RIGHT) {
            mDirection = DIRECTION_HORIZONTAL;
            mPositiveX = dx > 0;
            mMinTouch = -dx > 0;
        } else if (mSlideEdge == EDGE_TOP) {
            mDirection = DIRECTION_VERTICAL;
            mPositiveY = dy > 0;
            mMinTouch = mPositiveY;
        } else if (mSlideEdge == EDGE_BOTTOM) {
            mDirection = DIRECTION_VERTICAL;
            mPositiveY = dy > 0;
            mMinTouch = -dy > 0;
        } else if (mSlideEdge == (EDGE_LEFT | EDGE_RIGHT)) {
            mDirection = DIRECTION_HORIZONTAL;
            mPositiveX = dx > 0;
            mMinTouch = Math.abs(dx) > 0;

        } else if (mSlideEdge == (EDGE_TOP | EDGE_BOTTOM)) {
            mDirection = DIRECTION_VERTICAL;
            mPositiveY = dy > 0;//正方向
            mMinTouch = Math.abs(dy) > 0;
        } else if (mSlideEdge == (EDGE_LEFT | EDGE_TOP)) {
            if (mSingleDirection) {
                boolean slideX = Math.abs(dx) > Math.abs(dy);
                mDirection = slideX ? DIRECTION_HORIZONTAL : DIRECTION_VERTICAL;
                mPositiveX = dx > 0;
                mPositiveY = dy > 0;
                mMinTouch = slideX ? dx > 0 : dy > 0;
            } else {
                mDirection = DIRECTION_ALL;
                mPositiveX = dx > 0;
                mPositiveY = dy > 0;
                mMinTouch = mPositiveX && mPositiveY;
            }
        } else if (mSlideEdge == (EDGE_LEFT | EDGE_BOTTOM)) {
            if (mSingleDirection) {
                boolean slideX = Math.abs(dx) > Math.abs(dy);
                mDirection = slideX ? DIRECTION_HORIZONTAL : DIRECTION_VERTICAL;
                mPositiveX = dx > 0;
                mPositiveY = dy > 0;
                mMinTouch = slideX ? dx > 0 : -dy > 0;
            } else {
                mDirection = DIRECTION_ALL;
                mPositiveX = dx > 0;
                mPositiveY = dy > 0;
                mMinTouch = mPositiveX && !mPositiveY;
            }
        } else if (mSlideEdge == (EDGE_RIGHT | EDGE_TOP)) {
            if (mSingleDirection) {
                boolean slideX = Math.abs(dx) > Math.abs(dy);
                mDirection = slideX ? DIRECTION_HORIZONTAL : DIRECTION_VERTICAL;
                mPositiveX = dx > 0;
                mPositiveY = dy > 0;
                mMinTouch = slideX ? -dx > 0 : dy > 0;
            } else {
                mDirection = DIRECTION_ALL;
                mPositiveX = dx > 0;
                mPositiveY = dy > 0;
                mMinTouch = !mPositiveX && mPositiveY;
            }
        } else if (mSlideEdge == (EDGE_RIGHT | EDGE_BOTTOM)) {
            if (mSingleDirection) {
                boolean slideX = Math.abs(dx) > Math.abs(dy);
                mDirection = slideX ? DIRECTION_HORIZONTAL : DIRECTION_VERTICAL;
                mPositiveX = dx > 0;
                mPositiveY = dy > 0;

                mMinTouch = slideX ? -dx > 0 : -dy > 0;
            } else {
                mDirection = DIRECTION_ALL;
                mPositiveX = dx > 0;
                mPositiveY = dy > 0;
                mMinTouch = !mPositiveX && !mPositiveY;
            }
        } else if (mSlideEdge == (EDGE_LEFT | EDGE_RIGHT | EDGE_TOP)) {

            if (mSingleDirection) {
                boolean slideX = Math.abs(dx) > Math.abs(dy);
                mDirection = slideX ? DIRECTION_HORIZONTAL : DIRECTION_VERTICAL;
                mPositiveX = dx > 0;
                mPositiveY = dy > 0;
                mMinTouch = slideX ? Math.abs(dx) > 0 : dy > 0;
            } else {
                mDirection = DIRECTION_ALL;
                mPositiveX = dx > 0;
                mPositiveY = dy > 0;
                mMinTouch = mPositiveY;
            }
        } else if (mSlideEdge == (EDGE_LEFT | EDGE_RIGHT | EDGE_BOTTOM)) {
            boolean slideX = Math.abs(dx) > Math.abs(dy);
            if (mSingleDirection) {
                //必须只有一种情况的下
                mDirection = slideX ? DIRECTION_HORIZONTAL : DIRECTION_VERTICAL;
                mPositiveX = dx > 0;
                mPositiveY = dy > 0;
                mMinTouch = slideX ? Math.abs(dx) > 0 : -dy > 0;
            } else {
                mDirection = DIRECTION_ALL;
                mPositiveX = dx >= 0;
                mPositiveY = dy >= 0;
                mMinTouch = !mPositiveY;
            }
        } else if (mSlideEdge == (EDGE_TOP | EDGE_BOTTOM | EDGE_LEFT)) {
            if (mSingleDirection) {
                boolean slideX = Math.abs(dx) > Math.abs(dy);
                mDirection = slideX ? DIRECTION_HORIZONTAL : DIRECTION_VERTICAL;
                mPositiveX = dx > 0;
                mPositiveY = dy > 0;
                mMinTouch = slideX ? dx > 0 : Math.abs(dy) > 0;
            } else {
                mDirection = DIRECTION_ALL;
                mPositiveX = dx > 0;
                mPositiveY = dy > 0;
                mMinTouch = mPositiveX;
            }
        } else if (mSlideEdge == (EDGE_TOP | EDGE_BOTTOM | EDGE_RIGHT)) {
            if (mSingleDirection) {
                boolean slideX = Math.abs(dx) > Math.abs(dy);
                mDirection = slideX ? DIRECTION_HORIZONTAL : DIRECTION_VERTICAL;
                mPositiveX = dx > 0;
                mPositiveY = dy > 0;
                mMinTouch = slideX ? -dx > 0 : Math.abs(dy) > 0;
            } else {
                mDirection = DIRECTION_ALL;
                mPositiveX = dx >= 0;
                mPositiveY = dy >= 0;
                mMinTouch = !mPositiveX;
            }
        } else if (mSlideEdge == EDGE_ALL) {
            if (mSingleDirection) {
                boolean slideX = Math.abs(dx) >= Math.abs(dy);
                mDirection = slideX ? DIRECTION_HORIZONTAL : DIRECTION_VERTICAL;
                mPositiveX = dx > 0;
                mPositiveY = dy > 0;
                mMinTouch = slideX ? Math.abs(dx) > 0 : Math.abs(dy) > 0;
            } else {
                mDirection = DIRECTION_ALL;
                mPositiveX = dx > 0;
                mPositiveY = dy > 0;
                mOverflowParent = true;
                mMinTouch = true;
            }
        }
        return mMinTouch;
    }
/**
     * 优先在某个方向上滚动
     */
    private boolean chcekScrollPriority(float dx, float dy) {
        if (mDirection == DIRECTION_HORIZONTAL) {
            return Math.abs(dx) - Math.abs(dy) > 0;
        } else if (mDirection == DIRECTION_VERTICAL) {
            return Math.abs(dy) - Math.abs(dx) > 0;
        } else {
            //互斥方向的话无优先级
            return true;
        }
    }
 /**
     * 检测是否可以滚动
     *
     * @return
     */
    private boolean checkCanScrolly(float dx, float dy) {
        //如果优先处理子类View的滚动事件的话,需要先处理子类的,然后才交给自己
        if (mDirection == DIRECTION_HORIZONTAL) {
            return canScrollHorizontally(this, false, (int) dx, (int) mDownX, (int) mDownY);
        } else if (mDirection == DIRECTION_VERTICAL) {
            return canScrollVertically(this, false, (int) dy, (int) mDownX, (int) mDownY);
        } else if (mDirection == DIRECTION_ALL) {
            boolean canScrollH2 = canScrollHorizontally(this, false, (int) dx, (int) mDownX, (int) mDownY);
            boolean canScrollV2 = canScrollVertically(this, false, (int) dy, (int) mDownX, (int) mDownY);
            return canScrollH2 || canScrollV2;
        }
        return false;
    }

关于监测视图水平或者垂直方向上是否可滑动,下面只以监测水平方向为例子说明,垂直方向的可参考代码。

/**
     * 当前触点所在iew
     * 垂直方向上是否
     * 可以滚动
     *
     * @param v
     * @param dy
     * @param x
     * @param y
     * @return
     */
    private boolean canScrollVertically(View v, boolean checkV, int dy, int x, int y) {
        if (v instanceof ViewGroup) {
            final ViewGroup group = (ViewGroup) v;
            final int scrollX = getScrollX();
            final int scrollY = getScrollY();
            final int count = group.getChildCount();
            for (int i = count - 1; i >= 0; i--) {
                final View child = group.getChildAt(i);
                boolean touchInChild = checkTouchInChild(child, x, y);
                //只有当触点在view之内才判断
                if (touchInChild && canScrollVertically(child, true, dy,
                        x + scrollX - child.getLeft(),
                        y + scrollY - child.getTop()))
                    return true;
            }
        }

        return checkV && v.canScrollVertically(-dy);
    }

说明:
1、如果视图是ViewGroup类型,通过递归判断其子类是否可水平滑动
2、判断当前触点所在View水平方向上是否可滑动
3、checkV:false代表不检测自己,true代表检测子视图

DOWN事件:

主要是记录触点已经判断触点是否在直属子视图中,还有对于scroller滑动状态的判断,当处于滑动未结束的情况下,需要禁止父类拦截

/**
     * 不让父类拦截事件
     */
    private void disallowInterceptTouchEvent() {
        final ViewParent parent = getParent();
        if (parent != null)
            parent.requestDisallowInterceptTouchEvent(true);
    }
UP和CANCLE事件

如果处于可拖拽状态,需要恢复默认位置

/**
     * 恢复初始状态
     *
     * @param scrollX
     * @param scrollY
     */
    private void revertOriginalState(int scrollX, int scrollY, boolean overThreshold) {
        //恢复真正的状态
        smoothllyScroll(scrollX, scrollY, -scrollX, -scrollY, mScrollTime);
        //监听
        if (mOnSlideListener != null)
            mOnSlideListener.onSlideRecover(this, overThreshold);
    }
/**
     * 平滑滑动
     *
     * @param startX
     * @param startY
     * @param endX
     * @param endY
     * @param computeTime 计算滑动时间
     * @param mScrollTime
     */
    public void smoothllyScroll(int startX, int startY, int endX, int endY, boolean computeTime, int mScrollTime) {
        mCurrentState = STATE_RELEASE;
        int duration;
        if (computeTime) {
            //计算百分比时间
            float offsetXPercent = Math.abs(endX) * 1f / mMeasuredWidth;
            float offsetYPercent = Math.abs(endY) * 1f / mMeasuredHeight;
            duration = (int) (Math.max(offsetXPercent, offsetYPercent) * mScrollTime);
        } else {
            duration = mScrollTime;
        }

        mScroller.startScroll(startX, startY, endX, endY, duration);
        ViewCompat.postInvalidateOnAnimation(this);
    }
POINTER_UP事件

手势第二次触点抬起的动作,恢复触点位置即可

/**
     * 释放第二次触点
     *
     * @param ev
     */
    private void onSecondaryPointerUp(MotionEvent ev) {
        final int pointerIndex = ev.getActionIndex();
        final int pointerId = ev.getPointerId(pointerIndex);
        if (pointerId == mActivePointerId) {
            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
            mDownX = ev.getX(newPointerIndex);
            mDownY = ev.getY(newPointerIndex);
            mActivePointerId = ev.getPointerId(newPointerIndex);
        }
    }

消费事件

消费事件有三处来源,一是通过拦截拦截,二是不拦截,但是其子视图不消费,自身消费事件,三是手指二次按下,由于第一次已经消费,因此此次按下当由自身消费。消费的条件与拦截条件几乎一致,只是对事件做了不同的处理条件,具体如下:

@Override
    public boolean onTouchEvent(MotionEvent event) {
        final int action = event.getActionMasked();
        int pointerIndex;

        //第一步:监测是否含子类
        boolean checkNullChild = checkNullChild();
        if (checkNullChild || !mSlideEnable) {
            return super.onTouchEvent(event);
        }

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mDownX = event.getX();
                mDownY = event.getY();
                mActivePointerId = event.getPointerId(0);
                mCheckTouchInChild = checkTouchInChild(mChildRootView, mDownX, mDownY);

                if (mIsBeingDragged) {
                    disallowInterceptTouchEvent();
                }

                break;
            case MotionEvent.ACTION_MOVE:
                //如果触点不在子类中直接返回
                if (!mCheckTouchInChild) {
                    break;
                }
                //检测触点
                pointerIndex = event.findPointerIndex(mActivePointerId);
                if (pointerIndex == INVALID_POINTER) {
                    break;
                }

                float dx = event.getX(pointerIndex) - mDownX;
                float dy = event.getY(pointerIndex) - mDownY;
                performDrag(event, dx, dy, pointerIndex);

                break;
            case MotionEvent.ACTION_UP:
                // 根据手指释放时的位置决定回弹还是关闭,只要有一方超越就结束
                pointerIndex = event.findPointerIndex(mActivePointerId);
                if (pointerIndex == INVALID_POINTER) {
                    break;
                }

                performRelease();
                mActivePointerId = INVALID_POINTER;
                break;

            case MotionEvent.ACTION_POINTER_DOWN:
                //第二步:监测触点范围(必须有子类才去监测触点范围)
                if (mMultiPointerEnable) {
                    pointerIndex = event.getActionIndex();
                    mDownX = (int) event.getX(pointerIndex);
                    mDownY = (int) event.getY(pointerIndex);
                    mActivePointerId = event.getPointerId(pointerIndex);
                    mCheckTouchInChild = checkTouchInChild(mChildRootView, mDownX, mDownY);

                    if (mIsBeingDragged) {
                        disallowInterceptTouchEvent();
                    }
                }

                break;

            case MotionEvent.ACTION_POINTER_UP:
                //也可以做边缘释放,后期可以添加
                if (mMultiPointerEnable) {
                    onSecondaryPointerUp(event);
                    mCheckTouchInChild = checkTouchInChild(mChildRootView, mDownX, mDownY);
                }

                break;
            case MotionEvent.ACTION_CANCEL:
                if (mIsBeingDragged) {
                    revertOriginalState(getScrollY(), getScrollY(), false);
                }
                mIsBeingDragged = false;
                mActivePointerId = INVALID_POINTER;
                return false;
        }

        return true;
    }
Move事件:

由于来源有三处,其中有通过拦截获取,因此首先判断当前触点是否在直属子视图中。

/**
     * 拖拽操作
     *
     * @param event
     * @param dx
     * @param dy
     * @param pointerIndex
     */
    private void performDrag(MotionEvent event, float dx, float dy, int pointerIndex) {
        if (mIsBeingDragged) {
            disallowInterceptTouchEvent();
            //触发监听 UP的时候取消监听
            if (mOnSlideListener != null && mCurrentState != STATE_DRAG) {
                mOnSlideListener.onSlideStart(this);
            }

            mCurrentState = STATE_DRAG;
            int scrollX = getScrollX();
            int scrollY = getScrollY();

            if (mDirection == DIRECTION_HORIZONTAL) {
                boolean slideWelt = mPositiveX ? scrollX >= dx : scrollX <= dx;
                if (slideWelt && !mOverflowParent) {
                    scrollTo(0, 0);
                } else {
                    scrollBy((int) -dx, 0);
                }
            } else if (mDirection == DIRECTION_VERTICAL) {
                boolean slideWelt = mPositiveY ? scrollY >= dy : scrollY <= dy;
                if (slideWelt && !mOverflowParent) {
                    scrollTo(0, 0);
                } else {
                    scrollBy(0, (int) -dy);
                }
            } else if (mDirection == DIRECTION_ALL) {
                boolean limitX = mPositiveX ? scrollX >= dx : scrollX <= dx;
                boolean limitY = mPositiveY ? scrollY >= dy : scrollY <= dy;
                int realDx = limitX ? mOverflowParent ? (int) -dx : 0 : (int) -dx;
                int realDy = limitY ? mOverflowParent ? (int) -dy : 0 : (int) -dy;
                scrollBy(realDx, realDy);
            }

            //绘制背景
            invalidateBackground(scrollX, scrollY);
            mDownX = event.getX(pointerIndex);
            mDownY = event.getY(pointerIndex);
        } else {
            mIsBeingDragged = chechkCanDrag(dx, dy);
        }
    }
/**
     * 绘制背景
     * 缩放和背景颜色渐变
     *
     * @param scrollX
     * @param scrollY
     */
    private void invalidateBackground(int scrollX, int scrollY) {
        //计算滑动比例
        float mPercentSlideX = (scrollX * 1.0f) / mMeasuredWidth;
        float mPercentSlideY = (scrollY * 1.0f) / mMeasuredHeight;

        float maxPercent = Math.max(Math.abs(mPercentSlideX), Math.abs(mPercentSlideY));
        float mMaxScal = 0, mMaxAlpha = 0;

        //设置缩放
        if (mScaleEnable && mChildRootView != null) {
            //限制缩放最小值
            mMaxScal = maxPercent / mScaleRate;
            float limitScal = mMaxScal > 1 - mMinScale ? 1 - mMinScale : mMaxScal;
            mMaxScal = 1 - limitScal;
            mChildRootView.setScaleX(mMaxScal);
            mChildRootView.setScaleY(mMaxScal);
        }

        //设置背景
        if (mAlphaEnable) {
            float maxAlpha = maxPercent / mAlphaRate;
            float limitAlpha = maxAlpha > 1 - mMinAlpha ? 1 - mMinAlpha : maxAlpha;
            mMaxAlpha = 1 - limitAlpha;
            if (mBackground != null && mAlphaEnable) {
                mBackground.mutate().setAlpha((int) ((mMaxAlpha) * 255));
            }
        }

        //相对于屏幕的比例
        if (mOnSlideListener != null)
            mOnSlideListener.onSlideChange(this,
                    mPercentSlideX, mPercentSlideY,
                    mMaxScal, mMaxAlpha);
    }

说明:
1、拖拽条件判断与拦截一致
2、当执行拖拽的时,禁止父类拦截事件

Up事件:

up事件主要是松手之后执行自动滑动,通过判断拖拽位置是否超越阈值来设置最后的状态为原始状态还是关闭状态

/**
     * 释放手势
     */
    private void performRelease() {
        if (mIsBeingDragged) {
            int scrollX = getScrollX();
            int scrollY = getScrollY();

            mOverThreshold = checkThreshold(scrollX, scrollY);
            if (mCheckThreshold && mOverThreshold) {
                int endScrollX = scrollX < 0 ? -scrollX - mMeasuredWidth : mMeasuredWidth - scrollX;
                int endScrollY = scrollY < 0 ? -scrollY - mMeasuredHeight : mMeasuredHeight - scrollY;
                endScrollX = mDirection == DIRECTION_VERTICAL ? 0 : endScrollX;
                endScrollY = mDirection == DIRECTION_HORIZONTAL ? 0 : endScrollY;
                smoothllyScroll(scrollX, scrollY, endScrollX, endScrollY, mScrollTime);
            } else {
                revertOriginalState(scrollX, scrollY, mOverThreshold);
            }
        }
    }
/**
     * 检测阈值
     *
     * @return
     */
    private boolean checkThreshold(int scrollX, int scrollY) {
        if (mDirection == DIRECTION_HORIZONTAL) {
            return Math.abs(scrollX) > mMeasuredWidth * mSlideThresholdRate;
        } else if (mDirection == DIRECTION_VERTICAL) {
            return Math.abs(scrollY) > mMeasuredHeight * mSlideThresholdRate;
        } else {
            boolean xThreshold = Math.abs(scrollX) > mMeasuredWidth * mSlideThresholdRate;
            boolean yThreshold = Math.abs(scrollY) > mMeasuredHeight * mSlideThresholdRate;
            return xThreshold || yThreshold;
        }
    }

说明:重写computeScroll可监测滑动是否结束,在释放手势的时候回调,并且需要绘制背景

/**
     * 平滑的滚动到最终位置
     */
    @Override
    public void computeScroll() {
        int oldX = getScrollX();
        int oldY = getScrollY();
        if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
            int x = mScroller.getCurrX();
            int y = mScroller.getCurrY();
            //位置改变才去滑动
            if (oldX != x || oldY != y) {
                scrollTo(x, y);
                //绘制背景 如果是不检测阈值并且超过阈值则不绘制
                if (mCheckThreshold || !mOverThreshold) {
                    invalidateBackground(x, y);
                }
            }
            ViewCompat.postInvalidateOnAnimation(this);
            return;
        } else {
            boolean originalState = Math.abs(oldX) == 0 && Math.abs(oldY) == 0;
            boolean outParent = Math.abs(oldX) >= mMeasuredWidth || Math.abs(oldY) >= mMeasuredHeight;
            //释放状态
            if (originalState || outParent) {
                mCurrentState = STATE_IDLE;
                mIsBeingDragged = false;
            }
            if (outParent) {
                if (mOnSlideListener != null) mOnSlideListener.onSlideFinish(this);
                if (mActivity != null) mActivity.finish();
            }
        }

    }
POINTER_DOWN事件

主要处理第二次按下,重新设置触点位置,以及检测触点是否在直属子视图内和禁止父视图拦截事件

if (mMultiPointerEnable) {
   pointerIndex = event.getActionIndex();
   mDownX = (int) event.getX(pointerIndex);
   mDownY = (int) event.getY(pointerIndex);
   mActivePointerId = event.getPointerId(pointerIndex);
   mCheckTouchInChild = checkTouchInChild(mChildRootView, mDownX, mDownY);

   if (mIsBeingDragged) {
      disallowInterceptTouchEvent();
    }
}
POINTER_UP事件

再次检测初次按下位置是否在直属子视图中,目的是为了第一次滑动可继续

附属功能

系统状态栏

由于本次封装的主要功能是用于侧滑结束Activity,因此如果不做特殊处理,无法实现沉浸式Activity,侧滑结束效果不理想。因此需要单独对于系统栏做特殊处理。如实现沉浸式状态栏,需要Android版本大于5.0,Api版本大于21,具体处理如下。

/**
     * 下面三个方法主要用于处理状态栏
     */
    private void overlayStatusBar(Context context) {
        //获取系统默认状态栏颜色
        if (ViewCompat.getFitsSystemWindows(this)) {
            ViewCompat.setOnApplyWindowInsetsListener(this,
                    new android.support.v4.view.OnApplyWindowInsetsListener() {
                        @Override
                        public WindowInsetsCompat onApplyWindowInsets(View view, WindowInsetsCompat insets) {
                            final SuperSlideLayout superSlideLayout = (SuperSlideLayout) view;
                            superSlideLayout.setChildInsets(insets, insets.getSystemWindowInsetTop() > 0);
                            return insets.consumeSystemWindowInsets();
                        }
                    });

            //版本高于21才能采用透明状态栏
            if (Build.VERSION.SDK_INT >= 21) {

                int[] THEME_STATUSBAR = {android.R.attr.statusBarColor};
                final TypedArray a = context.obtainStyledAttributes(THEME_STATUSBAR);
                try {
                    mStatusBarBackground = a.getDrawable(0);
                } finally {
                    a.recycle();
                }
                setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                        | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
            }
        }
    }

    @RestrictTo(LIBRARY_GROUP)
    public void setChildInsets(WindowInsetsCompat insets, boolean draw) {
        mLastInsets = insets;
        mDrawStatusBarBackground = draw;
        setWillNotDraw(!draw && getBackground() == null);
        requestLayout();
    }

    /**
     * 绘制状态栏
     *
     * @param c
     */
    @Override
    public void onDraw(Canvas c) {
        super.onDraw(c);
        if (mDrawStatusBarBackground && mStatusBarBackground != null) {
            final int inset;
            if (Build.VERSION.SDK_INT >= 21) {
                inset = mLastInsets != null
                        ? ((WindowInsetsCompat) mLastInsets).getSystemWindowInsetTop() : 0;
            } else {
                inset = 0;
            }
            if (inset > 0) {
                mStatusBarBackground.setBounds(0, 0, getWidth(), inset);
                mStatusBarBackground.draw(c);
            }
        }
    }

说明:要实现沉浸式状态栏,需要三个条件,分别如下:
1、设置为Activity的setContentView()顶层视图
2、设置android:fitsSystemWindows="true"
3、设置样式

<style name="AppTheme.NoActionBar">
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
        <item name="android:statusBarColor">@android:color/transparent</item>
    </style>

    <style name="ImmersiveTheme" parent="@style/AppTheme.NoActionBar">
        <!--透明导航栏-->
        <item name="android:statusBarColor">@android:color/transparent</item>
        <item name="android:windowDrawsSystemBarBackgrounds">true</item>
    </style>

Home键处理

按下home键的时候,需要设置恢复原始位置

/**
     * 当window焦点改变的时候回调
     *
     * @param hasWindowFocus
     */
    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
        if (!hasWindowFocus && mIsBeingDragged) {
            //如果未归位,则条用
            int scrollX = getScrollX();
            int scrollY = getScrollY();
            revertOriginalState(scrollX, scrollY, false);
        }
    }

关于链式封装

由于属性过多,遂采用链式封装,便于调用。可用于Activity和普通View,本文主要是通过Scroller来实现滑动子视图,因此可用于Activity或普通View。

/**
     * 绑定目标
     *
     * @param builder
     * @param target
     */
    private SlideLayoutImpl attachTarget(Builder builder, Object target) {
        if (builder.mContext != null) {
            if (builder.mSlideEnable) {
                //设置公共参数
                SuperSlideLayout superSlideLayout = new SuperSlideLayout(builder.mContext);
                superSlideLayout.setSlideEnable(builder.mSlideEnable);
                superSlideLayout.setSlideEdge(builder.mSlideEdge);
                superSlideLayout.setSlideThresholdRate(builder.mSlideThresholdRate);
                superSlideLayout.setCheckThreshold(builder.mCheckThreshold);
                superSlideLayout.setAlphaEnable(builder.mAlphaEnable);
                superSlideLayout.setAlphaRate(builder.mAlphaRate);
                superSlideLayout.setMinAlpha(builder.mMinAlpha);
                superSlideLayout.setScaleEnable(builder.mScaleEnable);
                superSlideLayout.setScaleRate(builder.mScaleRate);
                superSlideLayout.setMinScale(builder.mMinScale);
                superSlideLayout.setOverflowParent(builder.mOverflowParent);
                superSlideLayout.setSingleDirection(builder.mSingleDirection);
                superSlideLayout.setMultiPointerEnable(builder.mMultiPointerEnable);
                superSlideLayout.setScrollTime(builder.mScrollTime);
                superSlideLayout.setBackground(builder.mBackground);
                superSlideLayout.setForeground(builder.mForeground);
                superSlideLayout.setOnSlideListener(builder.mOnSlideListener);

                if (target instanceof View) {
                    superSlideLayout.attachView((View) target);
                } else if (target instanceof Activity) {
                    superSlideLayout.attachActivity((Activity) target);
                }

                return superSlideLayout;
            }
        }

        return null;
    }
 /**
     * 绑定子视图
     */
    public void attachView(View view) {
        if (view != null) {
            ViewParent parent = view.getParent();
            if (parent != null) {
                ViewGroup parentView = (ViewGroup) parent;
                parentView.removeView(view);
                mChildRootView = view;
                addView(view);
                parentView.addView(this);
            }
        } else {
            throw new NullPointerException("ready to attach child view is null");
        }
    }

    /**
     * 绑定Activity
     */
    public void attachActivity(Activity activity) {
        if (activity != null) {
            mActivity = activity;
            ViewGroup decorView = (ViewGroup) mActivity.getWindow().getDecorView();
            mChildRootView = decorView.getChildAt(0);//contentview+titlebar
            View contentView = decorView.findViewById(android.R.id.content);
            Drawable contentViewBackground = contentView.getBackground();
            if (contentViewBackground == null) contentView.setBackground(mForeground);
            decorView.removeView(mChildRootView);
            addView(mChildRootView);
            decorView.addView(this);
        } else {
            throw new NullPointerException("ready to attach activity is null");
        }
    }

说明:绑定普通View是否的时候一定只能有一个直属子视图

最后

事件分发在Android中异常重要,因为Android系统主要是靠人与屏幕的触摸去交互。如果单纯说事件分发简单,也不是没有道理,基本做过Android一段时间,都会对事件分发的原理有所了解,大家都喜欢举的例子:公司上级分发事情给下级,这样便于理解。不过事件分发远远没有那么简单。一方面是因为事件分发包含很多不同的事件(down、up、move、pointDown、pointUp等),二是系统对于不同的事件优先级也不同(down优先级高),三是几乎所有的效果或者系统监听都需要不同的事件配合完成,四是事件分发还涉及到事件冲突问题,如何解决事件冲突成了事件分发中最难的部分。本次封装的内容还有诸多不足,如发现问题,请及时反馈,做进一步修改。

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

推荐阅读更多精彩内容