Navigation字面上的意思就是导航的意思,的确,它就是为了导航而生的。使用它,能够实现一个app项目只包含一个activity,其他的界面全部使用fragment进行替换。并且,能够自由切换fragment之间的跳转。

一、导入

app的build.gradle中添加如下配置:

def nav_version = "2.1.0"
    // Java
    implementation "androidx.navigation:navigation-fragment:$nav_version"
    implementation "androidx.navigation:navigation-ui:$nav_version"

二、使用

1、新建navigation导航

右击res目录,选择New->Android Resoure File。如下图:

android NavigationUI使用 安卓navigation_android


输入File name,回车即可。

<?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/main"
    >
    <fragment
        android:id="@+id/main"
        android:name="com.xinyartech.navigation.NavMainFragment"
        android:label="main"
        tools:layout="@layout/nav_fragment_main" >
        <action
            android:id="@+id/action_main_to_second"
            app:destination="@id/second" />
    </fragment>

    <fragment
        android:id="@+id/second"
        android:name="com.xinyartech.navigation.NavSecondFragment"
        android:label="second"
        tools:layout="@layout/nav_fragment_second" >
    </fragment>
    
</navigation>
  • navigation节点:表示这是一个导航结构图
  • fragment节点:对应不同的视图,需要指明idname切记name路径不要写错。
  • action节点:用于声明导航的行为,表示当前视图的下一视图,说白了就是从当前视图,跳转到action对应的视图,示例中对应NavMainFragment跳转到NavSecondFragment
  • app:startDestination属性:表示导航视图的起始页面对应的id,示例中对应的NavMainFragment
  • app:destination属性:目标视图
2、NavHostFragment
<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">

    <fragment
        android:id="@+id/nav_host"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="507dp"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph"
        tools:ignore="MissingConstraints"
        tools:layout_editor_absoluteX="51dp"
        tools:layout_editor_absoluteY="29dp" />

</androidx.constraintlayout.widget.ConstraintLayout>

在activity的主布局中引入NavHostFragment,意为导航容器,建立activity与导航视图的联系。
注意:需要设置 app:defaultNavHost 为 true,意思为覆盖系统的返回键,也就是说,当我们从FragmentA跳转到FragmentB后,按返回键,不会销毁当前activity,而是从FragmentB回退到FragmentA。

3、跳转与返回
NavController navHostController = Navigation.findNavController(this, R.id.nav_host);
// 跳转到secondFragment
navHostController.navigate(R.id.second);

通过NavController.navigate 控制跳转。

NavController navHostController = Navigation.findNavController(this, R.id.nav_host);
 navHostController.navigateUp();

使用NavController .navigateUp 控制返回。

当然,无论是跳转还是返回中间都是可以添加动画的,这里不做代码展示,只需要知道这个功能即可。需要的时候,可以自行添加。

4、疑问

看到上面的代码演示,想必大家已经对navigation的使用有了清楚的了解。但是你会不会有这样的疑问

  • NavHostFragment是什么玩意?
  • NavController 好像是控制跳转的,怎么控制的?

ok,带着这样的疑问,我们不得不从源码进行分析了。

三、原理

1、NavHostFragment

伪代码如下:

//----NavHostFragment 
public class NavHostFragment extends Fragment implements NavHost {

	//初始化NavHostController
	 public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        final Context context = requireContext();

        mNavController = new NavHostController(context);
        mNavController.setLifecycleOwner(this);
       ...
    }

	//FragmentContainerView填充容器
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        FragmentContainerView containerView = new FragmentContainerView(inflater.getContext());
        containerView.setId(getContainerId());
        return containerView;
    }
	@Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        ...
        Navigation.setViewNavController(view, mNavController);
        ...
    }
}

//----NavHost 
public interface NavHost {
    @NonNull
    NavController getNavController();
}

//----FragmentContainerView 
public final class FragmentContainerView extends FrameLayout {
	...
}

以上源码可以得出以下结论:

  • NavHostFragment是作为布局容器存在的,所有的导航视图fragment最终能够显示出来,都是显示在FragmentContainerView 这个FrameLayout中。
  • 每一个NavHostFragment都会对应一个NavController
  • 所有的 NavController 都会保存在 Navigation 中,并且已 key-value 形式存在

那么下面一行代码就不难理解了

NavController navHostController = Navigation.findNavController(this, R.id.nav_host);

根据key找到当前 NavHostFragmentNavController ,用它来实现跳转。

2、NavController

跟踪navigate方法源码,最终进入:

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);
        ...
    }

我们主要看是如何实现跳转的,其他代码省略了。以上源码可以看到几个类

  • NavDestination:一看就是对应目标Fragment,封装了跳转信息
  • Navigator:源码一看是个抽象类,具体实现呢?
  • NavOptions:封装了动画属性

好了,我们跳转拿到NavDestination不就可以了吗?这里面已经包含了跳转的信息,为什么最终使用Navigator.navigate进行跳转?

其实,google工程师这样设计的原因就是为了扩展,不仅仅是fragment可以使用NavDestination 进行跳转,activity也可以这样,所以就多出了代理跳转类Navigator。既然这样,想必大家都应该知道了真正的跳转逻辑,就是在Navigator的实现类中。

3、Navigator
3.1 ActivityNavigator
@Override
    public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        ...
    	 mContext.startActivity(intent);

		..
        mHostActivity.overridePendingTransition(enterAnim, exitAnim);
     	...
    }

最终就是通过大家熟悉的startActivity进行跳转。

3.2 FragmentNavigator
@Override
    public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        ...
        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);

       ...
    }

最终还是逃不了FragmentTransaction.replace方法。