很多老铁用BottomNavigationView+Fragment来实现主体APP的底部导航+页面渲染功能。老实说这个也是Google在Android Studio里推荐的用发。并且明显针对性的做了优化,所以你子啊使用BottomNavigationView+Fragment实现相关功能时你会感受到丝滑般的切换速度。
但是这个方案有一个致命的缺陷,就是每次切换后返回到上一个Fragment时都会重新加载页面,重新执行onCreateView和相关方法,数据和整个页面相当于刷新了一遍,这个对国内用户的习惯和体验来讲都是无法忍受的。所以才有了今天的这篇文章。
实现原理:
自己重写FragmentNavigator类,把核心的navigate方法重新实现,核心代码就是原版的暴力「ft.replace(mContainerId, frag);」这行替换成先判断目标Fragment是否已经存在,如果已经存在就直接返回旧的而不重建一个,不存在的才重建一个。
FixFragment类全代码如下 (核心方法为 navigate方法):
package util; import android.content.Context; import android.os.Bundle; import android.util.Log; import android.view.View; import java.util.ArrayDeque; import java.util.Map; import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.navigation.NavDestination; import androidx.navigation.NavOptions; import androidx.navigation.Navigator; import androidx.navigation.fragment.FragmentNavigator; /** * @ClassName FixFragmentNavigator * @Description TODO * @Author liqilin * @Date 2021/6/24 15:38 * @Version 1.0 */ @Navigator.Name("fixFragment") public class FixFragmentNavigator extends FragmentNavigator { private static final String TAG = "FixFragmentNavigator"; private static final String KEY_BACK_STACK_IDS = "androidx-nav-fragment:navigator:backStackIds"; private final Context mContext; private final FragmentManager mFragmentManager; private final int mContainerId; private ArrayDeque<Integer> mBackStack = new ArrayDeque<>(); public FixFragmentNavigator(@NonNull Context context, @NonNull FragmentManager manager, int containerId) { super(context, manager, containerId); mContext = context; mFragmentManager = manager; mContainerId = containerId; } /** * {@inheritDoc} * <p> * This method should always call * {@link FragmentTransaction#setPrimaryNavigationFragment(Fragment)} * so that the Fragment associated with the new destination can be retrieved with * {@link FragmentManager#getPrimaryNavigationFragment()}. * <p> * Note that the default implementation commits the new Fragment * asynchronously, so the new Fragment is not instantly available * after this call completes. */ @SuppressWarnings("deprecation") /* Using instantiateFragment for forward compatibility */ @Nullable @Override public NavDestination navigate(@NonNull FragmentNavigator.Destination destination, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) { if (mFragmentManager.isStateSaved()) { Log.i(TAG, "Ignoring navigate() call: FragmentManager has already" + " saved its state"); return null; } String className = destination.getClassName(); if (className.charAt(0) == '.') { className = mContext.getPackageName() + className; } final FragmentTransaction ft = mFragmentManager.beginTransaction(); int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1; int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1; int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1; int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1; if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) { enterAnim = enterAnim != -1 ? enterAnim : 0; exitAnim = exitAnim != -1 ? exitAnim : 0; popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0; popExitAnim = popExitAnim != -1 ? popExitAnim : 0; ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim); } /** * 1、先查询当前显示的fragment 不为空则将其hide * 2、根据tag查询当前添加的fragment是否不为null,不为null则将其直接show * 3、为null则通过instantiateFragment方法创建fragment实例 * 4、将创建的实例添加在事务中 */ Fragment fragment = mFragmentManager.getPrimaryNavigationFragment(); if (fragment != null) { ft.hide(fragment); } String tag = destination.getId() + ""; Fragment frag = mFragmentManager.findFragmentByTag(tag); if (frag != null) { ft.show(frag); } else { frag = instantiateFragment(mContext, mFragmentManager, className, args); frag.setArguments(args); ft.add(mContainerId, frag, tag); } ft.setPrimaryNavigationFragment(frag); final @IdRes int destId = destination.getId(); final boolean initialNavigation = mBackStack.isEmpty(); // TODO Build first class singleTop behavior for fragments final boolean isSingleTopReplacement = navOptions != null && !initialNavigation && navOptions.shouldLaunchSingleTop() && mBackStack.peekLast() == destId; boolean isAdded; if (initialNavigation) { isAdded = true; } else if (isSingleTopReplacement) { // Single Top means we only want one instance on the back stack if (mBackStack.size() > 1) { // If the Fragment to be replaced is on the FragmentManager's // back stack, a simple replace() isn't enough so we // remove it from the back stack and put our replacement // on the back stack in its place mFragmentManager.popBackStack( generateBackStackName(mBackStack.size(), mBackStack.peekLast()), FragmentManager.POP_BACK_STACK_INCLUSIVE); ft.addToBackStack(generateBackStackName(mBackStack.size(), destId)); } isAdded = false; } else { ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId)); isAdded = true; } if (navigatorExtras instanceof FragmentNavigator.Extras) { FragmentNavigator.Extras extras = (FragmentNavigator.Extras) navigatorExtras; for (Map.Entry<View, String> sharedElement : extras.getSharedElements().entrySet()) { ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue()); } } ft.setReorderingAllowed(true); ft.commit(); // The commit succeeded, update our view of the world if (isAdded) { mBackStack.add(destId); return destination; } else { return null; } } @NonNull private String generateBackStackName(int backStackIndex, int destId) { return backStackIndex + "-" + destId; } }
做了这个工作后,还需要把对应的xml(通常为mobile_navigation.xml)里面的Fragment标签换成自己写的fixFragment。换完后整个.xml文件应该是类似下面的:
<?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/mobile_navigation" app:itemTextColor="@drawable/main_bottom_navigation" app:itemIconTint="@drawable/main_bottom_navigation" app:startDestination="@+id/navigation_home"> <fixFragment android:id="@+id/navigation_home" android:name="com.XXX.ui.home.HomeFragment" android:label="@string/title_home" tools:layout="@layout/fragment_home" /> <fixFragment android:id="@+id/navigation_dashboard" android:name="com.XXX.ui.dashboard.DashboardFragment" android:label="@string/title_dashboard" tools:layout="@layout/fragment_dashboard" /> <fixFragment android:id="@+id/navigation_notifications" android:name="com.XXX.ui.my.MyFragment" android:label="@string/title_my" tools:layout="@layout/fragment_my" /> </navigation>
然后去掉默认的关联,取消方法,activity_main.xml里取消
app:navGraph="@navigation/mobile_navigation"
最后,在.java里面把导航和你的Fragment手动关联起来:
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_activity_user_info); NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration); NavigationUI.setupWithNavController(navView, navController); FragmentManager supportFragmentManager = getSupportFragmentManager(); NavHostFragment fragment = (NavHostFragment)supportFragmentManager.findFragmentById(R.id.nav_host_fragment_activity_user_info) ; Log.d("fragment.getId()",fragment.getId()+""); FixFragmentNavigator fragmentNavigator = new FixFragmentNavigator(this, supportFragmentManager, fragment.getId()); navController.getNavigatorProvider().addNavigator(fragmentNavigator); navController.setGraph(R.navigation.mobile_navigation);