大家好,我叫小琪;
本人16年畢業于中南林業科技大學軟件工程專業,畢業后在教育行業做安卓開發,后來于19年10月加入37手游安卓團隊;
目前主要負責國內發行安卓相關開發,同時兼顧內部幾款App開發。
上篇對Navigation的一些概念進行了介紹,并在前言中提到了app中常用的一個場景,就是app的首頁,一般都會由一個activity+多個子tab組成,這種場景有很多種實現方式,比如可以使用RadioGroup、FrgmentTabHost、TabLayout或者自定義view等方式,但這些都離不開經典的FragmentManager來管理fragment之間的切換。
現在,我們有了新的實現方式,Navigation+BottomNavigationView,廢話不多說,先看最終要實現的效果
先確保引入了navigation相關依賴
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1'implementation 'androidx.navigation:navigation-ui-ktx:2.3.1'
很簡單,包含三個頁面,首頁、發現、我的,點擊底部可以切換頁面,有了上一篇的基礎,先新建一個nav_graph的導航資源文件,包含三個framgent子節點
<?xml version="1.0" encoding="utf-8"?><navigation xmlns:andro xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android: app:startDestination="@id/FragmentHome"> <fragment android: android:name="com.example.testnavigation.FragmentHome" android:label="fragment_home" tools:layout="@layout/fragment_home"> </fragment> <fragment android: android:name="com.example.testnavigation.FragmentDicover" android:label="fragment_discover" tools:layout="@layout/fragment_discover"> </fragment> <fragment android: android:name="com.example.testnavigation.FragmentMine" android:label="fragment_mine" tools:layout="@layout/fragment_mine"> </fragment></navigation>
然后在activity的布局中(這里為MainActivity的activity_main)中添加BottomNavigationView控件,
<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout xmlns:andro xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <fragment android: android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" app:defaultNavHost="false" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:navGraph="@navigation/nav_graph" /> <com.google.android.material.bottomnavigation.BottomNavigationView android: android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:menu="@menu/bottom_nav_menu" /></androidx.constraintlayout.widget.ConstraintLayout>
其中fragment節點在上面已經介紹過了,這篇不再講解,BottomNavigationView是谷歌的一個實現底部導航的組件, app:menu屬性為底部導航欄指定元素,新建一個bottom_nav_menu的menu資源文件
<?xml version="1.0" encoding="utf-8"?><menu xmlns:andro> <item android: android:icon="@mipmap/icon_tab_home" android:title="首頁" /> <item android: android:icon="@mipmap/icon_tab_find" android:title="發現" /> <item android: android:icon="@mipmap/icon_tab_mine" android:title="我的" /></menu>
注意:這里item標簽的id和上面nav_graph中fragment標簽的id一致
資源準備好后,在MainActivity中
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) //fragment的容器視圖,navHost的默認實現——NavHostFragment val navHostFragment = supportFragmentManager.findFragmentById(R.id.fragment) as NavHostFragment //管理應用導航的對象 val navController = navHostFragment.navController //fragment與BottomNavigationView的交互交給NavigationUI bottom_nav_view.setupWithNavController(navController) }}
通過NavigationUI庫,將BottomNavigationView和navigation關聯,就能實現上面的效果圖了,是不是so easy!
是不是很疑惑,這是怎么做到的?,此時我們進到源碼看看,進入setupWithNavController方法
fun BottomNavigationView.setupWithNavController(navController: NavController) { NavigationUI.setupWithNavController(this, navController)}
再進入
public static void setupWithNavController( @NonNull final BottomNavigationView bottomNavigationView, @NonNull final NavController navController) { bottomNavigationView.setOnNavigationItemSelectedListener( new BottomNavigationView.OnNavigationItemSelectedListener() { @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { return onNavDestinationSelected(item, navController); } }); ......}
在這里可以看到,給bottomNavigationView設置了一個item點擊事件,進到onNavDestinationSelected方法,
public static boolean onNavDestinationSelected(@NonNull MenuItem item, @NonNull NavController navController) { NavOptions.Builder builder = new NavOptions.Builder() .setLaunchSingleTop(true) .setEnterAnim(R.animator.nav_default_enter_anim) .setExitAnim(R.animator.nav_default_exit_anim) .setPopEnterAnim(R.animator.nav_default_pop_enter_anim) .setPopExitAnim(R.animator.nav_default_pop_exit_anim); if ((item.getOrder() & Menu.CATEGORY_SECONDARY) == 0) { builder.setPopUpTo(findStartDestination(navController.getGraph()).getId(), false); } NavOptions options = builder.build(); try { //TODO provide proper API instead of using Exceptions as Control-Flow. navController.navigate(item.getItemId(), null, options); return true; } catch (IllegalArgumentException e) { return false; }}
還記得上篇介紹過的,怎么從一個頁面跳轉到另一個頁面的嗎,這里也一樣,其實最終就是調用到了navController.navigate()方法進行頁面切換的。
使用Navigation+BottomNavigationView結合navigationUI擴展庫,這種方式是不是相比于以往的實現方式更簡單?可能大家迫不及待的想應用到自己的項目中去了,可殊不知還有坑在里面。
分別在三個fragment中的主要生命周期中打印各自的log,運行程序,打開FragmentHome,可以看到生命周期是正常執行的
然后點擊底部的發現切換到FragmentDiscover,FragmentDiscover生命周期也是正常的,但卻發現FragmentHome回調了onDestoryView()方法,
再次點擊首頁切回到FragmentHome,神奇的事情發生了,原來的FragmentHome銷毀了,卻又重新創建了一個新的FragmentHome實例,即fragment的重繪,并且從log日志中也可以看到,剛剛打開的FragmentDiscover也執行了onDestory同樣也銷毀了。
下面從源碼角度分析為什么會這樣。
從NavHostFragment入手,首先看到它的oncreate方法中,
@CallSuper@Overridepublic void onCreate(@Nullable Bundle savedInstanceState) { ...... mNavController = new NavHostController(context); ...... onCreateNavController(mNavController); ...... }
去掉無關代碼,只看核心代碼,可以看到,有一個NavHostController類型的mNavController成員變量,mNavController就是前篇文章中提到的管理導航的navController對象,只不過它是繼承自NavController的,戳進去構造方法,發現調用了父類的構造方法,再戳進去來到了NavController的構造方法,
public NavController(@NonNull Context context) { mContext = context; ....... mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider)); mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));}
在構造方法中,mNavigatorProvider添加了兩個navigator,首先看看mNavigatorProvider是個什么東東,
public class NavigatorProvider { private static final HashMap<Class<?>, String> sAnnotationNames = new HashMap<>();...... @NonNull static String getNameForNavigator(@NonNull Class<? extends Navigator> navigatorClass) { String name = sAnnotationNames.get(navigatorClass); if (name == null) { Navigator.Name annotation = navigatorClass.getAnnotation(Navigator.Name.class); name = annotation != null ? annotation.value() : null; if (!validateName(name)) { throw new IllegalArgumentException("No @Navigator.Name annotation found for " + navigatorClass.getSimpleName()); } sAnnotationNames.put(navigatorClass, name); } return name; }}
看核心的一個方法getNameForNavigator,該方法傳入一個繼承了Navigator的類,然后獲取其注解為Navigator.Name的值,并通過sAnnotationNames緩存起來,這說起來好像有點抽象,我們看具體的,前面有說到mNavigatorProvider添加了兩個navigator,分別是NavGraphNavigator和ActivityNavigator,我們戳進去ActivityNavigator源碼,
getNameForNavigator方法對應到這里,其實就是獲取到了Navigator.Name的注解值activity,由此可以知道,mNavigatorProvider調用addNavigator方法,就會緩存key為navigator的類,值為這個類的Navigator.Name注解值。
回到前面的NavHostFragment的onCreate方法中,
@CallSuper@Overridepublic void onCreate(@Nullable Bundle savedInstanceState) { ...... mNavController = new NavHostController(context); ...... onCreateNavController(mNavController); ...... }
看完了mNavController的構造函數,繼續onCreateNavController方法,
@CallSuperprotected void onCreateNavController(@NonNull NavController navController) { navController.getNavigatorProvider().addNavigator( new DialogFragmentNavigator(requireContext(), getChildFragmentManager())); navController.getNavigatorProvider().addNavigator(createFragmentNavigator());}
createFragmentNavigator方法
@Deprecated@NonNullprotected Navigator<? extends FragmentNavigator.Destination> createFragmentNavigator() { return new FragmentNavigator(requireContext(), getChildFragmentManager(), getContainerId());}
可以看到,又繼續添加了DialogFragmentNavigator和FragmentNavigator兩個navigator,至此總共緩存了四個navigator。
回到NavHostFragment的oncreate方法,繼續看后面的代碼
@CallSuper@Overridepublic void onCreate(@Nullable Bundle savedInstanceState) { ...... mNavController = new NavHostController(context); ...... onCreateNavController(mNavController); ...... if (mGraphId != 0) { // Set from onInflate() mNavController.setGraph(mGraphId); } else { ...... }}
在onInflate()方法中可以看出,mGraphId就是在布局文件中定義NavHostFragment時,通過app:navGraph屬性指定的導航資源文件,
跟進setGraph()方法,
public void setGraph(@NavigationRes int graphResId) { setGraph(graphResId, null); } public void setGraph(@NavigationRes int graphResId, @Nullable Bundle startDestinationArgs) { setGraph(getNavInflater().inflate(graphResId), startDestinationArgs); } public void setGraph(@NonNull NavGraph graph, @Nullable Bundle startDestinationArgs) { if (mGraph != null) { // Pop everything from the old graph off the back stack popBackStackInternal(mGraph.getId(), true); } mGraph = graph; onGraphCreated(startDestinationArgs); }
在第二個重載方法中,通過getNavInflater().inflate方法創建出一個NavGraph對象,傳到第三個重載的方法中,并賦值給成員變量mGraph,最后在onGraphCreated方法中將第一個頁面顯示出來。
由此可見,導航資源文件nav_graph會被解析成一個NavGraph對象,看下NavGraph
public class NavGraph extends NavDestination implements Iterable<NavDestination> { final SparseArrayCompat<NavDestination> mNodes = new SparseArrayCompat<>();}
NavGraph繼承了NavDestination,NavDestination其實就是nav_graph.xml中navigation下的一個個節點,也就是一個個頁面,NavGraph內部有個集合mNodes,用來保存一組NavDestination。
至此我們具體分析了兩個重要的步驟,一個是navigator的,一個是nav_graph.xml是如何被解析并關聯到navController,弄清楚這兩個步驟,對接下來的分析大有幫助。
還記得前面有分析到,BottomNavigationView是怎么做到頁面切換的嗎,把上面代碼照樣搬過來,
public static boolean onNavDestinationSelected(@NonNull MenuItem item, @NonNull NavController navController) { NavOptions.Builder builder = new NavOptions.Builder() .setLaunchSingleTop(true) .setEnterAnim(R.animator.nav_default_enter_anim) .setExitAnim(R.animator.nav_default_exit_anim) .setPopEnterAnim(R.animator.nav_default_pop_enter_anim) .setPopExitAnim(R.animator.nav_default_pop_exit_anim); if ((item.getOrder() & Menu.CATEGORY_SECONDARY) == 0) { builder.setPopUpTo(findStartDestination(navController.getGraph()).getId(), false); } NavOptions options = builder.build(); try { //TODO provide proper API instead of using Exceptions as Control-Flow. navController.navigate(item.getItemId(), null, options); return true; } catch (IllegalArgumentException e) { return false; }}
沒錯,是通過 navController.navigate這個方法,傳入item.getItemId(),由此可以知道,上面提到過的,定義BottomNavigationView時 app:menu屬性指定的menu資源文件中,item標簽的id和nav_graph中fragment標簽的id保持一致的原因了吧,我們繼續跟蹤,
public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions) { navigate(resId, args, navOptions, null);} public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) { ...... @IdRes int destId = resId; ....... NavDestination node = findDestination(destId); ...... navigate(node, combinedArgs, navOptions, navigatorExtras); } private void navigate(@NonNull NavDestination node, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) { ...... Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator( node.getNavigatorName()); Bundle finalArgs = node.addInDefaultArgs(args); NavDestination newDest = navigator.navigate(node, finalArgs, navOptions, navigatorExtras); ...... }
可以看到,在第二個重載方法中,通過findDestination方法傳入導航到目標頁面的id,獲得NavDestination對象node,在第三個重載方法中,通過mNavigatorProvider獲取navigator,那么這個navigator是什么呢,還記得上面分析的NavHostFragment經過oncreate方法之后,navigatorProvider總共緩存了四個navigator嗎, 由于在nav.graph.xml中,定義的是<framgent>標簽,所以這里navigator最終拿到的是一個FragmentNavigator對象。進到FragmentNavigator的navigate方法
public NavDestination navigate(@NonNull 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 Fragment frag = instantiateFragment(mContext, mFragmentManager, className, args); frag.setArguments(args); 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); } ft.replace(mContainerId, frag); 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 Extras) { Extras extras = (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; }}
通過Destination拿到ClassName,instantiateFragment方法通過內反射創建出對應的fragment,最后通過FragmentTransaction的replace方法創建fragment。
至此,終于真相大白了!我們知道replace方法每次都會重新創建fragment,所以使用Navigation創建的底部導航頁面,每次點擊切換頁面當前fragment都會重建。
既然知道了fragment重繪的原因,那就可以對癥下藥了,我們知道,fragment的切換除了replace,還可以通過hide和show,那怎么做到呢,通過前面的分析,其實可以自定義一個navigator繼承FragmentNavigator,重寫它的navigate方法,從而達到通過hide和show進行fragment切換的目的。
這里新建一個FixFragmentNavigator類,我們希望在nav_graph中通過fixFragment標簽來指定每個導航頁面
@Navigator.Name("fixFragment")class FixFragmentNavigator(context: Context, manager: FragmentManager, containerId: Int) : FragmentNavigator(context, manager, containerId) { private val mContext = context private val mManager = manager private val mContainerId = containerId private val TAG = "FixFragmentNavigator" override fun navigate( destination: Destination, args: Bundle?, navOptions: NavOptions?, navigatorExtras: Navigator.Extras? ): NavDestination? { if (mManager.isStateSaved) { Log.i(TAG, "Ignoring navigate() call: FragmentManager has already" + " saved its state") return null } var className = destination.className if (className[0] == '.') { className = mContext.packageName + className } val ft = mManager.beginTransaction() var enterAnim = navOptions?.enterAnim ?: -1 var exitAnim = navOptions?.exitAnim ?: -1 var popEnterAnim = navOptions?.popEnterAnim ?: -1 var popExitAnim = navOptions?.popExitAnim ?: -1 if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) { enterAnim = if (enterAnim != -1) enterAnim else 0 exitAnim = if (exitAnim != -1) exitAnim else 0 popEnterAnim = if (popEnterAnim != -1) popEnterAnim else 0 popExitAnim = if (popExitAnim != -1) popExitAnim else 0 ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) } /** * 1、先查詢當前顯示的fragment 不為空則將其hide * 2、根據tag查詢當前添加的fragment是否不為null,不為null則將其直接show * 3、為null則通過instantiateFragment方法創建fragment實例 * 4、將創建的實例添加在事務中 */ val fragment = mManager.primaryNavigationFragment //當前顯示的fragment if (fragment != null) { ft.hide(fragment) ft.setMaxLifecycle(fragment, Lifecycle.State.STARTED); } var frag: Fragment? val tag = destination.id.toString() frag = mManager.findFragmentByTag(tag) if (frag != null) { ft.show(frag) ft.setMaxLifecycle(frag, Lifecycle.State.RESUMED); } else { frag = instantiateFragment(mContext, mManager, className, args) frag.arguments = args ft.add(mContainerId, frag, tag) } ft.setPrimaryNavigationFragment(frag) @IdRes val destId = destination.id /** * 通過反射的方式獲取 mBackStack */ val mBackStack: ArrayDeque<Int> val field = FragmentNavigator::class.java.getDeclaredField("mBackStack") field.isAccessible = true mBackStack = field.get(this) as ArrayDeque<Int> val initialNavigation = mBackStack.isEmpty() val isSingleTopReplacement = (navOptions != null && !initialNavigation && navOptions.shouldLaunchSingleTop() && mBackStack.peekLast() == destId) val isAdded: Boolean 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 mManager.popBackStack( zygoteBackStackName(mBackStack.size, mBackStack.peekLast()), FragmentManager.POP_BACK_STACK_INCLUSIVE ) ft.addToBackStack(zygoteBackStackName(mBackStack.size, destId)) } isAdded = false } else { ft.addToBackStack(zygoteBackStackName(mBackStack.size + 1, destId)) isAdded = true } if (navigatorExtras is Extras) { val extras = navigatorExtras as Extras? for ((key, value) in extras!!.sharedElements) { ft.addSharedElement(key, value) } } ft.setReorderingAllowed(true) ft.commit() // The commit succeeded, update our view of the world if (isAdded) { mBackStack.add(destId) return destination } else { return null } } private fun zygoteBackStackName(backIndex: Int, destid: Int): String { return "$backIndex - $destid" }}
新建一個導航資源文件fix_nav_graph.xml,將原本的fragment換成fixFragment
<?xml version="1.0" encoding="utf-8"?><navigation xmlns:andro xmlns:app="http://schemams.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android: app:startDestination="@id/FragmentHome"> <fixFragment android: android:name="com.example.testnavigation.FragmentHome" android:label="fragment_home" tools:layout="@layout/fragment_home"> </fixFragment> <fixFragment android: android:name="com.example.testnavigation.FragmentDicover" android:label="fragment_discover" tools:layout="@layout/fragment_discover"> </fixFragment> <fixFragment android: android:name="com.example.testnavigation.FragmentMine" android:label="fragment_mine" tools:layout="@layout/fragment_mine"> </fixFragment></navigation>
然后把activity_main.xml中的app:navGraph屬性值替換為fix_nav_graph,
“修復版的”FragmentNavigator寫好后,在MainActivity中,通過navController把它添加到fragmentNavigator中,
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val navController = Navigation.findNavController(this, R.id.fragment) val fragment = supportFragmentManager.findFragmentById(R.id.fragment) as NavHostFragment val fragmentNavigator = FixFragmentNavigator(this, supportFragmentManager, fragment.id) //添加自定義的FixFragmentNavigator navController.navigatorProvider.addNavigator(fragmentNavigator) bottom_nav_view.setupWithNavController(navController) }
滿心歡喜的以為大功告成了,運行程序發現崩了,報錯如下:
報錯信息很明顯,找不到fixFragment對應的navigator,必須通過addNavigator方法進行添加,這怎么回事呢?明明已經調用addNavigator方法添加自定義的FixFragmentNavigator了。別急,還是回到NavHostFragment的onCreate()方法中,
@CallSuper@Overridepublic void onCreate(@Nullable Bundle savedInstanceState) { ...... if (mGraphId != 0) { // Set from onInflate() mNavController.setGraph(mGraphId); } else { // See if it was set by NavHostFragment.create() final Bundle args = getArguments(); final int graphId = args != null ? args.getInt(KEY_GRAPH_ID) : 0; final Bundle startDestinationArgs = args != null ? args.getBundle(KEY_START_DESTINATION_ARGS) : null; if (graphId != 0) { mNavController.setGraph(graphId, startDestinationArgs); } }}
上面已經說過了mGraphId就是通過app:navGraph指定的導航資源文件,那么mGraphId此時不等于0,走到if語句中,
@CallSuperpublic void setGraph(@NavigationRes int graphResId) { setGraph(graphResId, null);}
@CallSuperpublic void setGraph(@NavigationRes int graphResId, @Nullable Bundle startDestinationArgs) { setGraph(getNavInflater().inflate(graphResId), startDestinationArgs);}
進到getNavInflater().inflate
@SuppressLint("ResourceType")@NonNullpublic NavGraph inflate(@NavigationRes int graphResId) { ...... try { ...... NavDestination destination = inflate(res, parser, attrs, graphResId); if (!(destination instanceof NavGraph)) { throw new IllegalArgumentException("Root element <" + rootElement + ">" + " did not inflate into a NavGraph"); } return (NavGraph) destination; } catch (Exception e) { throw new RuntimeException("Exception inflating " + res.getResourceName(graphResId) + " line " + parser.getLineNumber(), e); } finally { parser.close(); }}
進到inflate方法,
@NonNullprivate NavDestination inflate(@NonNull Resources res, @NonNull XmlResourceParser parser, @NonNull AttributeSet attrs, int graphResId) throws XmlPullParserException, IOException { Navigator<?> navigator = mNavigatorProvider.getNavigator(parser.getName()); ......}
進到getNavigator方法
@CallSuper@NonNullpublic <T extends Navigator<?>> T getNavigator(@NonNull String name) { if (!validateName(name)) { throw new IllegalArgumentException("navigator name cannot be an empty string"); } Navigator<? extends NavDestination> navigator = mNavigators.get(name); if (navigator == null) { throw new IllegalStateException("Could not find Navigator with name "" + name + "". You must call NavController.addNavigator() for each navigation type."); } return (T) navigator;}
原來報錯的信息在這里,這里其實就是通過標簽獲取對應的navigator,然而在NavHostFragmen執行oncreate后,默認只添加了原本的四個navigator,而此時在解析fixFragment節點時,我們自定義的FixFragmentNavigator還未添加進來,所以拋了這個異常。
那么我們是不能在布局文件中通過app:navGraph屬性指定自定義的導航資源文件了,只能在布局文件中去掉app:navGraph這個屬性,然后在添加FixFragmentNavigator的同時,通過代碼將導航資源文件設置進去。
最終代碼如下:
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val navController = Navigation.findNavController(this, R.id.fragment) val fragment = supportFragmentManager.findFragmentById(R.id.fragment) as NavHostFragment val fragmentNavigator = FixFragmentNavigator(this, supportFragmentManager, fragment.id) //添加自定義的FixFragmentNavigator navController.navigatorProvider.addNavigator(fragmentNavigator) //通過代碼將導航資源文件設置進去 navController.setGraph(R.navigation.fix_nav_graph) bottom_nav_view.setupWithNavController(navController) }
運行程序,觀察各fragment的生命周期,發現已經不會重新走生命周期了。
本篇在上篇的基礎上,結合BottomNavigationView實現了第一個底部導航切換的實例,然后介紹了這種方式引發的坑,進而通過源碼分析了發生這種現象的原因,并給出了解決的思路。讀懂源碼才是最重要的,現在再總結一下navigator進行頁面切換的原理:
本文由 貴州做網站公司 整理發布,部分圖文來源于互聯網,如有侵權,請聯系我們刪除,謝謝!
網絡推廣與網站優化公司(網絡優化與推廣專家)作為數字營銷領域的核心服務提供方,其價值在于通過技術手段與策略規劃幫助企業提升線上曝光度、用戶轉化率及品牌影響力。這...
在當今數字化時代,公司網站已成為企業展示形象、傳遞信息和開展業務的重要平臺。然而,對于許多公司來說,網站建設的價格是一個關鍵考量因素。本文將圍繞“公司網站建設價...
在當今的數字化時代,企業網站已成為企業展示形象、吸引客戶和開展業務的重要平臺。然而,對于許多中小企業來說,高昂的網站建設費用可能會成為其發展的瓶頸。幸運的是,隨...
拍賣行是怎么收費的?一、費用有兩種:1.倉儲費-費用為商店價格的20 %( 24小時),48小時內為40%。如果你的拍賣品被賣出,這個保管費會和賣出費一起退還給你;如果你拍賣的物品不是想要的,當時間到了,物品會被退回,保管費也不會退還。2.拍賣費——你賣出商品后,拍賣行收取的傭金。主城拍賣行收費5%,中立拍賣行收費20%。倉儲費不是根據你賣的價格來定的,而是根據賣給NPC的貨物的價格和倉儲時間...
郴州體育場館巡回演唱8月18號有哪些明星?蔡依琳、小豬,SHE、林宥嘉、胡彥斌、許飛這些。郴州與永州市哪一個熱鬧?郴州與永州市,郴州相對性于繁華。郴州從古至今全是戰略要地,唐朝是也是設計了一個郡,因此經濟發展可謂比較繁榮,而永州市相較于郴州歸屬于相對落后,當初柳宗元是由于被皇上打入冷宮貶謫為永州市知州,那時候依據詩詞記述永州市屬于莽荒的地方。因而郴州較永州市比較發達。郴州與永州市哪一個熱鬧?郴州。...
華縣隸屬安陽市。1.河南省直管縣華縣位于河南省北部,與濮陽、安陽、鶴壁、新鄉接壤。南距鄭州130公里,北距安陽70公里,東北距濮陽53公里,西南距新鄉70公里。2.滑縣位于河南省北部平原,隸屬安陽市。目前是省直管縣,與濮陽、演金、??h、長垣、封丘、內黃接壤。北距安陽70km,東北距濮陽53km,西南距新鄉70km,西北距鶴壁新城25km。河南的滑縣屬于哪個市?河南省直管縣滑縣?;h是河南省直屬縣,...