實現(xiàn) 滑動退出 Fragment + Activity 二合一

前天有個小伙伴在我的Fragmentation庫里提了個issues:

能否在不包含側滑菜單的時候伴逸,添加一個側滑返回焙贷,邊緣finish當前Fragment泼疑。

今天把這項工作完成了移袍,做成了單獨的SwipeBackFragment庫以及Fragmentation-SwipeBack拓展庫

特性:
1、SwipeBackFragment , SwipeBackActivity二合一:當Activity內(nèi)的Fragment數(shù)大于1時,滑動finish的是Fragment,如果小于等于1時炕檩,finish的是Activity斗蒋。

2捌斧、支持左、右泉沾、左&右滑動(未來可能會增加更多滑動區(qū)域)

3捞蚂、支持Scroll中的滑動監(jiān)聽

4、幫你處理了app被系統(tǒng)強殺后引起的Fragment重疊的情況

效果

效果圖

談談實現(xiàn)

拖拽部分大部分是靠ViewDragHelper來實現(xiàn)的跷究,ViewDragHelper幫我們處理了大量Touch相關事件姓迅,以及對速度、釋放后的一些邏輯監(jiān)控,大大簡化了我們對觸摸事件的處理丁存。(本篇不對ViewDragHelper做詳細介紹肩杈,有不熟悉的小伙伴可以自行查閱相關文檔)

對Fragment以及Activiy的滑動退出,原理是一樣的解寝,都是在Activity/Fragment的視圖上扩然,添加一個父View:SwipeBackLayout,該Layout里創(chuàng)建ViewDragHelper聋伦,控制Activity/Fragment視圖的拖拽夫偶。

1、Activity的實現(xiàn)

對于Activity的SwipeBack實現(xiàn)觉增,網(wǎng)上有大量分析兵拢,這里我簡要介紹下原理,如下圖:



我們只要保證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的實現(xiàn)

重點來了衙解,F(xiàn)ragment的實現(xiàn)!
在實現(xiàn)前焰枢,我先說明Fragment的幾個相關知識點:

1蚓峦、Fragment的視圖部分其實就是在onCreateView返回的View;

2济锄、同一個Activity里的多個通過add裝載的Fragment暑椰,他們在視圖層是疊加上去的:
hide()并不銷毀視圖,僅僅讓視圖不可見荐绝,即View.setVisibility(GONE);一汽,
show()讓視圖變?yōu)榭梢姡?code>View.setVisibility(VISIBLE);低滩;

add+show/hide的情況

3召夹、通過replace裝載的Fragment,他們在視圖層是替換的恕沫,replace()會銷毀當前的Fragment視圖监憎,即回調onDestoryView,返回時婶溯,重新創(chuàng)建視圖鲸阔,即回調onCreateView偷霉;

replace的情況

4、不管add還是replace褐筛,F(xiàn)ragment對象都會被FragmentManager保存在內(nèi)存中类少,即使app在后臺因系統(tǒng)資源不足被強殺,F(xiàn)ragmentManager也會為你保存Fragment渔扎,當重啟app時瞒滴,我們可以從FragmentManager中獲取這些Fragment。

分析:

Fragment之間的啟動無非下圖中的2種:


而這個庫我并沒有考慮replace的情況赞警,因為我們的SwipeBackFragment應該是在"流式"使用的場景(FragmentA -> FragmentB ->....)妓忍,而這種場景下結合上面的2、3愧旦、4條世剖,add+show(),hide()無疑更優(yōu)于replace,性能更佳笤虫、響應更快旁瘫、我們app的代碼邏輯更簡單。

add+hide的方式的實現(xiàn)

從第1條琼蚯,我們可以知道onCreateView的View就是需要放入SwipeBackLayout的子View酬凳,我們給該子View一個背景色,然后SwipeBackLayout透明遭庶,這樣在拖拽時宁仔,即可看到"上個Fragment"。

當我們拖拽時峦睡,上個Fragment A的View是GONE狀態(tài)翎苫,所以我們要做的就是當判斷拖拽發(fā)生時,F(xiàn)ragment A的View設置為VISIBLE狀態(tài)榨了,這樣拖拽的時候煎谍,上個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的視圖狀態(tài)是VISIBLE的呐粘,而我們的上個Fragment的視圖狀態(tài)是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來作為“生命周期”來實現(xiàn)我們的邏輯的)

還一種情況需要注意脯倚,當我已經(jīng)開始拖拽FragmentB打算pop時,拖拽到一半我放棄了嵌屎,這時FragmentA的視圖已經(jīng)是VISIBLE狀態(tài)推正,我又從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宝惰、觸摸事件沖突

當我們所拖拽的邊緣區(qū)域中的子View植榕,有其他Touch事件,比如Click事件尼夺,這時我們會發(fā)現(xiàn)我們的拖拽失效了尊残,這是因為,如果子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內(nèi)Fragment數(shù)量大于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本身就是可見狀態(tài),不管你是show還是不調用show吨凑,都不會回調B的onHiddenChanged方法捍歪;
另一方面,如果你調用了show鸵钝,滑動返回會后出現(xiàn)異常行為糙臼,回到PreFragment時,PreFragment的視圖會是GONE狀態(tài)恩商;如果你非要調用show的話弓摘,請按下面的方式處理:(沒必要的話,還是不要調用show了痕届,下面的代碼可能會產(chǎn)生閃爍)

@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庫是一個僅實現(xiàn)Fragment&Activity拖拽返回的基礎庫,適合輕度使用Fragment的小伙伴(項目屬于多Activity+多Fragment嚷炉,F(xiàn)ragment之間沒有復雜的邏輯)渊啰,當然你也可以隨意拓展。

Fragmentation-SwipeBack庫是作為Fragmentation拓展的申屹,這個庫我這篇文章簡要介紹了下:傳送門
Fragmentation主要是在項目結構為 單Activity+多Fragment绘证,或者重度使用Fragment的多Activity+多Fragment結構時的一個Fragment幫助庫,F(xiàn)ragment-SwipeBack是在其基礎上拓展的一個庫哗讥,用于實現(xiàn)滑動返回功能嚷那,可以用于各種項目結構。

最后再次放上相關Github源碼杆煞,目前由于個人時間問題魏宽,庫還有待完善,后續(xù)會持續(xù)維護的 :)
Fragmentation-SwipeBack
SwipeBackFragment

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末决乎,一起剝皮案震驚了整個濱河市队询,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌构诚,老刑警劉巖蚌斩,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異范嘱,居然都是意外死亡送膳,警方通過查閱死者的電腦和手機员魏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肠缨,“玉大人逆趋,你說我怎么就攤上這事盏阶∩罐龋” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵名斟,是天一觀的道長脑慧。 經(jīng)常有香客問我,道長砰盐,這世上最難降的妖魔是什么闷袒? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮岩梳,結果婚禮上囊骤,老公的妹妹穿的比我還像新娘。我一直安慰自己冀值,他們只是感情好也物,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著列疗,像睡著了一般滑蚯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上抵栈,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天告材,我揣著相機與錄音,去河邊找鬼古劲。 笑死斥赋,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的产艾。 我是一名探鬼主播灿渴,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼胰舆!你這毒婦竟也來了骚露?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤缚窿,失蹤者是張志新(化名)和其女友劉穎棘幸,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體倦零,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡误续,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年吨悍,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蹋嵌。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡育瓜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出栽烂,到底是詐尸還是另有隱情躏仇,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布腺办,位于F島的核電站焰手,受9級特大地震影響,放射性物質發(fā)生泄漏怀喉。R本人自食惡果不足惜书妻,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望躬拢。 院中可真熱鬧躲履,春花似錦、人聲如沸聊闯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽馅袁。三九已至域慷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間汗销,已是汗流浹背犹褒。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留弛针,地道東北人叠骑。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像削茁,于是被迫代替她去往敵國和親宙枷。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,071評論 25 707
  • 內(nèi)容抽屜菜單ListViewWebViewSwitchButton按鈕點贊按鈕進度條TabLayout圖標下拉刷新...
    皇小弟閱讀 46,754評論 22 665
  • 部分內(nèi)容來源于別人的總結,如有冒犯侵權,請告知! 郵箱:simoncqhy@163.com.謝謝!我只想做一個記錄...
    愛思考的程序員閱讀 10,390評論 13 21
  • 今天是立夏茧跋,我們這里有吃烏米飯的習慣慰丛,愛人從昨晚就開始忙開了,他先把從菜場買來的烏葉洗干凈放入榨汁機里打出汁水瘾杭,把...
    五月的荷閱讀 574評論 22 13
  • 所謂的現(xiàn)實就是無數(shù)次偶然的重合诅病,總有一些巧合,讓我們欣喜。 你并沒有任何期許贤笆,但它卻真實的發(fā)生了蝇棉。在一個小餐...
    千姍暮雪閱讀 226評論 0 4