前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >安卓Navigation系列——进阶篇

安卓Navigation系列——进阶篇

原创
作者头像
37手游安卓团队
修改2020-12-03 10:39:21
2.8K0
修改2020-12-03 10:39:21
举报

Navigation系列——进阶篇

作者

大家好,我叫小琪;

本人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: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/nav_graph"
    app:startDestination="@id/FragmentHome">

    <fragment
        android:id="@+id/FragmentHome"
        android:name="com.example.testnavigation.FragmentHome"
        android:label="fragment_home"
        tools:layout="@layout/fragment_home">
    </fragment>
    <fragment
        android:id="@+id/FragmentDicover"
        android:name="com.example.testnavigation.FragmentDicover"
        android:label="fragment_discover"
        tools:layout="@layout/fragment_discover">
    </fragment>
    <fragment
        android:id="@+id/FragmentMine"
        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: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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <fragment
        android:id="@+id/fragment"
        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:id="@+id/bottom_nav_view"
        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:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/FragmentHome"
        android:icon="@mipmap/icon_tab_home"
        android:title="首页" />

    <item
        android:id="@+id/FragmentDicover"
        android:icon="@mipmap/icon_tab_find"
        android:title="发现" />

    <item
        android:id="@+id/FragmentMine"
        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
@Override
public 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
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    ......
    mNavController = new NavHostController(context);
    ......
    onCreateNavController(mNavController);
    ......	
 }

看完了mNavController的构造函数,继续onCreateNavController方法,

@CallSuper
protected void onCreateNavController(@NonNull NavController navController) {
    navController.getNavigatorProvider().addNavigator(
            new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
    navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
}

createFragmentNavigator方法

@Deprecated
@NonNull
protected Navigator<? extends FragmentNavigator.Destination> createFragmentNavigator() {
    return new FragmentNavigator(requireContext(), getChildFragmentManager(),
            getContainerId());
}

可以看到,又继续添加了DialogFragmentNavigator和FragmentNavigator两个navigator,至此总共缓存了四个navigator。

回到NavHostFragment的oncreate方法,继续看后面的代码

@CallSuper
@Override
public 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:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemams.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nav_graph"
    app:startDestination="@id/FragmentHome">

    <fixFragment
        android:id="@+id/FragmentHome"
        android:name="com.example.testnavigation.FragmentHome"
        android:label="fragment_home"

        tools:layout="@layout/fragment_home">

    </fixFragment>
    <fixFragment
        android:id="@+id/FragmentDicover"
        android:name="com.example.testnavigation.FragmentDicover"
        android:label="fragment_discover"
        tools:layout="@layout/fragment_discover">

    </fixFragment>
    <fixFragment
        android:id="@+id/FragmentMine"
        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
@Override
public 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语句中,

@CallSuper
public void setGraph(@NavigationRes int graphResId) {
    setGraph(graphResId, null);
}
@CallSuper
public void setGraph(@NavigationRes int graphResId, @Nullable Bundle startDestinationArgs) {
    setGraph(getNavInflater().inflate(graphResId), startDestinationArgs);
}

进到getNavInflater().inflate

@SuppressLint("ResourceType")
@NonNull
public 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方法,

@NonNull
private NavDestination inflate(@NonNull Resources res, @NonNull XmlResourceParser parser,
        @NonNull AttributeSet attrs, int graphResId)
        throws XmlPullParserException, IOException {
    Navigator<?> navigator = mNavigatorProvider.getNavigator(parser.getName());
    ......
}

进到getNavigator方法

@CallSuper
@NonNull
public <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的重绘

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Navigation系列——进阶篇
    • 作者
      • 目录
        • 前言
          • 第一个实例
            • navigation的坑
              • 原因
                • 解决
                  • 总结
                  相关产品与服务
                  容器服务
                  腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档