官方文档:The Navigation Architecture Component
官方教程:Navigation Codelab
简介
Navigation 用于管理 APP 页面跳转导航,切换 fragment 更加直观,可视化界面展示 fragment 的切换流程图
Demo 地址:https://github.com/wuchao226/AndroidJetpack/tree/master
一、在项目中使用 Navigation
1. 在 Module下的 build.gradle 中添加以下依赖:
def nav_version = "2.3.2"
// Java language implementation
implementation "androidx.navigation:navigation-fragment:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version"
// Kotlin
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
如果你要使用SafeArgs
插件,还要在 Project 目录下的 build.gradle 文件添加:
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.0.0"
}
}
以及 Module 下面的 build.gradle 文件添加:
apply plugin: 'kotlin-android-extensions'
apply plugin: 'androidx.navigation.safeargs'
涉及到的类关系
- NavController 控制导航的跳转和弹出栈
- NavOptions 控制跳转过程中的配置选项, 例如动画和singleTop模式
- Navigation 工具类 创建点击事件或者获取控制器
- NavHostFragment 导航的容器, 可以设置和获取导航图(NavGraph)
- NavGraph 用于描述导航中页面关系的对象 可以增删改查页面,设置起始页等
- NavigationUI 用于将导航和一系列菜单控件自动绑定的工具类
- Navigator 页面的根接口, 如果想创建一个新的类型页面就要自定义他
- NavDeepLinkBuilder 构建一个能打开导航页面的Intent
2. 创建 Navigation 导航
- 在资源文件 res 目录下创建 navigation 目录 -> 右击 navigation 目录 New 一个Navigation resource file
- 创建一个Destination,在此之前,我已经写好了一个 LoginWelcomeFragment、LoginFragment 和 RegisterFragment
下面看内容组成,login_navigation.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"
android:id="@+id/login_navigation"
app:startDestination="@id/welcomeFragment">
<fragment
android:id="@+id/welcomeFragment"
android:name="com.wuc.jetpack.ui.fragment.login.LoginWelcomeFragment"
android:label="WelcomeFragment">
<action
android:id="@+id/action_welcomeFragment_to_loginFragment"
app:destination="@id/loginFragment"/>
<action
android:id="@+id/action_welcomeFragment_to_registerFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/registerFragment"/>
</fragment>
<fragment
android:id="@+id/loginFragment"
android:name="com.wuc.jetpack.ui.fragment.login.LoginFragment"
android:label="LoginFragment"/>
<fragment
android:id="@+id/registerFragment"
android:name="com.wuc.jetpack.ui.fragment.login.RegisterFragment"
android:label="RegisterFragment">
<argument
android:name="EMAIL"
android:defaultValue="wu@163.com"
app:argType="string"/>
</fragment>
</navigation>
下面是 navigation
标签的属性:
属性 | 解释 |
---|---|
app:startDestination | 默认的起始位置 |
下面是 fragment
标签的属性:
属性 | 解释 |
---|---|
name | 表示所属的 fragment 类 |
action | destination 属性用于指定下一个目标 fragment、进入退出动画等 |
argument | 用于传递数据 |
3. 建立 NavHostFragment
创建一个新的 LoginActivity
,在activity_login.xml
文件中:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.activity.LoginActivity">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/login_navigation"
android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
属性 | 解释 |
---|---|
android:name | 值必须是androidx.navigation.fragment.NavHostFragment,声明这是一个 NavHostFragment,表示这是一个可以切换 fragment 的控件 |
app:navGraph | 指定上面的导航意图文件login_navigation.xml ,也就是确定了 Navigation Graph |
app:defaultNavHost="true" | 表示是否拦截返回键,默认为 false, 假如页面处于第二个 fragment,这时按下返回键,会退出当前 activity 而不是回到上一个fragment。true 则反之 |
4. 界面跳转、参数传递和动画
在 LoginWelcomeFragment 中,点击登录和注册按钮可以分别跳转到 LoginFragment 和 RegisterFragment 中。
使用了两种方式实现:
一 利用ID导航
目标:LoginWelcomeFragment 携带 key 为 name 的数据跳转到 LoginFragment,LoginFragment 接收后显示。
btn_login.setOnClickListener {
//设置动画参数
val navOption = navOptions {
anim {
enter = R.anim.slide_in_right
exit = R.anim.slide_out_left
popEnter = R.anim.slide_in_left
popExit = R.anim.slide_out_right
}
}
// 参数设置
val bundle = Bundle()
bundle.putString("name", "wu")
findNavController().navigate(R.id.loginFragment, bundle, navOption)
LoginFragment 的接收代码如下:
val name = arguments?.getString("name")
if (!TextUtils.isEmpty(name)) {
et_account.setText(name)
}
二 利用Safe Args
LoginWelcomeFragment 通过 Safe Args 将数据传到 RegisterFragment,RegisterFragment 接收后显示。
再看一下已经展示过的 login_navigation.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"
android:id="@+id/login_navigation"
app:startDestination="@id/welcomeFragment">
<fragment
android:id="@+id/welcomeFragment"
android:name="com.wuc.jetpack.ui.fragment.login.LoginWelcomeFragment"
android:label="WelcomeFragment">
<action
android:id="@+id/action_welcomeFragment_to_loginFragment"
app:destination="@id/loginFragment"/>
<action
android:id="@+id/action_welcomeFragment_to_registerFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/registerFragment"/>
</fragment>
<fragment
android:id="@+id/loginFragment"
android:name="com.wuc.jetpack.ui.fragment.login.LoginFragment"
android:label="LoginFragment"/>
<fragment
android:id="@+id/registerFragment"
android:name="com.wuc.jetpack.ui.fragment.login.RegisterFragment"
android:label="RegisterFragment">
<argument
android:name="EMAIL"
android:defaultValue="wu@163.com"
app:argType="string"/>
</fragment>
</navigation>
action 标签
属性 | 作用 |
---|---|
app:destination"@+id/loginFragment" | 跳转完成到达的fragment的Id |
app:popUpTo="@id/home_dest" | 将 fragment 从栈中弹出,直到某个 Id 的 fragment |
app:popUpToInclusive="true/false" | 弹出栈是否包含目标 |
app:launchSingleTop="true/false" | 是否开启singleTop模式 |
argument 标签
属性 | 作用 |
---|---|
android:name | 标签名字 |
app:argType | 标签的类型 |
android:defaultValue | 默认值 |
LoginWelcomeFragment 中的 "注册" 按钮点击事件:
btn_register.setOnClickListener {
// 利用SafeArgs传递参数
val action = LoginWelcomeFragmentDirections
.actionWelcomeFragmentToRegisterFragment()
.setEMAIL("wu5@Gamil.com")
findNavController().navigate(action)
}
RegisterFragment中的接收:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val safeArgs:RegisterFragmentArgs by navArgs()
val email = safeArgs.email
et_email.setText(email)
}
下面是上述介绍的效果图:
二、Navigation 绑定 BottomNavigationView
先在 navigation 目录下新创建了 main_navigation.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/main_navigation"
app:startDestination="@id/homeFragment">
<fragment
android:id="@+id/homeFragment"
android:name="com.wuc.jetpack.ui.fragment.main.HomeFragment"
android:label="fragment_home"
tools:layout="@layout/fragment_home"/>
<fragment
android:id="@+id/scoreFragment"
android:name="com.wuc.jetpack.ui.fragment.main.ScoreFragment"
android:label="fragment_market"
tools:layout="@layout/fragment_score"/>
<fragment
android:id="@+id/meFragment"
android:name="com.wuc.jetpack.ui.fragment.main.MeFragment"
android:label="fragment_me"
tools:layout="@layout/fragment_me"/>
</navigation>
下面则是 activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="match_parent">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:navGraph="@navigation/main_navigation"
app:defaultNavHost="true"/>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/navigation_view"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@android:color/white"
app:menu="@menu/menu_main"
app:labelVisibilityMode="labeled"
app:itemHorizontalTranslationEnabled="false"
app:itemIconTint="@color/select_item_color"
app:itemTextColor="@color/select_item_color"/>
</androidx.appcompat.widget.LinearLayoutCompat>
下面是 menu_main.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/homeFragment"
android:icon="@drawable/btn_nav_home_press"
android:title="主页"
tools:ignore="HardcodedText"/>
<item
android:id="@+id/scoreFragment"
android:icon="@drawable/btn_nav_score_normal"
android:title="积分"
tools:ignore="HardcodedText"/>
<item
android:id="@+id/meFragment"
android:icon="@drawable/btn_nav_user_normal"
android:title="我"
tools:ignore="HardcodedText"/>
</menu>
注意:navigation 里 fragment 的 id 需要和 menu 里 item 的 id 对应
MainActivity
中的处理如下:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 获取NavController
val navController = findNavController(R.id.nav_host_fragment)
// 把BottomNavigationView与NavController绑定。
// 绑定后,当菜单项被选中时,会调用NavigationUI.onNavDestinationSelected(menuItem, navController)方法
// 这里有个注意事项,上面创建菜单时有说明,下面手动绑定的注释也有说明
// BottomNavigationView拥有setupWithNavController()方法,是因为Navigation组件依赖里对BottomNavigationView进行了扩展:
// fun BottomNavigationView.setupWithNavController(navController: NavController) {
// NavigationUI.setupWithNavController(this, navController)
// }
navigation_view.setupWithNavController(navController)
// 如果不用上面的方式,可以用下面的方式自己手动把菜单选中事件与Navigation的导航事件绑定
/*navigation_view.setOnNavigationItemSelectedListener { menuItem ->
// 导航到与menuItem菜单项关联的NavDestination,即与menu.xml中item的id相同的destinationId
// destinationId即navigation/nav_graph.xml中fragment的id
NavigationUI.onNavDestinationSelected(menuItem, navController)
}*/
}
}
下面是效果图:
参考文章:
谷歌实验室
Android官方架构组件Navigation:大巧不工的Fragment管理框架
即学即用Android Jetpack - Navigation
三、核心原理
NavHostFragment 是在 Activity 里创建的,下面先分析 NavHostFragment 源码。
3.1 初始化过程 NavHostFragment 生命周期方法
NavHostFragment 的创建,NavHostFragment 的 create 方法。
NavHostFragment#create
@NonNull
public static NavHostFragment create(@NavigationRes int graphResId) {
return create(graphResId, null);
}
@NonNull
public static NavHostFragment create(@NavigationRes int graphResId,
@Nullable Bundle startDestinationArgs) {
Bundle b = null;
if (graphResId != 0) {
b = new Bundle();
b.putInt(KEY_GRAPH_ID, graphResId);
}
if (startDestinationArgs != null) {
if (b == null) {
b = new Bundle();
}
b.putBundle(KEY_START_DESTINATION_ARGS, startDestinationArgs);
}
final NavHostFragment result = new NavHostFragment();
if (b != null) {
result.setArguments(b);
}
return result;
}
- 初始化 Bundle,并且将 graphResId,startDestinationArgs 存储在 Bundle 中,即将 app:navGraph="@navigation/login_navigation" 中 login_navigation 文件的id,graphResId 放进 Bundle
- 新建一个 NavHostFragment,把 bundle 里的数据设置给 NavHostFragment,最后返回一个新的 NavHostFragment;相当于把 activity_login.xml 里的 NavHostFragment 和 login_navigation.xml 文件绑定了。
通过打断点,我们发现 NavHostFragment 里生命周期各个方法的执行顺序是 onInflate、onCreate、onCreateNavController、onCreateView、onViewCreated
。
XML 文件的解析 NavHostFragment#onInflate
@CallSuper
@Override
public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs,
@Nullable Bundle savedInstanceState) {
super.onInflate(context, attrs, savedInstanceState);
final TypedArray navHost = context.obtainStyledAttributes(attrs,
androidx.navigation.R.styleable.NavHost);
final int graphId = navHost.getResourceId(
androidx.navigation.R.styleable.NavHost_navGraph, 0);
if (graphId != 0) {
mGraphId = graphId;
}
navHost.recycle();
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment);
final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false);
if (defaultHost) {
mDefaultNavHost = true;
}
a.recycle();
}
主要是解析 activity_login.xml 布局文件里的里的 fragment 标签包裹的一些 xml属性值,主要是两个属性:defaultNavHost 和 navGraph,并且初始化全局变量 。
获取 xml 里的导航图的 graphId,并将 graphId 赋值给 NavHostFragment 的成员变量 mGraphId,最后设置 defaultNavHost的值(defaultNavHost 值为 true 就可以实现拦截系统 back 键)。
NavHostFragment.onInflate 方法 当 Fragment 以 XML 的方式静态加载时,最先会调用 onInflate 的方法(调用时机:Fragment 所关联的 Activity 在执行 setContentView 时)
NavHostFragment#onCreate 导航初始化
@CallSuper
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
final Context context = requireContext();
// 始化 NavController,NavController 为导航的控制类,核心类。
mNavController = new NavHostController(context);
mNavController.setLifecycleOwner(this);
mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());
// Set the default state - this will be updated whenever
// onPrimaryNavigationFragmentChanged() is called
mNavController.enableOnBackPressed(
mIsPrimaryBeforeOnCreate != null && mIsPrimaryBeforeOnCreate);
mIsPrimaryBeforeOnCreate = null;
mNavController.setViewModelStore(getViewModelStore());
onCreateNavController(mNavController);
Bundle navState = null;
if (savedInstanceState != null) {// 开始恢复状态
navState = savedInstanceState.getBundle(KEY_NAV_CONTROLLER_STATE);
if (savedInstanceState.getBoolean(KEY_DEFAULT_NAV_HOST, false)) {
mDefaultNavHost = true;
getParentFragmentManager().beginTransaction()
.setPrimaryNavigationFragment(this)
.commit();
}
mGraphId = savedInstanceState.getInt(KEY_GRAPH_ID);
}
if (navState != null) {
// Navigation controller state overrides arguments
mNavController.restoreState(navState);
}
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);
}
}
// We purposefully run this last as this will trigger the onCreate() of
// child fragments, which may be relying on having the NavController already
// created and having its state restored by that point.
super.onCreate(savedInstanceState);
}
@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);
}
@CallSuper
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);
}
@SuppressLint("ResourceType")
@NonNull
public NavGraph inflate(@NavigationRes int graphResId) {
Resources res = mContext.getResources();
XmlResourceParser parser = res.getXml(graphResId);
final AttributeSet attrs = Xml.asAttributeSet(parser);
try {
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG
&& type != XmlPullParser.END_DOCUMENT) {
// Empty loop
}
if (type != XmlPullParser.START_TAG) {
throw new XmlPullParserException("No start tag found");
}
String rootElement = parser.getName();
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();
}
}
new NavHostController(context); 创建一个控制器 mNavController,把 mNavController 和 Lifecycle 建立绑定关系(监听生命周期有关);把 mNavController 和 ViewModelStore 建立关系(数据保存有关)。
onCreateNavController(mNavController); 将 mNavController 传入,根据新建这个控制器对应的 navigator,并把 navigator 和它对应的名字放进数组 mNavigators 里。
通过 mNavController.setGraph(mGraphId) ,根据导航图的 mGraphId 将导航图 NavGraph 和控制器 mNavController 关联起来,NavGraph 里又会通过 inflate方法解析导航图 xml 文件,并最后通过 addDestination 将目的地信息添加到到NavDestination,(控制器 mNavController 间接持有 NavDestination 数组: Deque<NavBackStackEntry> mBackStack = new ArrayDeque<>();)
NavBackStackEntry 类其实是包装了 NavDestination 类的;
NavInflater 主要就是解析导航图 xml 信息的。
NavInflater.inflate 方法根据传入的 XML 资源 id 构建 NavGraph,NavGraph 组成 Fragment 路由的导航地图,而 NavDestination 代表了导航的每一个目的地。在解析完 NavDestination 后,需要要求 NavDestination 为 NavGraph,即 NavGraph 是 NavDestination 的子类。而且在 NavGraph 内部存储了
NavDestination 信息。上面的 inflate方法内部会继续调用 inflate 方法。
(1)getNavigator方法获取都 Navigator 实例,该实例在构建 NavController 时被添加进去,这里获取的是 FragmentNavigator 对象。
(2)createDestination方法,会调用 FragmentNavigator 的 createDestination 构建 Destination 对象。
(3)onInflate 方法,解析 destination XML
(4)while 循环内部通过递归构建导航图。通过 NavInflater 类之后,解析了 XML 文件构建整个 Graph 之后,下面回到setGraph 方法,在解析完 XML 后会,回到 NavHostFragment.setGraph 方法。
(1)popBackStackInternal 方法将回退栈中的信息全部出栈。
(2)调用 onGraphCreated 主要是显示一个导航 Fragment 视图。onGraphCreated 方法
(1)恢复之前的导航状态
(2)调用 navigate 方法,显示第一个 Fragment。即在 Navigation 文件里,属性app:startDestination
的 Fragment。所以最终都会走到 navigate 导航方法。
NavHostFragment#onCreateNavController
onCreateNavController(mNavController);
@SuppressWarnings({"WeakerAccess", "deprecation"})
@CallSuper
protected void onCreateNavController(@NonNull NavController navController) {
navController.getNavigatorProvider().addNavigator(
new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
}
@Nullable
public final Navigator<? extends NavDestination> addNavigator(
@NonNull Navigator<? extends NavDestination> navigator) {
String name = getNameForNavigator(navigator.getClass());
return addNavigator(name, navigator);
}
@CallSuper
@Nullable
public Navigator<? extends NavDestination> addNavigator(@NonNull String name,
@NonNull Navigator<? extends NavDestination> navigator) {
if (!validateName(name)) {
throw new IllegalArgumentException("navigator name cannot be an empty string");
}
return mNavigators.put(name, navigator);
}
在实现导航的时候,我们需要根据 navigation 配置文件生成 NavGraph 类,然后在根据每个不同的 action id,找到对应的 NavDestination 就可以实现页面导航跳转了。
其中 mNavigatorProvider 是 NavController 中的全局变量,内部通过 HashMap 键值对的形式保存 Navigator 类。
createFragmentNavigator 方法,构建了 FragmentNavigator 对象,其中抽象类 Navigator 还有个重要的实现类 ActivityNavigator 和 NavGraphNavigator。
这个两个类的对象在 NavController 的构造方法中被添加。
public NavController(@NonNull Context context) {
mContext = context;
while (context instanceof ContextWrapper) {
if (context instanceof Activity) {
mActivity = (Activity) context;
break;
}
context = ((ContextWrapper) context).getBaseContext();
}
mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider));
mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
}
其中 Navigator 类的作用是:能够实例化对应的 NavDestination,并且能够实现导航功能,拥有自己的回退栈。
NavHostFragment#onCreateView
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
FragmentContainerView containerView = new FragmentContainerView(inflater.getContext());
// When added via XML, this has no effect (since this FragmentContainerView is given the ID
// automatically), but this ensures that the View exists as part of this Fragment's View
// hierarchy in cases where the NavHostFragment is added programmatically as is required
// for child fragment transactions
containerView.setId(getContainerId());// 用于以代码方式添加 fragment
return containerView;
}
创建顶层容器 FragmentContainerView,并且设置 FragmentContainerView 的 id(FragmentContainerView 是继承 FrameLayout 的)
NavHostFragment#onViewCreated
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (!(view instanceof ViewGroup)) {
throw new IllegalStateException("created host view " + view + " is not a ViewGroup");
}
Navigation.setViewNavController(view, mNavController);
// When added programmatically, we need to set the NavController on the parent - i.e.,
// the View that has the ID matching this NavHostFragment.
if (view.getParent() != null) {
mViewParent = (View) view.getParent();
if (mViewParent.getId() == getId()) {
// 把 mNavController 记录在 view 的 tag 中
Navigation.setViewNavController(mViewParent, mNavController);
}
}
}
将 mNavController 和 view 绑定起来。后面那段 if 判断的意思是:
(1)当通过 XML 添加时,父 View 是null,我们的 view 就是 NavHostFragment的根 FrameLayout。
(2)但是当以代码方式添加时,需要在父级上设置绑定 NavController(我们也可以在 MainActvity 里直接创建 NavHostFragment,并不一定在布局里创建)。
3.2 导航
在构建和获取到 NavController 对象以及 NavGraph 之后,下面是使用它来实现真正的导航了。下面从 navigate 开始分析。在 navigate 方法内部会查询到 NavDestination,然后根据不同的 Navigator 实现页面导航。
public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions,
@Nullable Navigator.Extras navigatorExtras) {
// 如果回退栈为null返回NavGraph,不为null返回回退栈中的最后一项。
NavDestination currentNode = mBackStack.isEmpty()
? mGraph
: mBackStack.getLast().getDestination();
if (currentNode == null) {
throw new IllegalStateException("no current navigation node");
}
@IdRes int destId = resId;
// 根据id,获取对应的NavAction。然后在通过NavAction获取目的地id。
final NavAction navAction = currentNode.getAction(resId);
Bundle combinedArgs = null;
if (navAction != null) {
if (navOptions == null) {
navOptions = navAction.getNavOptions();
}
destId = navAction.getDestinationId();
Bundle navActionArgs = navAction.getDefaultArguments();
if (navActionArgs != null) {
combinedArgs = new Bundle();
combinedArgs.putAll(navActionArgs);
}
}
if (args != null) {
if (combinedArgs == null) {
combinedArgs = new Bundle();
}
combinedArgs.putAll(args);
}
if (destId == 0 && navOptions != null && navOptions.getPopUpTo() != -1) {
popBackStack(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive());
return;
}
if (destId == 0) {
throw new IllegalArgumentException("Destination id == 0 can only be used"
+ " in conjunction with a valid navOptions.popUpTo");
}
// 利用目的地ID属性,通过findDestination方法,找到准备导航的目的地。
NavDestination node = findDestination(destId);
if (node == null) {
final String dest = NavDestination.getDisplayName(mContext, destId);
if (navAction != null) {
throw new IllegalArgumentException("Navigation destination " + dest
+ " referenced from action "
+ NavDestination.getDisplayName(mContext, resId)
+ " cannot be found from the current destination " + currentNode);
} else {
throw new IllegalArgumentException("Navigation action/destination " + dest
+ " cannot be found from the current destination " + currentNode);
}
}
// 开始导航
navigate(node, combinedArgs, navOptions, navigatorExtras);
}
一开始会查询到 NavDestination,然后根据不同的 Navigator 实现页面导航。
navigate 方法:
(1)如果回退栈为 null 返回 NavGraph,不为 null 返回回退栈中的最后一项。
(2)根据 id,获取对应的 NavAction。然后在通过 NavAction 获取目的地id。
(4)利用目的地 ID 属性,通过 findDestination 方法,找到准备导航的目的地。
(5)根据导航目的地的名字,调用 getNavigator 方法,获取 Navigator 对象。这里对应的是 FragmentNavigator。
NavDestination newDest = navigator.navigate(node, finalArgs, navOptions, navigatorExtras);
跳下一步
private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
boolean popped = false;
boolean launchSingleTop = false;
if (navOptions != null) {
if (navOptions.getPopUpTo() != -1) {
popped = popBackStackInternal(navOptions.getPopUpTo(),
navOptions.isPopUpToInclusive());
}
}
Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
node.getNavigatorName());
Bundle finalArgs = node.addInDefaultArgs(args);
NavDestination newDest = navigator.navigate(node, finalArgs,
navOptions, navigatorExtras);
if (newDest != null) {
if (!(newDest instanceof FloatingWindow)) {
// We've successfully navigating to the new destination, which means
// we should pop any FloatingWindow destination off the back stack
// before updating the back stack with our new destination
//noinspection StatementWithEmptyBody
while (!mBackStack.isEmpty()
&& mBackStack.peekLast().getDestination() instanceof FloatingWindow
&& popBackStackInternal(
mBackStack.peekLast().getDestination().getId(), true)) {
// Keep popping
}
}
// When you navigate() to a NavGraph, we need to ensure that a new instance
// is always created vs reusing an existing copy of that destination
ArrayDeque<NavBackStackEntry> hierarchy = new ArrayDeque<>();
NavDestination destination = newDest;
if (node instanceof NavGraph) {
do {
NavGraph parent = destination.getParent();
if (parent != null) {
NavBackStackEntry entry = new NavBackStackEntry(mContext, parent,
finalArgs, mLifecycleOwner, mViewModel);
hierarchy.addFirst(entry);
// Pop any orphaned copy of that navigation graph off the back stack
if (!mBackStack.isEmpty()
&& mBackStack.getLast().getDestination() == parent) {
popBackStackInternal(parent.getId(), true);
}
}
destination = parent;
} while (destination != null && destination != node);
}
// Now collect the set of all intermediate NavGraphs that need to be put onto
// the back stack
destination = hierarchy.isEmpty()
? newDest
: hierarchy.getFirst().getDestination();
while (destination != null && findDestination(destination.getId()) == null) {
NavGraph parent = destination.getParent();
if (parent != null) {
NavBackStackEntry entry = new NavBackStackEntry(mContext, parent, finalArgs,
mLifecycleOwner, mViewModel);
hierarchy.addFirst(entry);
}
destination = parent;
}
NavDestination overlappingDestination = hierarchy.isEmpty()
? newDest
: hierarchy.getLast().getDestination();
// Pop any orphaned navigation graphs that don't connect to the new destinations
//noinspection StatementWithEmptyBody
while (!mBackStack.isEmpty()
&& mBackStack.getLast().getDestination() instanceof NavGraph
&& ((NavGraph) mBackStack.getLast().getDestination()).findNode(
overlappingDestination.getId(), false) == null
&& popBackStackInternal(mBackStack.getLast().getDestination().getId(), true)) {
// Keep popping
}
mBackStack.addAll(hierarchy);
// The mGraph should always be on the back stack after you navigate()
if (mBackStack.isEmpty() || mBackStack.getFirst().getDestination() != mGraph) {
NavBackStackEntry entry = new NavBackStackEntry(mContext, mGraph, finalArgs,
mLifecycleOwner, mViewModel);
mBackStack.addFirst(entry);
}
// And finally, add the new destination with its default args
NavBackStackEntry newBackStackEntry = new NavBackStackEntry(mContext, newDest,
newDest.addInDefaultArgs(finalArgs), mLifecycleOwner, mViewModel);
mBackStack.add(newBackStackEntry);
} else if (navOptions != null && navOptions.shouldLaunchSingleTop()) {
launchSingleTop = true;
NavBackStackEntry singleTopBackStackEntry = mBackStack.peekLast();
if (singleTopBackStackEntry != null) {
singleTopBackStackEntry.replaceArguments(finalArgs);
}
}
updateOnBackPressedCallbackEnabled();
if (popped || newDest != null || launchSingleTop) {
dispatchOnDestinationChanged();
}
}
从 mNavigatorProvider 拿出对应的 navigator,然后调用 Navigator 的 navigate,将目的地,动画参数,跳转参数传入实现跳转
,而真正实现这个抽象方法的是在 FragmentNavigator 和 ActivityNavigator 的跳转方法里。我们看到 FragmentNavigator 里(ActivityNavigator里的实现更简单)
FragmentNavigator#navigate 方法
@SuppressWarnings("deprecation") /* Using instantiateFragment for forward compatibility */
@Nullable
@Override
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;
}
// 通过反射机制构建Fragment实例
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把fragment出栈
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;
}
}
(1)调用 instantiateFragment,通过反射机制构建 Fragment 实例
(2)处理进出场等动画逻辑 , 出入场动画都在 NavOptions 类里
(3)最终调用FragmentManager来处理导航逻辑;通过 mFragmentManager 把 fragment 出栈,入栈最后通过事务的提交 fragment。
ActivityNavigator最终也是调用了startActivity方法
总结
Navigation 的核心源码每个类的作用:
NavHosFragment
就是activty要绑定的Fragment,它和我们的导航xml绑定在一起,可以理解为实现导航的主要的FragmentNavHostController
导航控制器,也是整个Navigation源码里的核心类,是在NavHosFragment的onCreate方法里初始化和做一些关联操作的。用于中转控制xml解析,navigate导航等一系列主要操作。NavInflater
主要用于导航xml图的解析工作NavGraph
保存NavInflater解析后的目的地信息NavDestination
目的地实体类NavigatorProvider
导航Navigator类的提供者Navigator
导航类,提供导航-
NavAction
导航动作信息类,保存导航入场动画等信息