前天有個小伙伴在我的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);低滩;
3召夹、通過replace裝載的Fragment,他們在視圖層是替換的恕沫,replace()會銷毀當前的Fragment視圖监憎,即回調onDestoryView,返回時婶溯,重新創(chuàng)建視圖鲸阔,即回調onCreateView偷霉;
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