1. <nobr id="easjo"><address id="easjo"></address></nobr>

      <track id="easjo"><source id="easjo"></source></track>
      1. 
        

      2. <bdo id="easjo"><optgroup id="easjo"></optgroup></bdo>
      3. <track id="easjo"><source id="easjo"><em id="easjo"></em></source></track><option id="easjo"><span id="easjo"><em id="easjo"></em></span></option>
          貴州做網站公司
          貴州做網站公司~專業!靠譜!
          10年網站模板開發經驗,熟悉國內外開源網站程序,包括DEDECMS,WordPress,ZBlog,Discuz! 等網站程序,可為您提供網站建設,網站克隆,仿站,網頁設計,網站制作,網站推廣優化等服務。我們專注高端營銷型網站,企業官網,集團官網,自適應網站,手機網站,網絡營銷,網站優化,網站服務器環境搭建以及托管運維等。為客戶提供一站式網站解決方案?。?!

          安卓Navigation系列——進階篇

          來源:互聯網轉載 時間:2024-01-29 08:13:07

          Navigation系列——進階篇

          作者

          大家好,我叫小琪;

          本人16年畢業于中南林業科技大學軟件工程專業,畢業后在教育行業做安卓開發,后來于19年10月加入37手游安卓團隊;

          目前主要負責國內發行安卓相關開發,同時兼顧內部幾款App開發。

          目錄

          • navigation——入門篇
          • navigation——進階篇(本章講解)
          • navigation——實戰篇 (敬請期待...)

          前言

          上篇對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擴展庫,這種方式是不是相比于以往的實現方式更簡單?可能大家迫不及待的想應用到自己的項目中去了,可殊不知還有坑在里面。

          navigation的坑

          分別在三個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進行頁面切換的原理:

          • 首先需要一個承載頁面的容器NavHost,這個容器有個默認的實現NavHostFragment
          • NavHostFragment有個mNavController成員變量,它是一個NavController對象,最終頁面導航都是通過調用它的navigate方法實現的
          • mNavController內部通過NavigatorProvider管理navigator
          • NavHostFragment在oncreate方法中,mNavController添加了四個navigator,分別是FragmentNavigator、ActivityNavigator、DialogFragmentNavigator、NavGraphNavigator,分別實現各自的navigate方法,進行頁面切換
          • mNavController通過調用setGraph()方法,傳入導航資源文件,并進行解析,獲取導航資源文件中的節點,得到NavDestination
          • FragmentNavigator的navigate方法中,是通過replace方法達到fragment的切換目的,因此會引起fragment的重繪
          標簽:navigation-
          下一篇:ERC20介紹

          網絡推廣與網站優化公司(網絡優化與推廣專家)作為數字營銷領域的核心服務提供方,其價值在于通過技術手段與策略規劃幫助企業提升線上曝光度、用戶轉化率及品牌影響力。這...

          在當今數字化時代,公司網站已成為企業展示形象、傳遞信息和開展業務的重要平臺。然而,對于許多公司來說,網站建設的價格是一個關鍵考量因素。本文將圍繞“公司網站建設價...

          在當今的數字化時代,企業網站已成為企業展示形象、吸引客戶和開展業務的重要平臺。然而,對于許多中小企業來說,高昂的網站建設費用可能會成為其發展的瓶頸。幸運的是,隨...

          拍賣行是怎么收費的?一、費用有兩種:1.倉儲費-費用為商店價格的20 %( 24小時),48小時內為40%。如果你的拍賣品被賣出,這個保管費會和賣出費一起退還給你;如果你拍賣的物品不是想要的,當時間到了,物品會被退回,保管費也不會退還。2.拍賣費——你賣出商品后,拍賣行收取的傭金。主城拍賣行收費5%,中立拍賣行收費20%。倉儲費不是根據你賣的價格來定的,而是根據賣給NPC的貨物的價格和倉儲時間...

          郴州體育場館巡回演唱8月18號有哪些明星?蔡依琳、小豬,SHE、林宥嘉、胡彥斌、許飛這些。郴州與永州市哪一個熱鬧?郴州與永州市,郴州相對性于繁華。郴州從古至今全是戰略要地,唐朝是也是設計了一個郡,因此經濟發展可謂比較繁榮,而永州市相較于郴州歸屬于相對落后,當初柳宗元是由于被皇上打入冷宮貶謫為永州市知州,那時候依據詩詞記述永州市屬于莽荒的地方。因而郴州較永州市比較發達。郴州與永州市哪一個熱鬧?郴州。...

          華縣隸屬安陽市。1.河南省直管縣華縣位于河南省北部,與濮陽、安陽、鶴壁、新鄉接壤。南距鄭州130公里,北距安陽70公里,東北距濮陽53公里,西南距新鄉70公里。2.滑縣位于河南省北部平原,隸屬安陽市。目前是省直管縣,與濮陽、演金、??h、長垣、封丘、內黃接壤。北距安陽70km,東北距濮陽53km,西南距新鄉70km,西北距鶴壁新城25km。河南的滑縣屬于哪個市?河南省直管縣滑縣?;h是河南省直屬縣,...

          TOP
          国产初高中生视频在线观看|亚洲一区中文|久久亚洲欧美国产精品|黄色网站入口免费进人
          1. <nobr id="easjo"><address id="easjo"></address></nobr>

              <track id="easjo"><source id="easjo"></source></track>
              1. 
                

              2. <bdo id="easjo"><optgroup id="easjo"></optgroup></bdo>
              3. <track id="easjo"><source id="easjo"><em id="easjo"></em></source></track><option id="easjo"><span id="easjo"><em id="easjo"></em></span></option>