SwipeBackLayout源码分析

作者: SkyKai | 来源:发表于2016-03-14 09:11 被阅读2411次

    我每周会写一篇源代码分析的文章,以后也可能会有其他主题.
    如果你喜欢我写的文章的话,欢迎关注我的新浪微博@达达达达sky
    地址: http://weibo.com/u/2030683111
    每周我会第一时间在微博分享我写的文章,也会积极转发更多有用的知识给大家.谢谢关注_,说不定什么时候会有福利哈.


    项目地址:SwipeBackLayout,本文分析版本: e4ddae6d

    1.简介

    SwipeBackLayout是一个在Android平台上实现了Activity滑动返回的库.实现了左,右,上,下四种手势返回的功能,在ios里滑动返回是系统自带可以配置的功能,而在我们Android上并没有系统级别的提供,但是主流应用比如微信就带有滑动返回功能,而且滑动返回是一个非常容易培养用户使用习惯的操作,用惯了滑动返回再用没有滑动返回的应用简直不能好好用了。。我自己正是滑动返回这个手势的重度使用者,我非常喜欢用滑动返回,所以在我开发过的应用里我都尽量集成了滑动返回这个功能,当然我相信是有很多人有滑动返回的使用习惯的。SwipeBackLayout应该算是使用范围最广的滑动返回的库了,我一直也是这个库的使用者,今天我们就来分析一下这个库是如何实现的:

    2.使用方法

    1.设置需要滑动返回Activity的Theme

    首先需要在需要滑动返回的ActivityTheme里添加:

    <item name="android:windowIsTranslucent">true</item>
    

    2.将Activity继承SwipeBackActivity

    然后将我们的Activity继承SwipeBackActivity就集成完毕,我们的Activity就默认带有左划返回的功能了,当然我们也可以在onCreate()方法的super.onCreate(savedInstanceState);执行之后,调用下面的方法做一些自定义设置:

    
    //是否允许滑动返回
    setSwipeBackEnable(false);
    //滑动并关闭activity
    scrollToFinishActivity();
    //获得SwipeBackLayout对象
    getSwipeBackLayout();
    
    

    在获取到SwipeBackLayout对象之后,可以设定从哪个方向可以滑动mSwipeBackLayout.setEdgeTrackingEnabled(edgeFlag);可以使用setScrimColor()来设置滑动返回的背景色,mSwipeBackLayout.setEdgeSize(200);来设置滑动触发的范围等等。

    3.类关系图

    swipeback_class_relation.jpg

    SwipeBackLayout的类关系图非常的清晰简单,SwipeBackActivity继承自Activity并且实现了SwipeBackActivityBase接口,SwipeBackActivity,SwipeBackActivityHelperSwipeBackLayout相互引用,类图上来看还是比较简单的,下面我们来看具体实现:

    4.源码分析

    一句话概括SwipeBackLayout的实现原理就是:通过在DecorView和其包含的子View之间添加一个ViewGroup也就是SwipeBackLayout,通过在SwipeBackLayout里处理触摸事件与位移来实现滑动返回的效果。

    1.SwipeBackActivityBase的实现

    我们以前说过阅读一个框架的时候,先从它定义的一些接口开始,如果是小项目其实也没有太多的规定,因为代码量本身就不多,所以也可以整体来看,不过我们还是先来看看SwipeBackActivityBase接口是怎么定义的:

    public interface SwipeBackActivityBase {
        
        //得到SwipeBackLayout对象
        public abstract SwipeBackLayout getSwipeBackLayout();
    
        //设置是否可以滑动返回
        public abstract void setSwipeBackEnable(boolean enable);
    
        //自动滑动返回并关闭Activity
        public abstract void scrollToFinishActivity();
    }
    

    2.SwipeBackActivity,SwipeBackActivityHelper的实现

    以上就是SwipeBackActivityBase接口定义的内容,SwipeBackActivity类实现了这个接口,也就是我们要继承的这个类,具体实现如下:

    public class SwipeBackActivity extends AppCompatActivity implements SwipeBackActivityBase {
        private SwipeBackActivityHelper mHelper;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            //初始化mHelper
            mHelper = new SwipeBackActivityHelper(this);
            mHelper.onActivityCreate();
        }
    
        @Override
        protected void onPostCreate(Bundle savedInstanceState) {
            super.onPostCreate(savedInstanceState);
            mHelper.onPostCreate();
        }
    
        @Override
        public SwipeBackLayout getSwipeBackLayout() {
            return mHelper.getSwipeBackLayout();
        }
    
        @Override
        public void setSwipeBackEnable(boolean enable) {
            getSwipeBackLayout().setEnableGesture(enable);
        }
    
        @Override
        public void scrollToFinishActivity() {
            Utils.convertActivityToTranslucent(this);
            getSwipeBackLayout().scrollToFinishActivity();
        }
    }
    
    

    可以看到在onCreate()里创建了一个SwipeBackActivityHelper对象,而在getSwipeBackLayout()方法里通过mHelper得到了对应的SwipeBackLayout对象,所以mHelper应该是负责创建SwipeBackLayout并将SwipeBackLayout添加到Activity里的。
    mHelperonActivityCreate()方法里创建了SwipeBackLayout对象,并在onPostCreate()方法里将SwipeBackLayout添加到Activity里,逻辑很简单,这里就不贴代码了,最终是调用了SwipeBackLayoutattachToActivity()方法:

    
        public void attachToActivity(Activity activity) {
            //获得activity对象
            mActivity = activity;
            TypedArray a = activity.getTheme().obtainStyledAttributes(new int[]{
                    android.R.attr.windowBackground
            });
            //得到窗口背景
            int background = a.getResourceId(0, 0);
            a.recycle();
            //得到DecorView对象,并先将decorChild移除并添加到
            //SwipeBackLayout里,再添加进DecorView
            ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
            ViewGroup decorChild = (ViewGroup) decor.getChildAt(0);
            decorChild.setBackgroundResource(background);
            decor.removeView(decorChild);
            addView(decorChild);
            setContentView(decorChild);
            decor.addView(this);
        }
    
    

    这就是整个添加过程,至此我们的Activity里就包含了SwipeBackLayout了,下面我们就来看看SwipeBackLayout的具体实现。

    3.SwipeBackLayout的实现

    现在就只剩下一个SwipeBackLayout类了,它是继承自FrameLayout的,我们从它的构造方法开始看:

        public SwipeBackLayout(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs);
            mDragHelper = ViewDragHelper.create(this, new ViewDragCallback());
    
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SwipeBackLayout, defStyle,
                    R.style.SwipeBackLayout);
    
            int edgeSize = a.getDimensionPixelSize(R.styleable.SwipeBackLayout_edge_size, -1);
            ...
            a.recycle();
            final float density = getResources().getDisplayMetrics().density;
            final float minVel = MIN_FLING_VELOCITY * density;
            mDragHelper.setMinVelocity(minVel);
            mDragHelper.setMaxVelocity(minVel * 2f);
        }
    

    除了从AttributeSet获取一些预先设定的属性外,最重要的是初始化了ViewDragHelper对象,关于ViewDragHelper:

    ViewDragHelper是谷歌在2013年的I/O大会上推出的一个用于View拖拽操作的帮助类,借助于该类谷歌同时推出了两个用于侧滑的布局SlidingPaneLayout和DrawerLayout,现在市场上的很多带有侧滑菜单的应用都是基于这两种布局。
    
    ViewDragHelper极大的简化了View的拖拽操作,在它没有出现之前很多侧滑都是采用的第三方库,为了实现侧滑效果需要对touch事件进行自定义拦截和处理,代码量大,实现逻辑也需要很大的耐心才能看懂。如果每个开发人员都从这么原始的步奏开始做起,那对于安卓生态是相当不利的。所以说ViewDragHelper等的出现反映了安卓开发框架已经开始向成熟的方向迈进。
    

    我们下一篇文章会具体讨论ViewDragHelper的使用以及原理,这篇文章就不过多讨论了,有需要的可以先看这篇文章ViewDragHelper解析,所以在这里ViewDragHelper的触摸事件就直接交给ViewDragHelperSwipeBackLayoutViewDragCallback类来处理了,以下为SwipeBackLayout的处理触摸事件的方法:

        @Override
        public boolean onInterceptTouchEvent(MotionEvent event) {
            if (!mEnable) {
                return false;
            }
            try {
                return mDragHelper.shouldInterceptTouchEvent(event);
            } catch (ArrayIndexOutOfBoundsException e) {
                // FIXME: handle exception
                // issues #9
                return false;
            }
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            if (!mEnable) {
                return false;
            }
            mDragHelper.processTouchEvent(event);
            return true;
        }
    

    可以看到是完全委托ViewDragHelper来处理,我们可以看到SwipeBackLayout中的ViewDragHelper并不是直接引用的support包中的ViewDragHelper而是将代码拷贝出来,是因为需要添加一些supportViewDragHelper并不存在的方法,例如mDragHelper.setEdgeSize(size);,mDragHelper.setMaxVelocity(minVel * 2f);等,这篇文章我们就讨论更多关于ViewDragHelper的内容了,我们下篇会详细解释(其实是我偷懒=。=,想分两篇写。。不过ViewDragHelper还是确实值得一写的)。

    这里我们需要知道当每次拖动发生时都会回调ViewDragCallbackonViewPositionChanged()方法,在这个方法里会调用invalidate();方法,最终会调用SwipeBackLayoutdrawChild()方法:

        @Override
        protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
            Log.d(TAG, "drawChild");
            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;
        }
    

    可以看到阴影和遮罩就是在这个方法里绘制的了。除去ViewDragHelper的一些逻辑,SwipeBackLayout的实现还是非常好理解的,下周我会继续写关于ViewDragHelper的文章,到时候我们继续分析。

    5.存在的问题

    1.部分Android版本不兼容:

    在实际使用的时候发现当一个Activity栈内的所有ActivityTheme里都添加了<item name="android:windowIsTranslucent">true</item>参数时,在某些Android 4.4.x的手机上或者4.4.x的MIUI上,滑动返回的时候不会看到前面一个Activity而会把桌面显示出来,这应该是这些系统的bug.所以为了避免这些问题,我们需要在最底层的ActivityTheme里设置<item name="android:windowIsTranslucent">false</item>来避免这个问题,通常都是我们的MainActivity而且一般这个Activity我们并不需要左划返回的功能,所以这个问题也算是找到了解决办法。

    2.性能问题:

    由于被设置了<item name="android:windowIsTranslucent">true</item>Activity无法进入onStop()生命周期,所以导致ActivityWindow无法回收,所以在多个Activity叠加时会出现明显的卡顿现象,目前并没有特别好的解决办法。

    6.个人评价

    虽然有上面的这些问题,但是我了解的目前开源社区中好像也并没有更好的滑动返回库了,但是一定有更好的实现方式可以解决这些问题,例如微信。虽然并不知道微信滑动返回的具体技术实现方式,但是一定是避免或者解决了上面的一些问题的。总体来说,如果我们的项目里需要滑动返回,SwipeBackLayout还是值得推荐的.

    相关文章

      网友评论

      • 小小亭长:windowIsTranslucent会影响到activity的生命周期,现在有办法了吗?现在来看我的想法和你的文章中写的差不多。但是影响到activity的生命周期是不太能选择的。。。
      • 大姨夫斯基:微信里的实现方式估计也是这种 因为我在微信的聊天界面 收起键盘的时候 会闪首页的tab 。而我们的app 也是由于这个原因 导致在收起键盘的时候 会闪现上一个页面的画面 这个画面的高度就是键盘的高度
        SkyKai:@大姨夫斯基 微信应该也是类似方式。应该是比较好的控制了activity的生命周期才解决的性能问题的
        SkyKai:@大姨夫斯基 但是这种方法会有性能问题。而微信却没有。我试着自己控制生命周期但是并没有作用。目前还没有深入研究如何才能解决性能问题
      • 大姨夫斯基:你在达达?
      • 08e018ace9d5:你好,看了文章后,我在使用的时候有这种错误:Caused by: java.lang.ClassCastException: android.support.v7.widget.AppCompatTextView cannot be cast to me.imid.swipebacklayout.lib.SwipeBackLayout。你遇到过吗?
      • 小王泽哥:滑动返回的实现方式有很多种,期待大神写一个关于fragment 和其他实现方式的研究。
      • oaosj:我一直在纠结。android的微信有滑动返回么??????????哪个版本
        SkyKai: @Lukey丶宽 几乎所有界面。。
        Luke_单车:@达达达达sky 我也想问下 微信哪个地方是滑动返回的,iOS是有的
        SkyKai:@oaosj 一直都有啊。。好像很久了
      • 布鲁马:其实我只是想问为什么没有收藏文章的选项!
        SkyKai:@布鲁马 右上角第三个钮
      • Ziv_xiao:目前项目用的就是这个,是挺方便的,不过带来的问题也不少。。。
      • 無尘雨:SwipeBackLayout目前网上的教程都是基于activity,但是大部分应用现在的构架都是大面积的Fragment,如何基于Fragment的侧滑也是值得研究的。我也试了整整一天才搞定Fragment的问题
        SkyKai:@無尘雨 我没开发过fragment的滑动返回,不过fragment滑动返回也应该是可以使用文中的原理的。
      • 80a8a5d2edfa:正规的滑动返回应该是使用Fragment而不是Activity
        SkyKai:@80a8a5d2edfa 要根据具体的业务场景来做了。
      • 梦华芳秋:我是从stormzhang那里听说您的,大神,我也跟着你的简书学技术,支持你!
        SkyKai:@梦华芳秋 谢谢支持,我每周都会分享文章的!一起加油

      本文标题:SwipeBackLayout源码分析

      本文链接:https://www.haomeiwen.com/subject/ygdflttx.html