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

前天有个小伙伴在我的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

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

推荐阅读更多精彩内容