Fragment

183 阅读4分钟

Fragment是什么?

在没有Fragment之前,如何一个页面非常复杂,我们通常会把一个界面拆分成多个部分把的xml布局,然后用这个几个部分的xml组合成一个界面。

image.png 这样的界面很常见,如果不使用Fragment,一般会用一个两个父布局,控制这两个父布局的显示和隐藏来显示不同的界面。xml布局如下:

<FrameLayout 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"
    tools:context=".FirstFragment">
    
    <!--界面1-->
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    </FrameLayout>
    
    <!--界面2-->
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    </FrameLayout>
</FrameLayout>

或者使用一个父布局,然后根据需要add()或者remove()子布局来显示不同的界面。

<!--Container-->
<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <!--动态的add或remove来显示不同的界面-->
    
</FrameLayout>

但是这个两种方法都有一个缺点就是,如果有一个界面不显示了,我需要关闭或释放一些资源,我需要自己写类似的ActivityonDestory等方法,并且如果我想显示控制的这几个界面的进入、返回顺序的话,也需要我们自己写一个栈来管理。
Fragment本质上就是使用的这种方法,只不过多了生命周期控制,以及调用栈的管理等功能。


Fragment是如何创建的?

Fragment的使用方法有两种

  1. 在xml中使用<fragment>,在解析xml布局的时候会根据<fragment>标签中的内容创建对应的Fragment
  2. 使用FragmentManager动态添加

第二种方法如下,

val fragmentTransaction = supportFragmentManager.beginTransaction()
//R.id.fragment_container可以是任意的ViewGroup
fragmentTransaction.replace(R.id.fragment_container, FirstFragment())
fragmentTransaction.commit()

Fragment的核心类

image.png

FragmentTransaction

  • BackStackRecord继承了FragmentManager
    FragemnetTransacation 用于记录添加、删除、隐藏 Fragement的操作,通过FragmentManager.beginTransaction会new一个BackStackRecord 调用FragmentTransaction.add()、FragmentTransaction.replace()等方法最终都会保存到FragmentTransaction中的一个list。

image.png 调用FragmentTransaction.commit之后会正式执行在List<Op>中的的操作。然后调用到FragmentManager.scheduleCommit中。
FragmentTransaction的主要功能就是记录add,replace等相关操作,真正的添加删除Fragment是由Fragmentmanager完成的。

FragmentManager

FragmentManager管理 Fragment的创建、删除等。调用FragmentTransaction.commit 之后会调用FragmentManager.equeueAction把记录的操作放在一个队列中,因此调用Fragment.comit不会立即执行。

boolean execPendingActions(boolean allowStateLoss) {
    ensureExecReady(allowStateLoss);

    boolean didSomething = false;
    while (generateOpsForPendingActions(mTmpRecords, mTmpIsPop)) {
        mExecutingActions = true;
        try {
            //执行add、remove相关操作
            removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop);
        } finally {
            cleanupExec();
        }
        didSomething = true;
    }

    updateOnBackPressedCallbackEnabled();
    doPendingDeferredStart();
    mFragmentStore.burpActive();

    return didSomething;
}

//执行FragmentTransaction中记录的操作,已经Fragment的生命周期相关方法
private void executeOpsTogether(@NonNull ArrayList<BackStackRecord> records,
        @NonNull ArrayList<Boolean> isRecordPop, int startIndex, int endIndex) {
    //...
    //把FragmentTransaction中记录的数据传递给FragmentManager
    //以及把Fragment添加到FragmentManager中法
    executeOps(records, isRecordPop, startIndex, endIndex);

    //...
    // And only then do we move all other fragments to the current state
    //更新Fragment生命周期
    moveToState(mCurState, true);
    
    
    Set<SpecialEffectsController> changedControllers = collectChangedControllers(
            records, startIndex, endIndex);
    for (SpecialEffectsController controller : changedControllers) {
        controller.updateOperationDirection(isPop);
        controller.markPostponedState();
        //执行动画
        controller.executePendingOperations();
    }
}

Fragment动画

SpecialEffectsController控制Fragment的动画

Controller for all "special effects" (such as Animation, Animator, framework Transition, and AndroidX Transition) that can be applied to a Fragment as part of the addition or removal of that Fragment from its container. Each SpecialEffectsController is responsible for a single ViewGroup container.
SpecialEffectsController是控制在Fragment添加或移除时的动画效果,一个SpecialEffectsController负责管理一个ViewGroup。

在调用Fragment生命周期相关方法后会执行SpecialEffectsController.executeOperations()执行动画,在DefaultSpecialEffectsController.startAnimations()中会分别执行AnimatorAnimation

private void startAnimations(@NonNull List<AnimationInfo> animationInfos,
        @NonNull List<Operation> awaitingContainerChanges,
        boolean startedAnyTransition, @NonNull Map<Operation, Boolean> startedTransitions) {

    // First run Animators
    boolean startedAnyAnimator = false;
    for (final AnimationInfo animationInfo : animationInfos) 
    //...
    // Now run Animations
    for (final AnimationInfo animationInfo : animationsToRun) 
    //...
}

常见问题

  1. IllegalStateException:Can not perform this action after onSaveInstanceState
    Activity在调用了onSaveInstanceState之后,如果在调用commit()就会抛出这个异常。因为在Activity.onSaveInstanceState会调用Fragment.saveAllState保存Fragment的状态信息到Bundle中(active,added等相关信息)。保存之后再调用commit()会导致保存的数据和现在的不一样,导致Activity重建从恢复数据的候,恢复的界面状态是之前的。为避免这种情况Android就直接在commit()方法中抛出异常。

Activity的onSaveInstanceState(bundle)是保存Activity的数据以便在界面重建后,能够保持和销毁之前显示的界面一致。
onRestoreInstanceState(bundle)的参数bundle就是之前保存的bundle,之前保存的数据都在bundle中。

//Activity
protected void onSaveInstanceState(@NonNull Bundle outState) {
    outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());

    //保存Fragment的相关数据
    Parcelable p = mFragments.saveAllState();
    if (p != null) {
        outState.putParcelable(FRAGMENTS_TAG, p);
    }
    getAutofillClientController().onSaveInstanceState(outState);
    dispatchActivitySaveInstanceState(outState);
}

//Fragment
Bundle saveAllStateInternal() {
    Bundle bundle = new Bundle();

    // First save all active fragments.
    ArrayList<String> active = mFragmentStore.saveActiveFragments();

    // And grab all FragmentState objects
    ArrayList<FragmentState> savedState = mFragmentStore.getAllSavedState();
    if (savedState.isEmpty()) {
        if (isLoggingEnabled(Log.VERBOSE)) {
            Log.v(TAG, "saveAllState: no fragments!");
        }
    } else {
        //保存状态信息
        FragmentManagerState fms = new FragmentManagerState();
        fms.mActive = active;
        fms.mAdded = added;
        fms.mBackStack = backStack;
        fms.mBackStackIndex = mBackStackIndex.get();
        if (mPrimaryNav != null) {
            fms.mPrimaryNavActiveWho = mPrimaryNav.mWho;
        }
        fms.mBackStackStateKeys.addAll(mBackStackStates.keySet());
        fms.mBackStackStates.addAll(mBackStackStates.values());
        fms.mLaunchedFragments = new ArrayList<>(mLaunchedFragments);
        bundle.putParcelable(FRAGMENT_MANAGER_STATE_TAG, fms);

        for (String resultName : mResults.keySet()) {
            bundle.putBundle(RESULT_NAME_PREFIX + resultName, mResults.get(resultName));
        }

        for (FragmentState state : savedState) {
            Bundle fragmentBundle = new Bundle();
            fragmentBundle.putParcelable(FRAGMENT_STATE_TAG, state);
            bundle.putBundle(FRAGMENT_NAME_PREFIX + state.mWho, fragmentBundle);
        }
    }

    return bundle;
}

如何解决这个异常? Android提供了commitNowAllowingStateLoss这个方法,不会检查Acivity是否已经保存过了。如果能接受保存的页面State和实际的State不一致,就可以使用这个方法替代。