1. 程式人生 > 其它 >Android:Fragment + Activity 二合一

Android:Fragment + Activity 二合一

前言

能否在不包含側滑選單的時候,新增一個側滑返回,邊緣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即可。

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);
            }
        });
    }
}


最後

我為什麼把這個庫做成2個,一個單獨使用的SwipeBackFragment和一個Fragmentation-SwipeBack拓展庫呢?

原因在於:
SwipeBackFragment庫是一個僅實現Fragment&Activity拖拽返回的基礎庫,適合輕度使用Fragment的小夥伴(專案屬於多Activity+多Fragment,Fragment之間沒有複雜的邏輯),當然你也可以隨意拓展。

Fragmentation主要是在專案結構為 單Activity+多Fragment,或者重度使用Fragment的多Activity+多Fragment結構時的一個Fragment幫助庫,Fragment-SwipeBack是在其基礎上拓展的一個庫,用於實現滑動返回功能,可以用於各種專案結構。

相關教程

Android基礎系列教程:
Android基礎課程U-小結_嗶哩嗶哩_bilibili
Android基礎課程UI-佈局_嗶哩嗶哩_bilibili
Android基礎課程UI-控制元件_嗶哩嗶哩_bilibili
Android基礎課程UI-動畫_嗶哩嗶哩_bilibili
Android基礎課程-activity的使用_嗶哩嗶哩_bilibili
Android基礎課程-Fragment使用方法_嗶哩嗶哩_bilibili
Android基礎課程-熱修復/熱更新技術原理_嗶哩嗶哩_bilibili

本文轉自https://juejin.cn/post/7038445433965772813,如有侵權,請聯絡刪除。