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。如下图:
输入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节点:对应不同的视图,需要指明id和name,切记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找到当前 NavHostFragment 的 NavController ,用它来实现跳转。
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方法。