天天看點

FragmentManager的back stack是如何影響使用者互動的

Fragment

既可以在布局檔案中進行靜态配置建立,也可以使用

FragmentManager

進行動态建立.在動态建立時, 可以通過調用

FragmentTransaction

addToBackStack

接口, 指定該FragmentTransaction對象添加到FragmentManager中的

back stack

中,這将對使用者的操作(按back鍵)産生影響.本文檔通過一個demo程式說明其中的一些細節.

Demo程式說明

包括一個Activity和四個Fragment, Activity中動态建立Fragment和銷毀Fragment的操作.四個Fragment共用一個布局檔案, 通過一個文本區域顯示目前的Fragment名稱.Activity類代碼實作如下:

public class MainActivity extends Activity implements View.OnClickListener {
    private static final String TAG = MainActivity.class.getSimpleName();

    private int mAddedNum;
    private CheckBox mAddToBackStack;
    private CheckBox mPopFlag;
    private EditText mBackStackName;
    private FragmentManager mFragmentManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_demo);

        mAddToBackStack = findViewById(R.id.cb_add_to_back_stack);
        mPopFlag = findViewById(R.id.cb_include_flag);
        mBackStackName = findViewById(R.id.et_back_stack_name);

        findViewById(R.id.bt_add_fragment).setOnClickListener(this);
        findViewById(R.id.bt_pop_fragment).setOnClickListener(this);
        findViewById(R.id.bt_pop_fragment_with_name).setOnClickListener(this);

        mFragmentManager = getFragmentManager();
    }

    @Override
    public void onClick(View v) {
        final int id = v.getId();
        switch (id) {
            case R.id.bt_add_fragment:
                addFragment();
                break;

            case R.id.bt_pop_fragment:
                popBackStack();
                break;

            case R.id.bt_pop_fragment_with_name:
                popBackStackByNameOrId();
                break;
        }
    }

    private boolean needAddToBackStack() {
        return mAddToBackStack.isChecked();
    }

    private int getPopFlag() {
        if (mPopFlag.isChecked()) return FragmentManager.POP_BACK_STACK_INCLUSIVE;
        return 0;
    }

    private void addFragment() {
        Fragment fragment;
        String tag;
        switch (mAddedNum) {
            case 0:
                fragment = new Fragment1();
                tag = "Fragment1";
                break;

            case 1:
                fragment = new Fragment2();
                tag = "Fragment2";
                break;

            case 2:
                fragment = new Fragment3();
                tag = "Fragment3";
                break;
            case 3:
                fragment = new Fragment4();
                tag = "Fragment4";
                break;

            default:
                mAddedNum = 0;
                fragment = new Fragment1();
                tag = "Fragment1";
                break;
        }

        mAddedNum++;
        addFragment(R.id.fragment_container, fragment, tag);
    }

    private void addFragment(@IdRes int resId, @NonNull Fragment fragment, @NonNull String tag) {
        final FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();
        fragmentTransaction.add(resId, fragment, tag);
        if (needAddToBackStack()) fragmentTransaction.addToBackStack(tag);
        final int commitId = fragmentTransaction.commit();
        Log.d(TAG, "Commit id:" + commitId);
    }

    private void popBackStack() {
        mFragmentManager.popBackStack();
    }

    private void popBackStackByNameOrId() {
        final int popFlag = getPopFlag();
        final String text = mBackStackName.getText().toString();
        if (text.startsWith("id:")) {
            final int id = Integer.valueOf(text.substring(3)).intValue();
            mFragmentManager.popBackStack(id, popFlag);
        } else if (text.startsWith("name:")) {
            final String name = text.substring(5);
            mFragmentManager.popBackStack(name, popFlag);
        }
    }
}
           

四個Fragment的代碼實作如下:

public class Fragment1 extends BaseFragment {
    private static final String TAG = Fragment1.class.getSimpleName();

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return createView(inflater, container, TAG);
    }
}

public class Fragment2 extends BaseFragment {
    private static final String TAG = Fragment2.class.getSimpleName();

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return createView(inflater, container, TAG);
    }
}

public class Fragment3 extends BaseFragment {
    private static final String TAG = Fragment3.class.getSimpleName();

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return createView(inflater, container, TAG);
    }
}

public class Fragment4 extends BaseFragment {
    private static final String TAG = Fragment4.class.getSimpleName();

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return createView(inflater, container, TAG);
    }
}

public class BaseFragment extends Fragment {
    protected View createView(LayoutInflater inflater, ViewGroup container, String text) {
        final View view = inflater.inflate(R.layout.fragment_demo, container, false);
        ((TextView) view.findViewById(R.id.fragment_id)).setText(text);
        return view;
    }
}
           

Activity的布局檔案為:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="10dp">

    <CheckBox
        android:id="@+id/cb_add_to_back_stack"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:checked="false"
        android:text="Add to back stack"
        android:textAllCaps="false" />

    <CheckBox
        android:id="@+id/cb_include_flag"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:checked="false"
        android:text="Pop back stack record with include flag"
        android:textAllCaps="false" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Pop back stack record with name or ID:" />

    <EditText
        android:id="@+id/et_back_stack_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="id:0" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp"
        android:orientation="horizontal">

        <Button
            android:id="@+id/bt_add_fragment"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="add"
            android:textAllCaps="false" />

        <Button
            android:id="@+id/bt_pop_fragment"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="pop"
            android:textAllCaps="false" />

        <Button
            android:id="@+id/bt_pop_fragment_with_name"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="popBy"
            android:textAllCaps="false" />

    </LinearLayout>

    <FrameLayout
        android:id="@+id/fragment_container"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:layout_marginTop="10dp"
        android:background="#BFBEBE">
    </FrameLayout>

</LinearLayout>
           

Fragment共用的布局檔案為:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#5d8989">

    <TextView
        android:id="@+id/fragment_id"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        tools:text="Dynamic fragment"
        android:textSize="20sp" />

</RelativeLayout>
           

Activity啟動的初始界面為:

FragmentManager的back stack是如何影響使用者互動的

Fragment Manager Back Stack對使用者back按鍵的響應

當使用者按下

back

鍵時,

Activity

的預設執行為(Activity.java):

/**
 * Called when the activity has detected the user's press of the back
 * key.  The default implementation simply finishes the current activity,
 * but you can override this to do whatever you want.
 */
public void onBackPressed() {
    if (mActionBar != null && mActionBar.collapseActionView()) {
        return;
    }

    FragmentManager fragmentManager = mFragments.getFragmentManager();

    if (fragmentManager.isStateSaved() || !fragmentManager.popBackStackImmediate()) {
        finishAfterTransition();
    }
}
           

也就是說, 如果使用者執行back按鍵操作,

FragmentManager

back stack

中的記錄進行

pop

處理,如果棧中存在記錄,函數

popBackStackImmediate

将傳回

true

, 那麼通常情況下, Activity将不會調用

finishAfterTransition

進行銷毀.

場景一

在不選中複選框

Add to back stack

的前提下,點選兩次

add

按鈕, 根據代碼邏輯, 将動态建立

Fragment1

Fragment2

. 如果使用者點選一次

back

鍵, 那麼

MainActivity

将銷毀,顯示桌面.

場景二

在選中複選框

Add to back stack

的前提下,點選兩次

add

按鈕, 根據代碼邏輯, 将動态建立

Fragment1

Fragment2

. 顯示結果為:

FragmentManager的back stack是如何影響使用者互動的

如果使用者點選一次

back

鍵,那麼FragmentManager将對back stack進行pop處理, Fragment2出棧,隻有Fragment1在棧中, 是以結果顯示為:

FragmentManager的back stack是如何影響使用者互動的

如果這時候使用者再次點選一次back按鍵,因為Fragment1還在back stack中,是以Fragment1出棧,函數

popBackStackImmediate

傳回值仍熱為

true

, MainActivity仍然不調用函數

finishAfterTransition

進行銷毀, 顯示結果為MainActivity的初始界面:

FragmentManager的back stack是如何影響使用者互動的

如果使用者再次點選一次back按鍵,因為此時FragmentManager中的back stack中已經沒有記錄,函數

popBackStackImmediate

将傳回

false

, 導緻調用

finishAfterTransition

銷毀MainActivity,顯示桌面.

通過這兩個場景, 基本說明了FragmentManager的back stack對使用者的back按鍵的響應情況.點選back按鍵時, 會對back stack中的記錄進行pop操作, 但是,新的棧頂的Fragment是否顯示,取決于目前顯示的Fragment與新的棧頂Fragment的添加順序.

場景三

在選中複選框

Add to back stack

的前提下,點選兩次

add

按鈕, 然後取消複選框

Add to back stack

, 再點選一次

add

按鈕.FragmentManager中依次添加了

Fragment1

,

Fragment2

Fragment3

, 前兩個Fragment同時添加到了back stack, Fragment3沒有添加到back stack, 最終顯示的是Fragment3的界面.

FragmentManager的back stack是如何影響使用者互動的

如果使用者點選一次

back

按鍵,Fragment2将從棧中退出,目前顯示界面不變,仍然為Fragment3.再次點選一次back按鍵,Fragment1将從棧中退出,目前顯示界面不變,仍然為Fragment3, 再次點選一次back按鍵,因為棧中已經沒有記錄,MainActivity将銷毀, 傳回桌面.

Commit索引以及pop指定的記錄

FragmentManager動态建立Fragment時, 如果該Fragment同時添加到back stack, 則

commit

函數的傳回值為該

BackStackRecord

(back stack中的每個記錄用該類描述)的索引, 如果該Fragment沒有添加到back stack, commit函數傳回值為

-1

.BackStackRecord的索引的一個作用是, 我們可以從back stack中pop出該索引對應的記錄, 需要注意的是,根據棧的屬性, 棧中該記錄以上的記錄也将被pop操作.BackStackRecord的索引是從0開始配置設定的.

FragmentManager

提供了接口

popBackStack(int id, int flags)

來pop指定索引的記錄.FragmentManager還提供了接口

popBackStackImmediate(String name, int flags)

根據記錄的名稱進行pop操作, 二者内在邏輯幾乎完全相同.當調用這兩個接口時, 其第二個輸入參數

flags

的含義是,如果flags為

, 該索引對應的記錄不會從back stack中pop出去,隻是将該記錄上面的記錄pop出去.如果

flags

POP_BACK_STACK_INCLUSIVE

,則該索引對應的記錄也将被pop出去.

場景四

在選中複選框

Add to back stack

的前提下,點選四次

add

按鈕,即将

Fragment1

,

Fragment2

,

Fragment3

Fragment4

添加到了back stack中, 并且目前顯示為

Fragment4

.FragmentManager的back stack中的情況為:

FragmentManager的back stack是如何影響使用者互動的

如果通過FragmentManager調用

mFragmentManager.popBackStack(1, 0)

,那麼棧中的操作結果将是:

FragmentManager的back stack是如何影響使用者互動的

如果通過FragmentManager調用

mFragmentManager.popBackStack(1, FragmentManager.POP_BACK_STACK_INCLUSIVE),

那麼棧中的操作結果将是:

FragmentManager的back stack是如何影響使用者互動的

FragmentManager的接口

popBackStack()

将把棧頂的記錄pop出去, 而接口

popBackStack(String name, int flags)

popBackStack(int id, int flags)

将有可能從棧中pop出多個記錄.對于判斷哪些記錄需要pop操作的函數為(FragmentManager.java):

@SuppressWarnings("unused")
boolean popBackStackState(ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop,
        String name, int id, int flags) {
    if (mBackStack == null) {
        return false;
    }
    if (name == null && id < 0 && (flags & POP_BACK_STACK_INCLUSIVE) == 0) {
        int last = mBackStack.size() - 1;
        if (last < 0) {
            return false;
        }
        records.add(mBackStack.remove(last));
        isRecordPop.add(true);
    } else {
        int index = -1;
        if (name != null || id >= 0) {
            // If a name or ID is specified, look for that place in
            // the stack.
            index = mBackStack.size()-1;
            while (index >= 0) {
                BackStackRecord bss = mBackStack.get(index);
                if (name != null && name.equals(bss.getName())) {
                    break;
                }
                if (id >= 0 && id == bss.mIndex) {
                    break;
                }
                index--;
            }
            if (index < 0) {
                return false;
            }
            if ((flags&POP_BACK_STACK_INCLUSIVE) != 0) {
                index--;
                // Consume all following entries that match.
                while (index >= 0) {
                    BackStackRecord bss = mBackStack.get(index);
                    if ((name != null && name.equals(bss.getName()))
                            || (id >= 0 && id == bss.mIndex)) {
                        index--;
                        continue;
                    }
                    break;
                }
            }
        }
        if (index == mBackStack.size()-1) {
            return false;
        }
        for (int i = mBackStack.size() - 1; i > index; i--) {
            records.add(mBackStack.remove(i));
            isRecordPop.add(true);
        }
    }
    return true;
}
           

該函數執行完畢後,輸出參數

records

中将儲存需要pop出的

BackStackRecord

對象. 如果沒有指定

name

或者

id

時, 從back stack棧頂pop出一個記錄, 否則根據

name

或者

id

,并結合

flags

找出需要pop的記錄.

BackStackRecord的索引配置設定

每個添加到back stack的

BackStackRecord

對象都要配置設定索引.如果一直是向back stack添加BackStackRecord對象,其是以将從0開始連續增加進行配置設定. 如果back stack發生了pop操作,則pop出的記錄的索引将會被回收,用作下一次的配置設定.

FragmentManager

類中的如下兩個成員變量控制BackStackRecord的索引配置設定(FragmentManager.java):

// Must be accessed while locked.
ArrayList<BackStackRecord> mBackStackIndices;
ArrayList<Integer> mAvailBackStackIndices;
           

配置設定索引時(FragmentManager.java):

public int allocBackStackIndex(BackStackRecord bse) {
    synchronized (this) {
        if (mAvailBackStackIndices == null || mAvailBackStackIndices.size() <= 0) {
            if (mBackStackIndices == null) {
                mBackStackIndices = new ArrayList<BackStackRecord>();
            }
            int index = mBackStackIndices.size();
            if (DEBUG) Log.v(TAG, "Setting back stack index " + index + " to " + bse);
            mBackStackIndices.add(bse);
            return index;

        } else {
            int index = mAvailBackStackIndices.remove(mAvailBackStackIndices.size()-1);
            if (DEBUG) Log.v(TAG, "Adding back stack index " + index + " with " + bse);
            mBackStackIndices.set(index, bse);
            return index;
        }
    }
}
           

即如果

mAvailBackStackIndices

有空閑的索引(說明之前發生過pop操作), 則從其中配置設定索引, 否則,從索引結構

mBackStackIndices

中配置設定索引.

當back stack發生pop操作時, 釋放BackStackRecord的索引(FragmentManager.java):

public void freeBackStackIndex(int index) {
    synchronized (this) {
        mBackStackIndices.set(index, null);
        if (mAvailBackStackIndices == null) {
            mAvailBackStackIndices = new ArrayList<Integer>();
        }
        if (DEBUG) Log.v(TAG, "Freeing back stack index " + index);
        mAvailBackStackIndices.add(index);
    }
}
           

索引結構

mBackStackIndices

不再引用BackStackRecord, 但是該結構的

size

不會減小,而是把釋放的索引儲存在

mAvailBackStackIndices

中.

尾聲

FragmentManager的接口popBackStack是異步調用, 其同步調用接口為popBackStackImmediate. fragmentTransaction的commit接口也是異步調用, 其同步調用接口為commitNow.