SwipeBackLayout源码解析

    SwipeBackLayout是一个开源的实现了Activity滑动返回的库,当Activity滑动返回时,另一个Activity界面逐渐显示。效果图如下:




SwipeBackLayout


    从上图中看出,滑动的是一个Activity上最外层的ViewGroup,使用帧布局比较合适,在SwipeBackLayout的源码中,他也是继承自帧布局。在帧布中定义了一些常量和一些成员变量,例如:滑动速率,屏幕边沿标志,滑动过程中的一些参数等等。在构造方法里面进行了一些自定义属性的初始化内容。在SwipeBackLayout控件中有一个ViewDragHelper,他是实现手势触摸识别的核心类,要研究SwipeBackLayout的源码,首先从ViewDragHelper进行着手。


ViewDragHelper


    在处理触摸事件中常常使用事件分发机制进行分析,对于复杂的事件处理使用这种机制分析起来很麻烦,正常情况下需要响应down事件,记录当时触摸的点,然后根据这个点,判断触摸到了哪个view,其次响应move事件,得到移动的x轴、y轴的偏移量,最后将偏移量作用于view。因此使用ViewDragHelper进行处理,他是官方v4包下提供的一个类,提供了一系列的方法和状态跟踪,能够简化事件处理流程,可以把以上事件处理全部交给ViewDragHelper进行处理。


    1、将事件拦截交给shouldInterceptTouchEvent(MotionEvent ev)方法

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        if (!mEnable) {
            return false;
        }
        try {
            return mDragHelper.shouldInterceptTouchEvent(event);
        } catch (ArrayIndexOutOfBoundsException e) {
            return false;
        }
    }

    2、将事件触摸交给processTouchEvent(MotionEvent ev),并且返回true,响应此事件。

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!mEnable) {
            return false;
        }
        mDragHelper.processTouchEvent(event);
        return true;
    }

ViewDragCallback


    ViewDragCallback继承自ViewDragHelper.Callback拖拽效果实现类,核心方法如下:


tryCaptureView


    表示该view是否支持滑动,返回true表示支持滑动, false表示不支持滑动,包含两个参数:
    参数一:支持滑动的view
    参数二:多指触控


    实现步骤:
    1、是否支持边沿触控
    2、判断是那侧的边沿触控,设置触控回调
    3、判断是否为指定的滑动,防止乱划,斜着滑屏
    4、是边缘滑动并且满足可以滑动的时候,contentview才可以拖动  

    @Override
    public boolean tryCaptureView(View view, int i) {
        //是否支持边沿触摸
        boolean ret = mDragHelper.isEdgeTouched(mEdgeFlag, i);
        if (ret) {
            //判断三个边沿那侧支持触摸
            if (mDragHelper.isEdgeTouched(EDGE_LEFT, i)) {
                mTrackingEdge = EDGE_LEFT;
            } else if (mDragHelper.isEdgeTouched(EDGE_RIGHT, i)) {
                mTrackingEdge = EDGE_RIGHT;
            } else if (mDragHelper.isEdgeTouched(EDGE_BOTTOM, i)) {
                mTrackingEdge = EDGE_BOTTOM;
            }
            if (mListeners != null && !mListeners.isEmpty()) {
                for (SwipeListener listener : mListeners) {
                    //给回调设置那侧支持触摸
                    listener.onEdgeTouch(mTrackingEdge);
                }
            }
            mIsScrollOverValid = true;
        }
        //判断是否为指定的滑动,防止乱划,斜着滑屏
        boolean directionCheck = false;
        if (mEdgeFlag == EDGE_LEFT || mEdgeFlag == EDGE_RIGHT) {
            //当左右滑动时候检查滑动方向是否是左右滑动,滑动过程中多次被调用
            directionCheck = !mDragHelper.checkTouchSlop(ViewDragHelper.DIRECTION_VERTICAL, i);
        } else if (mEdgeFlag == EDGE_BOTTOM) {
            //当左右滑动时候检查滑动方向是否是上下滑动,滑动过程中多次被调用
            directionCheck = !mDragHelper.checkTouchSlop(ViewDragHelper.DIRECTION_HORIZONTAL, i);
        } else if (mEdgeFlag == EDGE_ALL) {
            directionCheck = true;
        }
        //是边缘滑动并且满足可以滑动的时候,contentview才可以拖动
        return ret & directionCheck;
    }


clampViewPositionHorizontal


    该类是获取child需要被移动到的x轴位置距离,接收三个参数:
    参数一:拖动的view
    参数二:x轴坐标位置
    参数三:x轴移动位置


    实现步骤:
    1、如果为左边界滑动,边界限制为(最小值为0,最大值为contentview的宽度)
    2、如果为右边界滑动,边界限制为(最小值为-contentView的宽度,最大值为0)

    @Override
    public int clampViewPositionHorizontal(View child, int left, int dx) {
        //如果为左边界滑动,边界限制为(最小值为0,最大值为contentview的宽度)
        //如果为右边界滑动,边界限制为(最小值为-contentView的宽度,最大值为0)
        int ret = 0;
        if ((mTrackingEdge & EDGE_LEFT) != 0) {
            ret = Math.min(child.getWidth(), Math.max(left, 0));
        } else if ((mTrackingEdge & EDGE_RIGHT) != 0) {
            ret = Math.min(0, Math.max(left, -child.getWidth()));
        }
        return ret;
    }

clampViewPositionVertical


    该类是获取child需要被移动到的y轴位置距离,接收三个参数:
    参数一:拖动的view
    参数二:y轴坐标位置
    参数三:y轴移动位置


    实现步骤:
    如果为底部滑动,边界限制为(最小值为-contentView的高度,最大值为0);

    @Override
    public int clampViewPositionVertical(View child, int top, int dy) {
        //如果为底部滑动,边界限制为(最小值为-contentView的高度,最大值为0)
        int ret = 0;
        if ((mTrackingEdge & EDGE_BOTTOM) != 0) {
            ret = Math.min(0, Math.max(top, -child.getHeight()));
        }
        return ret;
    }

getViewHorizontalDragRange


    该类表示是否支持水平滑动,返回结果>0,支持水平拖动;<0,不支持,接收一个参数:
    参数一:拖动的view


    实现步骤:
    如果指定的滑动为左右滑动,则支持

    @Override
    public int getViewHorizontalDragRange(View child) {
        //如果指定的滑动是做边沿或者有边沿则支持左右滑动
        return mEdgeFlag & (EDGE_LEFT | EDGE_RIGHT);
    }

getViewVerticalDragRange


    该类表示是否支持竖直滑动,返回结果>0,支持竖直拖动;<0,不支持,接收一个参数:
    参数一:拖动的view


    实现步骤:
    滑动边沿为底部时候支持竖直滑动

    @Override
    public int getViewVerticalDragRange(View child) {
        //滑动边沿为底部时候支持竖直滑动
        return mEdgeFlag & EDGE_BOTTOM;
    }

onViewPositionChanged


    当位置发生改变时候回调,参数含义,接收三个参数:
    参数一:拖动的view
    参数二三:left,top变化时新的x左/y顶坐标
    参数四五:从旧到新位置的偏移量


    实现步骤:
    1、获取一个滑过位置占屏幕总宽或者高的百分比
    2、获取拖动过后的left跟top,然后调用invalidate会重新绘制ViewGroup,在onLayout方法中重新摆放子控件位置
    3、滑动超过设置自动滑动距离的临界值时回调
    4、当滑动完毕时候,关闭Activity

    @Override
    public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
        super.onViewPositionChanged(changedView, left, top, dx, dy);
        //获取一个滑动位置的百分比参数
        if ((mTrackingEdge & EDGE_LEFT) != 0) {
            //如果是左边滑动的情况,mScrollPercent=拖动过后的left/(mContentView的宽度+左边阴影的宽度)
            mScrollPercent = Math.abs((float) left
                    / (mContentView.getWidth() + mShadowLeft.getIntrinsicWidth()));
        } else if ((mTrackingEdge & EDGE_RIGHT) != 0) {
            mScrollPercent = Math.abs((float) left
                    / (mContentView.getWidth() + mShadowRight.getIntrinsicWidth()));
        } else if ((mTrackingEdge & EDGE_BOTTOM) != 0) {
            mScrollPercent = Math.abs((float) top
                    / (mContentView.getHeight() + mShadowBottom.getIntrinsicHeight()));
        }
        //获取拖动过后的left跟top,然后调用invalidate会重新绘制ViewGroup,在onLayout方法中重新摆放子控件位置
        mContentLeft = left;
        mContentTop = top;
        invalidate();
        if (mScrollPercent < mScrollThreshold && !mIsScrollOverValid) {
            mIsScrollOverValid = true;
        }
        //滑动超过设置距离的临界值时回调
        if (mListeners != null && !mListeners.isEmpty()
                && mDragHelper.getViewDragState() == STATE_DRAGGING
                && mScrollPercent >= mScrollThreshold && mIsScrollOverValid) {
            mIsScrollOverValid = false;
            for (SwipeListener listener : mListeners) {
                listener.onScrollOverThreshold();
            }
        }
        //当activity滑动的完全滑动完毕时候,关闭activity
        if (mScrollPercent >= 1) {
            if (!mActivity.isFinishing()) {
                mActivity.finish();
                mActivity.overridePendingTransition(0, 0);
            }
        }
    }

onViewReleased


    前被捕获的View释放之后回调,即手指抬起的回调,接收三个参数:
    参数一:拖动的view
    参数二:x轴方向瞬时速度
    参数三:y轴方向瞬时速度


    实现步骤:
    1、计算自动滚动到指定的left跟top位置
    2、设置自动滚动到指定的left跟top位置
    3、重绘

    @Override
    public void onViewReleased(View releasedChild, float xvel, float yvel) {
        final int childWidth = releasedChild.getWidth();
        final int childHeight = releasedChild.getHeight();

        //满足条件时候是设置left,top的值,条件内容:如果释放位置的滑动距离大于设定距离自动滑动关闭,
        //否则自动滑动到开始位置
        int left = 0, top = 0;
        if ((mTrackingEdge & EDGE_LEFT) != 0) {
            //当为左边滑动的时候
            //x轴上滑动的速度>=0并且滑动的距离到达总距离自定的0.3f的时候,
            //left即为(content的宽度+左阴影的宽度+超出滑动距离的offset)
            left = xvel > 0 || xvel == 0 && mScrollPercent > mScrollThreshold ? childWidth
                    + mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE : 0;
        } else if ((mTrackingEdge & EDGE_RIGHT) != 0) {
            left = xvel < 0 || xvel == 0 && mScrollPercent > mScrollThreshold ? -(childWidth
                    + mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE) : 0;
        } else if ((mTrackingEdge & EDGE_BOTTOM) != 0) {
            top = yvel < 0 || yvel == 0 && mScrollPercent > mScrollThreshold ? -(childHeight
                    + mShadowBottom.getIntrinsicHeight() + OVERSCROLL_DISTANCE) : 0;
        }
        //自动滚动到指定的left跟top位置
        mDragHelper.settleCapturedViewAt(left, top);
        invalidate();
    }

onViewDragStateChanged


    当ViewDragHelper状态发生变化时回调,接收一个参数:
    参数一:滑动状态表示值


    实现步骤:设置回调通知

    @Override
    public void onViewDragStateChanged(int state) {
        super.onViewDragStateChanged(state);
        if (mListeners != null && !mListeners.isEmpty()) {
            for (SwipeListener listener : mListeners) {
                listener.onScrollStateChange(state, mScrollPercent);
            }
        }
    }

    以上就是SwipeBackLayout核心类ViewDragHelper中所处理的内容,每个方法都有总结,并且源码中都有详细的备注。

    在使用ViewDragHelper.Callback处理完毕滑动事件之后就需要对View进行重新位置的摆放和重绘了。需要重写onLayout,drawChild两个方法,做出相应处理。


onLayout


    onLayout是指在前面Callback的onViewPositionChanged方法中已经获得滑动后View的left、top、mScrollPercent,在调用invalidate时候会调用onLayout方法,重新摆放控件位置。


    实现内容:
    mContentView调用layout方法,进行设置。

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        mInLayout = true;
        if (mContentView != null)
            //对控件进行重新定位调用onLayout()这个方法mContentLeft,mContentTop为拖动后的宽高
            mContentView.layout(mContentLeft, mContentTop,
                    mContentLeft + mContentView.getMeasuredWidth(),
                    mContentTop + mContentView.getMeasuredHeight());
        mInLayout = false;
    }

drawChild


    drawChild是指当View滑动过后,滑过空间部分半透明阴影部分的绘制。


实现内容:
1、获取child的坐标范围,绘制阴影部分,动态设置透明度
2、阴影部分裁剪,防止其余部分被显示

    /**
     * 遍历了所有子View,每个子View都调用了drawChild这个方法
     */
    @Override
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        final boolean drawContent = child == mContentView;

        boolean ret = super.drawChild(canvas, child, drawingTime);
        if (mScrimOpacity > 0 && drawContent
                && mDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE) {
            //阴影部分绘制
            drawShadow(canvas, child);
            //阴影部分裁剪,防止其余部分被显示
            drawScrim(canvas, child);
        }
        return ret;
    }


attachToActivity


    通过阴影部分的绘制,以及透明度的动态设置,就会出现当View滑动时,滑动过的部分渐渐变亮,只到完全显示。现在来看自定义的View如何设置到Activity上去,使Activity变成可拖动的。步骤如下:

    1、获取Activity顶级视图
    2、获得Activity界面所用的xml文件的根view
    3、给Activity的根View设置背景
    4、从Activity的顶级视图移除activity的xml的根view
    5、给SwipeBackLayout添加子view,作为SwipeBackLayout的第一个view
    6、把decorChild当成SwipeBackLayout的contentView进行设置
    7、把SwipeBackLayout作为Activity顶级视图的子View进行设置

    public void attachToActivity(Activity activity) {
        mActivity = activity;
        // 返回一个与主题Theme定义的 attrs数组对应的typedArray类型数组
        TypedArray a = activity.getTheme().obtainStyledAttributes(new int[]{
                android.R.attr.windowBackground
        });
        // 获取typedArray数组中指定位置的资源id值
        int background = a.getResourceId(0, 0);
        a.recycle();

        // 返回顶层窗口根视图
        ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
        //得到我activity的xml的根view
        ViewGroup decorChild = (ViewGroup) decor.getChildAt(0);
        // 给顶层窗口根视图的根view设置背景资源
        decorChild.setBackgroundResource(background);
        // 移除activity的xml的根view
        decor.removeView(decorChild);
        //给SwipeBackLayout添加子view,作为SwipeBackLayout的第一个view
        addView(decorChild);
        //把decorChild当成SwipeBackLayout的contentView进行设置
        setContentView(decorChild);
        decor.addView(this);
    }
    这样View就和Activiy建立起关联了,只需要把decorChild传递进去,SwipeBackLayout作为他的父View,结构如下:



版本兼容


    SwipeBackLayout绘在5.0下的版本出现透明度显示问题,为了兼容5.0以下的版本,在convertActivityToTranslucent做了一个版本兼容。在Utils类中做了转换:

    public static void convertActivityToTranslucent(Activity activity) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            convertActivityToTranslucentAfterL(activity);
        } else {
            convertActivityToTranslucentBeforeL(activity);
        }
    }
    主要内容如下:
    1、通过关键字段反射获得透明度设置的类名,方法名称,
    2、传递参数手动调用透明度设置的方法。
    3、另外在使用时候还需要在应用的主题下添加:
    <item name="android:windowIsTranslucent">true</item>


Window设置


    Activity的Window默认颜色是白色的,如果不进行设置就会遮住下面的Activity,所在加载布局之前需要设置当前窗口颜色。在SwipeBackActivityHelper类中有个onActivityCreate方法就做了如下处理:
1、设置当前的Window为透明色
2、去掉Activity根视图的背景颜色

    public void onActivityCreate() {
        //加载布局之前设置当前窗口颜色
        mActivity.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
        mActivity.getWindow().getDecorView().setBackgroundDrawable(null);
            ……………………………………………………………………………………省略部分代码…………………………………………………………………………………
    }


    以上就是SwipeBackLayout的实现原理,接下来看看SwipeBackActivity。


SwipeBackActivity

    1、SwipeBackActivity 需要实现接口SwipeBackActivityBase,重写以下三个方法:
     (1) getSwipeBackLayout:获取SwipeBackLayout控件
     (2)setSwipeBackEnable:设置是否支持手势滑动
     (3)scrollToFinishActivity:滑动结束后关闭Activity


    2、onCreate
    这里重写onActivityCreate方法,调用SwipeBackActivityHelper中的onActivityCreate方法:
    (1)加载布局前设置window的颜色
    (2)加载布局前解决滑动时候5.0之前黑屏闪动的bug兼容


    3、onPostCreate
    这里调用attachToActivity方法,在onStart之后把当前的SwipeBackLayout设置为activity的顶层view。


    4、其他设置,例如:获取SwipeBackLayout布局,设置是否支持触摸等。


用法


    1、引入库文件或者添加依赖
        compile 'me.imid.swipebacklayout.lib:library:1.1.0'


    2、继承自SwipeBackActivity





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值