美文网首页week.ioAndroid进阶Android
实现 滑动退出 Fragment + Activity 二合一

实现 滑动退出 Fragment + Activity 二合一

作者: YoKey | 来源:发表于2016-04-21 21:49 被阅读10092次

    前天有个小伙伴在我的Fragmentation库里提了个issues:

    能否在不包含侧滑菜单的时候,添加一个侧滑返回,边缘finish当前Fragment。

    今天把这项工作完成了,做成了单独的SwipeBackFragment库以及Fragmentation-SwipeBack拓展库

    特性:
    1、SwipeBackFragment , SwipeBackActivity二合一:当Activity内的Fragment数大于1时,滑动finish的是Fragment,如果小于等于1时,finish的是Activity。

    2、支持左、右、左&右滑动(未来可能会增加更多滑动区域)

    3、支持Scroll中的滑动监听

    4、帮你处理了app被系统强杀后引起的Fragment重叠的情况

    效果

    效果图

    谈谈实现

    拖拽部分大部分是靠ViewDragHelper来实现的,ViewDragHelper帮我们处理了大量Touch相关事件,以及对速度、释放后的一些逻辑监控,大大简化了我们对触摸事件的处理。(本篇不对ViewDragHelper做详细介绍,有不熟悉的小伙伴可以自行查阅相关文档)

    对Fragment以及Activiy的滑动退出,原理是一样的,都是在Activity/Fragment的视图上,添加一个父View:SwipeBackLayout,该Layout里创建ViewDragHelper,控制Activity/Fragment视图的拖拽。

    1、Activity的实现

    对于Activity的SwipeBack实现,网上有大量分析,这里我简要介绍下原理,如下图:



    我们只要保证SwipeBackLayout、DecorView和Window的背景是透明的,这样拖拽Activity的xml布局时,可以看到上个Activity的界面,把布局滑走时,再finish掉该Activity即可。

    核心代码:(致谢SwipeBackLayout这个库)

    public void attachToActivity(FragmentActivity activity) {
        ...
        ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
        ViewGroup decorChild = (ViewGroup) decor.getChildAt(0);
        decorChild.setBackgroundResource(background);
        decor.removeView(decorChild);  // 移除decorChild
        addView(decorChild);        // 添加decorChild到SwipeBackLayout(FrameLayout)
        setContentView(decorChild);
        decor.addView(this);}        // 把SwipeBackLayout添加到DecorView下
    

    2、Fragment的实现

    重点来了,Fragment的实现!
    在实现前,我先说明Fragment的几个相关知识点:

    1、Fragment的视图部分其实就是在onCreateView返回的View;

    2、同一个Activity里的多个通过add装载的Fragment,他们在视图层是叠加上去的:
    hide()并不销毁视图,仅仅让视图不可见,即View.setVisibility(GONE);
    show()让视图变为可见,即View.setVisibility(VISIBLE);

    add+show/hide的情况

    3、通过replace装载的Fragment,他们在视图层是替换的,replace()会销毁当前的Fragment视图,即回调onDestoryView,返回时,重新创建视图,即回调onCreateView;

    replace的情况

    4、不管add还是replace,Fragment对象都会被FragmentManager保存在内存中,即使app在后台因系统资源不足被强杀,FragmentManager也会为你保存Fragment,当重启app时,我们可以从FragmentManager中获取这些Fragment。

    分析:

    Fragment之间的启动无非下图中的2种:


    而这个库我并没有考虑replace的情况,因为我们的SwipeBackFragment应该是在"流式"使用的场景(FragmentA -> FragmentB ->....),而这种场景下结合上面的2、3、4条,add+show(),hide()无疑更优于replace,性能更佳、响应更快、我们app的代码逻辑更简单。

    add+hide的方式的实现

    从第1条,我们可以知道onCreateView的View就是需要放入SwipeBackLayout的子View,我们给该子View一个背景色,然后SwipeBackLayout透明,这样在拖拽时,即可看到"上个Fragment"。

    当我们拖拽时,上个Fragment A的View是GONE状态,所以我们要做的就是当判断拖拽发生时,Fragment A的View设置为VISIBLE状态,这样拖拽的时候,上个Fragment A就被完好的显示出来了。

    核心代码:

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(...);
        return attachToSwipeBack(view);
    }
    
    protected View attachToSwipeBack(View view) {
        mSwipeBackLayout.addView(view);
        mSwipeBackLayout.setFragment(this, view);
        return mSwipeBackLayout;
    }
    

    但是相比Activity,上个Activity的视图状态是VISIBLE的,而我们的上个Fragment的视图状态是GONE的,所以我们需要FragmentA.getView().setVisibility(VISIBLE),但是时机是什么时候呢?

    最好的方案是开始拖拽前的那一刻,我是在ViewDragHelper里的tryCaptureView方法处理的:

    @Override
    public boolean tryCaptureView(View child, int pointerId) {
        boolean dragEnable = mHelper.isEdgeTouched(ViewDragHelper.EDGE_LEFT);
        if (mPreFragment == null) {
            if (dragEnable && mFragment != null) {
                ...省略获取上一个Fragment代码
                mPreFragment = fragment;
                mPreFragment.getView().setVisibility(VISIBLE);
                break;
            }
        } else {
           View preView = mPreFragment.getView();
           if (preView != null && preView.getVisibility() != VISIBLE) {
                 preView.setVisibility(VISIBLE);
           }
        }
        return dragEnable;
    }
    

    通过上面代码,我们拖拽当前Fragment前的一瞬间,PreFragment的视图会被VISIBLE,同时完全不会影响onHiddenChanged方法,完美。(到这之前可能有小伙伴想到,只通过add不hide上个Fragment的思路怎么样?很明显是不行的,因为这样的话onHiddenChanged方法不会被回调,而我们使用add的方式,主要通过onHiddenChanged来作为“生命周期”来实现我们的逻辑的)

    还一种情况需要注意,当我已经开始拖拽FragmentB打算pop时,拖拽到一半我放弃了,这时FragmentA的视图已经是VISIBLE状态,我又从B进入到Fragment C,这是我们应该把A的视图GONE掉:

    SwipeBackFragment里:
    @Override
    public void onHiddenChanged(boolean hidden) {
        super.onHiddenChanged(hidden);
        if (hidden && mSwipeBackLayout != null) {
            mSwipeBackLayout.hiddenFragment();
        }
    }
    
    SwipeBackLayout里:
    public void hiddenFragment() {
        if (mPreFragment != null && mPreFragment.getView() != null) {
            mPreFragment.getView().setVisibility(GONE);
        }
    }
    

    坑点

    1、触摸事件冲突

    当我们所拖拽的边缘区域中的子View,有其他Touch事件,比如Click事件,这时我们会发现我们的拖拽失效了,这是因为,如果子View不消耗事件,那么整个Touch流程直接走onTouchEvent,在onTouchEvent的DOWN的时候就确定了CaptureView。如果子View消耗事件,那么就会先走onInterceptTouchEvent方法,判断是否可以捕获,而在这过程中会去判断另外两个回调的方法:getViewHorizontalDragRange和getViewVerticalDragRange,只有这两个方法返回大于0的值才能正常的捕获;

    并且你需要考虑当前拖拽的页面下是有2个SwipeBackLayout:当前Fragment的和Activity的,最后代码如下:

    @Override
    public int getViewHorizontalDragRange(View child) {
        if (mFragment != null) {
            return 1;
        } else {
            if (mActivity != null && mActivity.getSupportFragmentManager().getBackStackEntryCount() == 1) {
                return 1;
            }
        }
        return 0;
    }
    

    这样的话,一方面解决了事件冲突,一方面完成了Activity内Fragment数量大于1时,拖拽的是Fragment,等于1时拖拽的是Activity。

    2、动画

    我们需要在拖拽完成时,将Fragment/Activity移出屏幕,紧接着关闭,最重要的是要保证当前Fragment/Actiivty关闭和上一个Fragment/Activity进入时是无动画的!

    对于Activity这项工作很简单:Activity.overridePendingTransition(0, 0)即可。

    对于Fragment,如果本身在Fragment跳转时,就不为其设置转场动画,那就可以直接使用了;
    如果你使用了setCustomAnimations(enter,exit)或者setCustomAnimations(enter,exit,popenter,popexit),你可以这样处理:

    SwipeBackLayout里:
    {
        mPreFragment.mLocking = true;
        mFragment.mLocking =true;
        mFragment.getFragmentManager().popBackStackImmediate();
        mFragment.mLocking = false;
        mPreFragment.mLocking = false;
    }
    
    SwipeBackFragment里:
    @Override
    public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
        if(mLocking){
            return mNoAnim;
        }
        return super.onCreateAnimation(transit, enter, nextAnim);
    }
    

    3、启动新Fragment时,不要调用show()

    getSupportFragmentManager().beginTransaction()
                 .setCustomAnimations(xxx)
                 .add(xx, B)
    //             .show(B)
                 .hide(A)
                 .commit();
    

    请不要调用上述代码里的show(B)
    一方面是新add的B本身就是可见状态,不管你是show还是不调用show,都不会回调B的onHiddenChanged方法;
    另一方面,如果你调用了show,滑动返回会后出现异常行为,回到PreFragment时,PreFragment的视图会是GONE状态;如果你非要调用show的话,请按下面的方式处理:(没必要的话,还是不要调用show了,下面的代码可能会产生闪烁)

    @Overridepublic void onHiddenChanged(boolean hidden) {
        super.onHiddenChanged(hidden);
        if (!hidden && getView().getVisibility() != View.VISIBLE) {
            getView().post(new Runnable() {
                @Override
                public void run() {
                    getView().setVisibility(View.VISIBLE);
                }
            });
        }
    }
    

    致谢

    感谢ikew0ng/SwipeBackLayout,站在巨人的肩膀才有了这个库。

    最后

    我什么把这个库做成2个,一个单独使用的SwipeBackFragment和一个Fragmentation-SwipeBack拓展库呢?

    原因在于:
    SwipeBackFragment库是一个仅实现Fragment&Activity拖拽返回的基础库,适合轻度使用Fragment的小伙伴(项目属于多Activity+多Fragment,Fragment之间没有复杂的逻辑),当然你也可以随意拓展。

    Fragmentation-SwipeBack库是作为Fragmentation拓展的,这个库我这篇文章简要介绍了下:传送门
    Fragmentation主要是在项目结构为 单Activity+多Fragment,或者重度使用Fragment的多Activity+多Fragment结构时的一个Fragment帮助库,Fragment-SwipeBack是在其基础上拓展的一个库,用于实现滑动返回功能,可以用于各种项目结构。

    最后再次放上相关Github源码,目前由于个人时间问题,库还有待完善,后续会持续维护的 :)
    Fragmentation-SwipeBack
    SwipeBackFragment

    相关文章

      网友评论

      • CLOWN_c0df:博主你好,首先感谢博主的分享,博主辛苦了。然后我在使用过程中遇到了一个小问题,希望博主在百忙之中可以抽出5分钟的时间瞄一眼我的问题,为此我表示诚恳的感谢。我的问题是:整个页面都上升了,占据了状态栏的位置,嗯,为此我表示很烦恼,还望博主能够为我解答,谢谢
        BKQ_SYC:这应该是你自己对状态栏进行处理了, A界面隐藏了状态栏或者是状态栏透明例如fitsystem.. = true。 而B界面并没对状态栏处理。所以从B返回到A时 整个界面会向上移动
      • 0599beed3c0c:博主,请问使用原生回退栈的时候,怎么才能清除栈里指定的fragment呢?我看了几个pop方法,貌似都是栈顶或tag到栈顶的,不知道有没有思路帮忙理解下?另外Android9.0要封非sdk接口?我看库里有用反射的方法会不会影响呢?
      • 462191cfd704:你好,我在使用loadMultipleRootFragment加载多个fragment时,当fragment被替换时我想为显示和隐藏的fragment添加进入和推出动画,请问怎么做?
      • 贝贝beibei96:请问这个Demo在哪里可以下载 ?
      • 阿吹md:作者好,就我用你的这个库侧滑出现黑屏怎么解决啊?
      • c0e88aaec828:能不能在屏幕中间就可以滑动呢 就像QQ那样的。。。
      • 75bb53f5d932:简介明了
      • 黎清海:为什么我的Fragment滑动时不能看到下面的Fragment呢,只有整个滑动退出后才会显示出来,Activity设置了透明属性是可以的,但是Fragment不行
      • fendo:赞一个!!
      • Cliper:您好,把您的开源库干到项目中去遇到一个问题,现在的需求是就像您demo中,侧滑菜单+底部的4个tab,现在两个要一起同时用,请问怎么在一个activity去处理侧滑菜单中的item跳转的fragment和底部的tab跳转的fragment,希望能提供一个思路!!!谢谢了
        YoKey:@dreamLuo 是的,不过要看你们产品的交互需求~ 满足不了再考虑其他的栈设计
        Cliper: @YoKey 您的意思是把,tab和侧滑item当成一个两个兄弟栈,在分别从兄弟栈里面去处理底部的tab和侧滑的item吗?🤔
        YoKey:@dreamLuo 个人建议的栈设计:底部4个Tab是兄弟栈,包含这个4个Tab的父Fragment和侧滑的item跳转的fragment是兄弟栈;
        具体看需求~
      • 赛博吟游诗人:请问 我的工程是app包下的fragment,怎么用您的v4包fragment,
        YoKey:@蕉凸 你可以参照修改下,方法都是有的~
        不过话说不建议用app下的Fragment,一些安卓版本的Fragment源码实现有些不同,而v4包下的Fragment是和版本无关的~
      • openGL小白:我们的应用,也是重度使用fragment,一些完全独立的使用Activity,fragment是使用add、hide加侧滑退出的。但我们遇到了在把SwipeBackLayout代码插入时,v4包从19升级到23出了层次bug的问题,不知道你们谁有没有遇到过,二个版的v4包中的fragment的view根部层级有变化
      • kxs109:试了一下,有edittextview这些的时候,点击空白处没反应了
        swipbacklayout好像也是这样
      • a3fa6eddac9c:O(∩_∩)O哈哈~。。。直接干到项目中了 :joy:
      • BangAiN:刚看完fragment的那个坑还留言问一下滑动返回有没有好的实现呢
        YoKey:@BangAiN :smile:
      • c83a487548eb:学习啦
      • 于连林520wcf:学习了

      本文标题:实现 滑动退出 Fragment + Activity 二合一

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