Android View 层级实现仿 iOS 左边缘右滑关闭页面

2018-03-10  本文已影响0人  OhHoo

前言

之前在实现功能需求的时候,遇到过一些知识点和难点,但在解决了之后,自己并没有详细的记录下来,而是仅仅简单的收藏起关键的代码,准备以后需要的时候可以用到。但是过了一段时间之后,当需要再次实现这个功能点的时候,之前那些知识已经忘记的差不多了,所以不得不再次查找资料进行二次学习,现在将其总结起来,方便自己,也分享给需要的人,如果有哪些是错误的,欢迎指正。

需求

前段时间在开发公司项目 V1.0.3 版本的时候,设计师提了个需求,在打开设置页的某项二级页面时,能否像 iOS 那样,打开的时候,二级页面是从右边出现,然后滑到左边的边缘。并且手指在左边的边缘可以滑动这个二级页面,快速滑动的时候,可以关闭这个二级页面。当二级页面滑到中间手指抬起,如果滑动距离超过一半,则关闭二级页面,否则让二级页面回到左边缘。当完全打开二级页面时,按手机 Back 键,需要从左到右滑动页面,然后关闭。

需求分析

首先,分析以上的需求,再结合项目已有的代码布局情况,然后总结,提炼出我们需要完成的功能点。因为项目中的设置页布局最外层是一个 FrameLayout,所以首先想到的就是我们自定义的View 基础自 FrameLayout,这样可以方便的加上之前的项目代码。需求的功能点如下:

效果截屏

技术实现

以上为此次自定义 View 的实现效果,下面介绍使用方法以及源码分析

  1. onInterceptTouchEvent
@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int action = MotionEventCompat.getActionMasked(ev);
        switch (action){
            case MotionEvent.ACTION_DOWN:{
                //Down 事件触发时,表示有第一个手指接触到屏幕了
                //获取第一个手指Down 的PointerId
                mActivePointerId = ev.getPointerId(0);
                mInitDownX = getMotionEventX(ev);
//                mLastDownX = mInitDownX;
                mInitDownY = getMotionEventY(ev);
                if(mInitDownX == INVALID_VALUE || mInitDownY == INVALID_VALUE){
                    mIsBeingDrag = false;
                    return super.onInterceptTouchEvent(ev);
                }
                break;
            }
            case MotionEvent.ACTION_MOVE:{
                float x = getMotionEventX(ev);
                float y = getMotionEventY(ev);

                float diffX = x - mInitDownX;
                float diffY = y - mInitDownY;

                //手指按下的初始位置在屏幕左侧的 十分之一的范围里,并且 X 方向的距离
                //比 Y 方向上的多,也超过最小的 mTouchSlop,就可以认为已经开始拖拽了
                if( mInitDownX < getWidth() / 10 && Math.abs(diffX) >= mTouchSlop
                        && Math.abs(diffX) > Math.abs(diffY)){
                    mIsBeingDrag = true;
                }

                break;

            }
            case MotionEvent.ACTION_POINTER_UP:{
                //当有多个手指按在屏幕上,其中一个手指抬起时会进入此方法
                onSecondaryPointerUp(ev);
                break;
            }
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:{
                //最后一个手指抬起,或者事件被父view 拦截时,恢复到初始状态
                mIsBeingDrag = false;
                mInitDownX = 0;
                mInitDownY = 0;
//                mLastDownX = 0;
                mActivePointerId = MotionEvent.INVALID_POINTER_ID;
                break;
            }
        }

        //如果 mIsBeingDrag 为 true ,说明已经触发了滑动的条件
        //事件会被拦截,交给 onTouchEvent 处理
        return mIsBeingDrag ||  super.onInterceptTouchEvent(ev);
    }

首先,我们在第一个手指按下的时候,记录其 PointerId 值,和最初的 X,Y值。然后在手指移动的过程中,也就是 Move 事件中判断,如果手指按下的初始位置在屏幕左侧的 十分之一的范围里,并且 X 方向的距离比 Y 方向上的多,也超过最小的 mTouchSlop,就可以认为已经开始拖拽了,mIsBeingDrag 被设置为 true, 方法结束的时候返回 mIsBeingDrag ,代表我们拦截了此次事件。
需要注意的是,我们在 onInterceptTouchEvent 方法中加多了 MotionEvent.ACTION_POINTER_UP 这个判断,因为我们设置第一个按下的手指的 PointerId 为 mActivePointerId,所以考虑到有可能在按下第一个手指的时候并未触发滑动,而是接着按下第二、第三...个手指,但是在抬起手指的情况下,有可能会抬起第一个手指,所以这里加多了这种情况的处理。

/**
     * 当屏幕上有手指抬起时,判断是不是 Down 事件触发时记录的 PointerId
     * 如果是的话,选其他手指的 PointerId 作为 mActivePointerId
     * @param event
     */
    private void onSecondaryPointerUp(MotionEvent event){
        int pointerIndex = MotionEventCompat.getActionIndex(event);
        int pointerId = event.getPointerId(pointerIndex);
        if(pointerId == mActivePointerId){
            int newPointerIndex = pointerIndex == 0 ? 1: 0;
            mActivePointerId = event.getPointerId(newPointerIndex);
        }

    }
  1. onTouchEvent
    当 onInterceptTouchEvent 返回 true 的情况下,事件被拦截,交由 onTouchEvent 处理
 @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (MotionEventCompat.getActionMasked(event)){

            case MotionEvent.ACTION_DOWN:{
                mInitDownX = getMotionEventX(event);
                mInitDownY = getMotionEventY(event);
                break;
            }
            case MotionEvent.ACTION_MOVE:{

                //初始化速度追踪器,用以追踪手指的滑动速度
                if(mVelocityTracker == null){
                    mVelocityTracker = VelocityTracker.obtain();
                }
                mVelocityTracker.addMovement(event);

                float x = getMotionEventX(event);
                float diffX = x - mInitDownX;

                if( diffX >= 0 ){
                    //手指是向右滑动的,偏移 SliderView
                    if(mSliderView != null){
                        mSliderView.setTranslationX(diffX);
                    }
                }

                if(mSliderListener != null){
                    mSliderListener.onProgress((int) diffX,diffX * 1.0f  / getWidth(),mSliderView);
                }

                Log.w("lala","getScrollX: "+diffX+" rate: "+ diffX * 1.0f / getWidth() );

                // 左侧即将滑出屏幕

                return true;
            }
            case MotionEvent.ACTION_POINTER_UP:
                //当有多个手指按在屏幕上,其中一个手指抬起时会进入此方法
                onSecondaryPointerUp(event);
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:{

                if(mVelocityTracker != null  && mActivePointerId != MotionEvent.INVALID_POINTER_ID){
                    //获取手指抬起的一瞬间,获取 X 方向上的速度
                    mVelocityTracker.computeCurrentVelocity(1000);
                    float xVelocity = mVelocityTracker.getXVelocity(mActivePointerId);
                    Log.w("tracker","X velocity: "+xVelocity);

                    mVelocityTracker.clear();
                    mVelocityTracker = null;
                    if( xVelocity >= HORIZANTAL_SPEED  && mSliderView != null){
                        //如果水平的速度超过了特定值,可以认为是手指 fling 操作
                        //让 sliderview 做向右的动画操作,关闭页面
                        mCurTranslationX = mSliderView.getTranslationX();

                        actionEnd(true);
                        break;

                    }

                }

                // 根据手指释放时的位置决定回弹还是关闭
                float x = getMotionEventX(event);
                float diffX = x - mInitDownX;
                if( diffX == 0 ){
                    //手指滑动了 sliderview,但是最后手指抬起时,让它回到了原来的位置
                    if(mSliderListener != null){
                        mSliderListener.onSliderShow(mSliderView);
                    }
                    resetValue();

                } else  if( diffX == getWidth()){
                    if(mSliderListener != null){
                        mSliderListener.onSliderHidden();
                    }
                    resetValue();

                } else {

                    if (mSliderView != null ){

                        mCurTranslationX = mSliderView.getTranslationX();
                        //sliderview 在 水平方向的偏移少于父布局的宽度的一半
                        //则让其回到原位,否则做动画打开
                        if(mCurTranslationX < getWidth() / 2){
                            actionEnd(false);
                        }
                        else {
                            actionEnd(true);
                        }
                    }

                }

                break;
            }

        }
        return super.onTouchEvent(event);
    }

事件拦截后,我们在 ACTION_MOVE 里判断手指是不是从左边滑向右边,是的话,让 view 往右边偏移,防止将其滑出屏幕。同样有可能出现的一种情况是,我们在用第一个手指滑动 view ,然后接着按下第二、第三...个手指,接着又抬起手指,所以同样的这里也要考虑 ACTION_POINTER_UP 这种情况。
最后,在最后一个手指抬起时,判断水平方向上的速度是否超过了限定的阈值,超过的话,开启动画关闭页面。否则根据已经滑动的距离,判断是否关闭页面。

  1. 接口回调
public interface OnSliderListener{
        //判断打开的进度
        void onProgress(int current, float progress,View view);
        //页面关闭
        void onSliderHidden();
        //页面打开
        void onSliderShow(View  page);
    }

总结

由于知识水平有限,难免有错误,欢迎指正,最后附上项目代码地址。
https://github.com/hanilala/CoolCode

上一篇 下一篇

猜你喜欢

热点阅读