Android Jetpack之Navigation

Android Jetpack之Navigation

1 前言

Android Jetpack 的导航组件Navigation可以很方便的管理fragment/activity的导航。

注意:如果您要在 Android Studio 中使用 Navigation 组件,则必须使用 Android Studio 3.3 或更高版本。

导航组件有三个关键部分

  1. NavGraph:导航图,包含一组页面和它们之间的跳转关系,比如A页面跳到B页面 B页面跳到C页面这样的关系信息
  2. NavHost:一个可以显示导航页面的空白容器,系统默认实现了一个NavHostFragment
  3. NavController:管理应用导航的对象,用来控制NavHost容器中当前应该显示的页面

2 页面跳转简单使用

下面先来一个简单的小例子了解一下基本使用效果如下:FirstFragment->SecondFragment;SecondFragment->ThirdFragment;ThirdFragment->FirstFragment;ThirdFragment->SecondFragment

添加最新依赖:

1
2
implementation "androidx.navigation:navigation-fragment:2.1.0"
implementation "androidx.navigation:navigation-ui:2.1.0"

2.1 创建导航图

  • 右键res文件夹
  • 依次选择New->Android Resource File
  • 第一行File name 中输入一个文件名 比如 nav_graph
  • 第二行在Resource type 下拉列表中选择 Navigation,然后点击 OK。

点击OK之后,Android Studio 会在 res 目录内创建一个 navigation 资源目录,这里面有我们刚才创建的文件。

双击该文件可以打开该文件如下图,我们可以使用图像化界面编辑,也可以使用代码来编辑,右下角可以切换。

点击上图1处的加号,把刚才创建的3个Fragment都添加进来,页面跳转关系可以直接手动连线,比如从FirstFragment连一个箭头到SecondFragment,就表示从FirstFragment跳转到SecondFragment。

如果点解了某条线,上图最右边会显示该线的属性信息,我们可以自己定义,包括线的id,将要导航的目的地,页面切换的动画,动画系统内置了几个,我们也可以自己定义补间画。弹出的时候切换到哪个页面等。

切换到编码栏可以看到最后生成的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<?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_demo_1"
app:startDestination="@id/firstFragment">

<fragment
android:id="@+id/firstFragment"
android:name="com.chs.androiddailytext.jetpack.navigation.FirstFragment"
android:label="fragment_frist"
tools:layout="@layout/fragment_frist">
<action
android:id="@+id/action_firstFragment_to_secondFragment"
app:destination="@id/secondFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
</fragment>

<fragment
android:id="@+id/secondFragment"
android:name="com.chs.androiddailytext.jetpack.navigation.SecondFragment"
android:label="fragment_second"
tools:layout="@layout/fragment_second">
<action
android:id="@+id/action_secondFragment_to_thirdFragment"
app:destination="@id/thirdFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
</fragment>

<fragment
android:id="@+id/thirdFragment"
android:name="com.chs.androiddailytext.jetpack.navigation.ThirdFragment"
android:label="fragment_third"
tools:layout="@layout/fragment_third">
<action
android:id="@+id/action_thirdFragment_to_firstFragment"
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:popUpTo="@id/firstFragment" />
<action
android:id="@+id/action_thirdFragment_to_secondFragment"
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:popUpTo="@+id/secondFragment" />
<deepLink app:uri="www.chs.com/{userName}" />
</fragment>
</navigation>

  • 根标签是navigation,它需要有一个属性startDestination,表示默认第一个显示的界面,这里设置FirstFragment
  • 每个fragment标签代表一个fragment类,其实不止可以写fragment标签,还可以写activity/dialog标签,代表着Naviagtion的默认能力,既可以导航Fragment,也可以导航Activity,DialogFragment。
  • 每个action标签就相当于上图中的每条线,代表了执行切换的时候的目的地,切换动画等信息。
  • 页面切换的动画就是简单的补间动画非常简单,比如定义一个从右边滑入的动画如下
    1
    2
    3
    4
    5
    <set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="-100%" android:toXDelta="0%"
    android:fromYDelta="0%" android:toYDelta="0%"
    android:duration="200"/>
    </set>

2.2 给activity添加NavHost

创建一个Activity,在其xml文件中添加FragmentNavHost

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">

<fragment
android:id="@+id/fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />

</FrameLayout>

  • 标签为fragment,android:name就是NavHost的实现类,这里是NavHostFragment
  • app:navGraph 属性就是我们前面在res文件夹下创建的文件
  • app:defaultNavHost="true" 意思是可以拦截系统的返回键,这样我们点击手机返回按钮的时候就能跟activity一样回到上一个页面了。

2.3 开启导航

1
2
3
4
5
view.findViewById(R.id.button).setOnClickListener(v -> {
Bundle bundle = new Bundle();
bundle.putString("title","我是前面传过来的");
Navigation.findNavController(v).navigate(R.id.action_firstFragment_to_secondFragment,bundle);
});

开启导航非常简单,就一句话,通过Navigation#findNavController方法找到NavController,调用它的navigate方法开始导航。

  • 第一个参数是action的id。
  • 第二个参数是bundle,用于两个界面之间传递参数,可以不传。在目标页面通过getArguments()方法来检索是否有bundle并获取数据。

除了直接使用Bundle来传递数据,Google更推荐我们使用Safe Args来传递数据,因为他可以确保数据的类型安全。

使用afe Args,首先在工程的build.gradle文件夹中添加afe Args的gradle插件的依赖

1
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0"

然后在app下的build.gradle文件中引入插件

1
2
3
apply plugin: "androidx.navigation.safeargs"
//kotlin使用下面方式引入
apply plugin: "androidx.navigation.safeargs.kotlin"

然后在最开始创建的nav_graph导航图文件中添加argument标签如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<fragment
android:id="@+id/firstFragment"
android:name="com.chs.androiddailytext.jetpack.navigation.FirstFragment"
android:label="fragment_frist"
tools:layout="@layout/fragment_frist">
<action
android:id="@+id/action_firstFragment_to_secondFragment"
app:destination="@id/secondFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
<argument android:name="title"
android:defaultValue="i am title"
app:argType="string"/>
</fragment>

argument标签中添加参数名,参数类型和默认值。添加完之后重新编译,插件就会自动给我们生成几个类,位置在 \app\build\generated\source\navigation-args\debug 如下图

下面开始使用这几个类,首先在FirstFragment中,直接通过FirstFragmentArgs.Builder()方法来创建bundle对象

1
2
3
4
view.findViewById(R.id.button).setOnClickListener(v -> {
Bundle bundle = new FirstFragmentArgs.Builder().setTitle("我是前面传过来的").build().toBundle();
Navigation.findNavController(v).navigate(R.id.action_firstFragment_to_secondFragment,bundle);
});

然后在SecondFragment中使用FirstFragmentArgs.fromBundle方法来接收值

1
2
3
4
5
Bundle arguments = getArguments();
if(arguments!=null){
String title = FirstFragmentArgs.fromBundle(getArguments()).getTitle();
tvTitle.setText(title);
}

这样也能正常接收到正确的传参,并且是类型安全的。

对于传递参数这块,由于这几个fragment在同一个activity中,所以我们还可以使用Jetpack组件库中的ViewModel和LiveData来共享参数。这样不仅可以保证类型安全,还可以在屏幕旋转导致的activity重建的时候保持数据不丢失。数据共享之前文章有Android Jetpack之ViewModel

2.3 Activity跳转

1
2
3
4
5
6
7
8
9
10
11
12
NavController navController = new NavController(this);
NavigatorProvider navigatorProvider = navController.getNavigatorProvider();
NavGraph navGraph = new NavGraph(new NavGraphNavigator(navigatorProvider));
ActivityNavigator navigator = navigatorProvider.getNavigator(ActivityNavigator.class);
ActivityNavigator.Destination destination = navigator.createDestination();
//id可以随便一个资源id
destination.setId(R.id.bottom_nav_activity);
destination.setComponentName(new ComponentName(getApplication().getPackageName(),
"com.chs.androiddailytext.jetpack.botton_navigation.BottomNavActivity"));
navGraph.addDestination(destination);
navGraph.setStartDestination(destination.getId());
navController.setGraph(navGraph);

NavController并不直接执行具体的导航操作,而是交给Navigator的子类具体的ActivityNavigator,FragmentNavigator,DialogFragmentNavigator去做,甚至可以自己继承一个Navigator来完成一个不一样的跳转。

这里跳转Activity就创建一个ActivityNavigator,使用它的全类名进行跳转。

Activity的跳转看起来还是挺麻烦的,直接一个startActivity不就完事了。那有啥用啊。

首先这部分代码可以封装一下会简单很多,其次把Activity和Fragment的跳转方式统一,最后,在组件化项目开发中,不同模块之间如果没有依赖关系,两者之间如果想要相互跳转的时候,就需要使用Activity的全类名来进行跳转了。之前组件化开发项目的时候可能会引入阿里的ARouter路由框架或者别的或者自己写路由框架,现在其实也可以直接使用Navigation,稍微改造一下也能很方便的实现组件之间跳转。

deepLink:深层链接,就是直接跳转到应用中的某个页面,比如从通知栏直接跳转到详情页面。

Navigation 可以创建两种不同的深层链接:显示深层链接和隐式深层链接

显示深层链接使用PendingIntent来导航到特定页面,比如可以在通知栏,快捷方式等地方跳转,比如下面的通过通知跳转。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
view.findViewById(R.id.button1).setOnClickListener(v -> {
Bundle bundle = new Bundle();
bundle.putString("userName","大海");
// PendingIntent pendingIntent = Navigation.findNavController(v).createDeepLink().setArguments(bundle)
// .setDestination(R.id.thirdFragment)
// .createPendingIntent();
PendingIntent pendingIntent = new NavDeepLinkBuilder(requireContext())
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.thirdFragment)
.setArguments(bundle)
.createPendingIntent();
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(requireContext());
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
notificationManager.createNotificationChannel(new NotificationChannel(
"deepLink","deepLinkName", NotificationManager.IMPORTANCE_HIGH
));
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(requireContext(), "deepLink")
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentTitle("测试deepLink")
.setContentText("哈哈哈")
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_DEFAULT);
notificationManager.notify(10,builder.build());
});

可以使用Navigation.findNavController(v).createDeepLink()或者new NavDeepLinkBuilder(context)两种方法来创建一个深层链接的PendingIntent。

注意使用显示深层链接打开应用的时候,当前堆栈会被清空,替换为当前深层上的结点。当为嵌套图表的时候(就是标签里面还有标签)那么每个都会添加到相应的堆栈中。

隐式链接是当用户点击某个链接的时候,通过URI跳转到某个页面。
建立隐式链接,首先在最开始创建的nav_graph.xml文件中给某个fragment添加deepLink标签。前面nav_graph.xml文件中已经添加

1
<deepLink app:uri="www.chs.com/{userName}" />

该uri没有声明是http还是https,那么这两个都能匹配。大括号内的是传递的参数。

然后去manifest.xml 文件中给当前的activity添加一个<nav-graph>属性

1
2
3
<activity android:name=".jetpack.navigation.NavigationActivity">
<nav-graph android:value="@navigation/nav_graph"/>
</activity>

在build的时候,Navigation 组件会将 <nav-graph> 元素替换为生成的 <intent-filter> 元素来匹配深层链接。其实我们在studio中双击打开apk就能在看到manifest.xml中替换完成的样子如下:
apk路径:app - > build - > outputs - > apk - > debug - > app-debug.apk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<activity
android:name="com.chs.androiddailytext.jetpack.navigation.NavigationActivity">
<intent-filter>
<action
android:name="android.intent.action.VIEW" />
<category
android:name="android.intent.category.DEFAULT" />
<category
android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="http" />
<data
android:scheme="https" />
<data
android:host="www.chs.com" />
<data
android:pathPrefix="/" />
</intent-filter>
</activity>

可以通过adb来测试隐式深层链接的效果,打开命令行输入

1
adb shell am start -a android.intent.action.VIEW -d "http://www.chs.com/Divad"

在系统弹出的窗口中,选择使用我们的应用打开,就能跳转到对应的页面了。

2.5 底部导航

使用Navigation还可以完成顶部应用栏的导航,侧滑抽屉的导航,底部导航。

下面来完成一个最常用的底部导航,底部导航非常容易实现,因为有现成的模板,只需在某个包下面右击鼠标,new->activity->Bottom Navigaton Activity就能直接创建一个带有底部导航的Activity。

不过我们学习阶段就不这么搞了,自己建议空白的Activity,一点点的添加进去,走一遍流程更容易熟悉。

创建一个BottomNavActivity和三个fragment:OneFragment,TowFragment,ThreeFragment。

  1. 在res->navigation文件夹下新创建一个导航图命名为nav_bottom_graph.xml。这个图里的fragment彼此没有关系,所以也不用写action,最终很简单:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <?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_bottom_graph"
    app:startDestination="@id/oneFragment">

    <fragment
    android:id="@+id/oneFragment"
    android:name="com.chs.androiddailytext.jetpack.botton_navigation.OneFragment"
    android:label="fragment_one"
    tools:layout="@layout/fragment_one" />
    <fragment
    android:id="@+id/towFragment"
    android:name="com.chs.androiddailytext.jetpack.botton_navigation.TowFragment"
    android:label="fragment_tow"
    tools:layout="@layout/fragment_tow" />
    <fragment
    android:id="@+id/threeFragment"
    android:name="com.chs.androiddailytext.jetpack.botton_navigation.ThreeFragment"
    android:label="fragment_three"
    tools:layout="@layout/fragment_three" />
    </navigation>

如果引用有ActionBar,android:label属性的内容就会显示在标题栏,成为该页面的标题。

  1. 创建一个menu,menu中就是我们的底部菜单的各个子项的信息
  • 右键res文件夹
  • 依次选择New->Android Resource File
  • 第一行File name 中输入一个文件名 比如 bottom_nav.xml
  • 第二行在Resource type 下拉列表中选择 Menu,然后点击 OK。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/oneFragment"
android:icon="@drawable/ic_home"
android:contentDescription="首页"
android:title="首页" />
<item
android:id="@+id/towFragment"
android:icon="@drawable/ic_list"
android:contentDescription="二页"
android:title="二页" />
<item
android:id="@+id/threeFragment"
android:icon="@drawable/ic_feedback"
android:contentDescription="三页"
android:title="三页" />
</menu>
  1. 在BottomNavActivity的布局文件中引入BottomNavigationView和NavHostFragment
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <fragment
    android:id="@+id/bottom_fragment"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"
    app:defaultNavHost="true"
    app:navGraph="@navigation/nav_bottom_graph" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
    android:id="@+id/bottom_nav"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:menu="@menu/bottom_nav"/>

    </LinearLayout>

NavHostFragment通过app:navGraph属性关联导航图,BottomNavigationView通过app:menu属性关联前面创建的menu

  1. 最后去Activity中使用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class BottomNavActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_bottom_navigation);
    BottomNavigationView bottomNavigationView = findViewById(R.id.bottom_nav);
    //导航控制器
    NavController controller = Navigation.findNavController(this,R.id.bottom_fragment);
    //底部导航的配置
    AppBarConfiguration configuration = new AppBarConfiguration.Builder(bottomNavigationView.getMenu()).build();
    //关联控制器和底部导航的配置
    NavigationUI.setupActionBarWithNavController(this,controller,configuration);
    //关联底部bottomNavigationView和控制器
    NavigationUI.setupWithNavController(bottomNavigationView,controller);
    }
    }

OK 底部导航视图建立完毕,效果

3 原理分析

下面根据第一个跳转的小例子来看一下Navigation的工作流程。

3.1 NavHostFragment#onCreate

从Activity中开始,Activity中就一个布局文件,内部有一个NavHostFragment,它是页面的承载区域,那就从它的onCreate方法开始分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Context context = requireContext();

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

创建了一个NavHostController类,并关联Fragment的生命周期,设置各种属性,先看一下NavHostController的构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public NavHostController(@NonNull Context context) {
super(context);
}
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));
}

往NavigatorProvider中添加了两个Navigator:NavGraphNavigator和ActivityNavigator

  • NavGraphNavigator:我们前面创建一个导航图NavGraph的时候会指定一个默认的第一个显示的页面(startDestination)该导航器就是用来导航到这个页面,
  • ActivityNavigator:顾名思义,用来给Activity导航的

回到NavHostFragment的onCreate方法中继续往下看,可以看到onCreateNavController(mNavController);方法

1
2
3
4
5
protected void onCreateNavController(@NonNull NavController navController) {
navController.getNavigatorProvider().addNavigator(
new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
}

这里又往NavigatorProvider添加了两个Navigator:

  • DialogFragmentNavigator:用来给DialogFragment导航
  • FragmentNavigator:用来给Fragment导航

3.2 Navigator

NavigatorProvider内部有个HashMap用来存储这几个Navigator。NavGraphNavigator,ActivityNavigator,DialogFragmentNavigator,FragmentNavigator,这几个类是专门用来导航的,那就先来了解一下这几个类,首先看其父类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public abstract class Navigator<D extends NavDestination> {
@Retention(RUNTIME)
@Target({TYPE})
@SuppressWarnings("UnknownNullness") // TODO https://issuetracker.google.com/issues/112185120
public @interface Name {
String value();
}

@NonNull
public abstract D createDestination();

@Nullable
public abstract NavDestination navigate(@NonNull D destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Extras navigatorExtras);

public abstract boolean popBackStack();

@Nullable
public Bundle onSaveState() {
return null;
}

public void onRestoreState(@NonNull Bundle savedState) {}

public interface Extras { }
}

  • 该类的泛型是NavDestination的子类,NavDestination其实就是一个一个的页面。
  • Navigator子类需要添加Name注解,比如ActivityNavigator中天街了@Navigator.Name(“activity”) ,FragmentNavigator中添加了@Navigator.Name(“fragment”),用来往NavigatorProvider中的HashMap中存放的时候最为key来使用
  • createDestination创造一个Destination(目标),也就是一个页面,抽象方法由子类实现
  • navigate方法,导航到目标页面,抽象方法由子类实现
  • onSaveState,onRestoreState 保存状态,恢复状态
  • Extras 提供一些额外的行为,比如转场动画等

父类看完,在来看看子类 就看比较常用的ActivityNavigator和FragmentNavigator,NavGraphNavigator,并且主要看它们实现的父类的哪两个抽象方法:createDestination和navigate

3.3 ActivityNavigator

ActivityNavigator#createDestination方法创建了一个ActivityNavigator.Destination对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
public static class Destination extends NavDestination {
private Intent mIntent;
private String mDataPattern;

public Destination(@NonNull NavigatorProvider navigatorProvider) {
this(navigatorProvider.getNavigator(ActivityNavigator.class));
}
public Destination(@NonNull Navigator<? extends Destination> activityNavigator) {
super(activityNavigator);
}

@CallSuper
@Override
public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
super.onInflate(context, attrs);
TypedArray a = context.getResources().obtainAttributes(attrs,
R.styleable.ActivityNavigator);
String targetPackage = a.getString(R.styleable.ActivityNavigator_targetPackage);
if (targetPackage != null) {
targetPackage = targetPackage.replace(NavInflater.APPLICATION_ID_PLACEHOLDER,
context.getPackageName());
}
setTargetPackage(targetPackage);
String className = a.getString(R.styleable.ActivityNavigator_android_name);
if (className != null) {
if (className.charAt(0) == '.') {
className = context.getPackageName() + className;
}
setComponentName(new ComponentName(context, className));
}
setAction(a.getString(R.styleable.ActivityNavigator_action));
String data = a.getString(R.styleable.ActivityNavigator_data);
if (data != null) {
setData(Uri.parse(data));
}
setDataPattern(a.getString(R.styleable.ActivityNavigator_dataPattern));
a.recycle();
}
@NonNull
public final Destination setIntent(@Nullable Intent intent) {
mIntent = intent;
return this;
}

@NonNull
public final Destination setTargetPackage(@Nullable String packageName) {
if (mIntent == null) {
mIntent = new Intent();
}
mIntent.setPackage(packageName);
return this;
}
@NonNull
public final Destination setComponentName(@Nullable ComponentName name) {
if (mIntent == null) {
mIntent = new Intent();
}
mIntent.setComponent(name);
return this;
}
@NonNull
public final Destination setAction(@Nullable String action) {
if (mIntent == null) {
mIntent = new Intent();
}
mIntent.setAction(action);
return this;
}
@NonNull
public final Destination setData(@Nullable Uri data) {
if (mIntent == null) {
mIntent = new Intent();
}
mIntent.setData(data);
return this;
}
@NonNull
public final Destination setDataPattern(@Nullable String dataPattern) {
mDataPattern = dataPattern;
return this;
}
......
}

构造方法中把当前Navigator的全类名保存到Destination的父类的成员变量中。

ActivityNavigator.Destination类内部还有很多跟Intent相关的方法比如setAction,setData等,用来创建Intent和给Intent设置参数。

ActivityNavigator#navigate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
if (destination.getIntent() == null) {
throw new IllegalStateException("Destination " + destination.getId()
+ " does not have an Intent set.");
}
Intent intent = new Intent(destination.getIntent());
if (args != null) {
intent.putExtras(args);
String dataPattern = destination.getDataPattern();
if (!TextUtils.isEmpty(dataPattern)) {
// Fill in the data pattern with the args to build a valid URI
StringBuffer data = new StringBuffer();
Pattern fillInPattern = Pattern.compile("\\{(.+?)\\}");
Matcher matcher = fillInPattern.matcher(dataPattern);
while (matcher.find()) {
String argName = matcher.group(1);
if (args.containsKey(argName)) {
matcher.appendReplacement(data, "");
//noinspection ConstantConditions
data.append(Uri.encode(args.get(argName).toString()));
} else {
throw new IllegalArgumentException("Could not find " + argName + " in "
+ args + " to fill data pattern " + dataPattern);
}
}
matcher.appendTail(data);
intent.setData(Uri.parse(data.toString()));
}
}
if (navigatorExtras instanceof Extras) {
Extras extras = (Extras) navigatorExtras;
intent.addFlags(extras.getFlags());
}
if (!(mContext instanceof Activity)) {
// If we're not launching from an Activity context we have to launch in a new task.
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
if (navOptions != null && navOptions.shouldLaunchSingleTop()) {
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
}
if (mHostActivity != null) {
final Intent hostIntent = mHostActivity.getIntent();
if (hostIntent != null) {
final int hostCurrentId = hostIntent.getIntExtra(EXTRA_NAV_CURRENT, 0);
if (hostCurrentId != 0) {
intent.putExtra(EXTRA_NAV_SOURCE, hostCurrentId);
}
}
}
final int destId = destination.getId();
intent.putExtra(EXTRA_NAV_CURRENT, destId);
if (navOptions != null) {
// For use in applyPopAnimationsToPendingTransition()
intent.putExtra(EXTRA_POP_ENTER_ANIM, navOptions.getPopEnterAnim());
intent.putExtra(EXTRA_POP_EXIT_ANIM, navOptions.getPopExitAnim());
}
if (navigatorExtras instanceof Extras) {
Extras extras = (Extras) navigatorExtras;
ActivityOptionsCompat activityOptions = extras.getActivityOptions();
if (activityOptions != null) {
ActivityCompat.startActivity(mContext, intent, activityOptions.toBundle());
} else {
mContext.startActivity(intent);
}
} else {
mContext.startActivity(intent);
}
if (navOptions != null && mHostActivity != null) {
int enterAnim = navOptions.getEnterAnim();
int exitAnim = navOptions.getExitAnim();
if (enterAnim != -1 || exitAnim != -1) {
enterAnim = enterAnim != -1 ? enterAnim : 0;
exitAnim = exitAnim != -1 ? exitAnim : 0;
mHostActivity.overridePendingTransition(enterAnim, exitAnim);
}
}
return null;
}

Activity的启动当然需要一个Intent对象,上面代码中可以看到,ActivityNavigator的navigate方法中就是通过Destination获取到Intent对象,然后设置传递的参数,设置Activity的启动模式,设置切换动画等,最后通过startActivity方法来启动一个Activity。

3.4 FragmentNavigator

FragmentNavigator#createDestination创建了一个FragmentNavigator.Destination对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public static class Destination extends NavDestination {

private String mClassName;

public Destination(@NonNull NavigatorProvider navigatorProvider) {
this(navigatorProvider.getNavigator(FragmentNavigator.class));
}

public Destination(@NonNull Navigator<? extends Destination> fragmentNavigator) {
super(fragmentNavigator);
}

@CallSuper
@Override
public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
super.onInflate(context, attrs);
TypedArray a = context.getResources().obtainAttributes(attrs,
R.styleable.FragmentNavigator);
String className = a.getString(R.styleable.FragmentNavigator_android_name);
if (className != null) {
setClassName(className);
}
a.recycle();
}

@NonNull
public final Destination setClassName(@NonNull String className) {
mClassName = className;
return this;
}

......

}

这里面的代码比ActivityNavigator中的少了很多,主要是设置ClassName的方法。后面根据ClassName反射创建出一个fragment对象。

FragmentNavigator#navigate方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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方法内通过ClassName加载出class对象并通过反射创建出fragment,最后通过FragmentTransaction的replace方法将创建出来的fragment放到相应的容器里面。

这里使用的是replace,我们知道replace方法每次都会重新创建fragment,所以使用Navigation创建的底部导航页面,每次点击切换页面当前fragment都会重建,这就不太友好了。解决思路可以继承FragmentNavigator 重写navigate方法,自己将replace改为hide和show

3.5 NavGraphNavigator

NavGraphNavigator#createDestination

1
2
3
4
5
6
7
public NavGraph createDestination() {
return new NavGraph(this);
}
public class NavGraph extends NavDestination implements Iterable<NavDestination> {
final SparseArrayCompat<NavDestination> mNodes = new SparseArrayCompat<>();
......
}

可以看到NavGraphNavigator的createDestination,并没有直接创建一个NavDestination对象,而是创建了一个NavGraph对象,它内部有个集合mNodes来保存了一组的NavDestination对象。

这个NavGraph对象其实就是最开始创建的那个nav_graph.xml解析并创建出来的。mNodes集合中保存的就是nav_graph.xml中的一个一个的结点,其实就是一个一个的页面。

那么nav_graph.xml文件是在哪里被解析的呢?这就得回到我们最开始看源码的地方NavHostFragment的onCreate方法中,有一句话 mNavController.setGraph(mGraphId)参数是nav_graph.xml文件的id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
  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);
}
```java
可以看到在其第二个重载的方法中,通过`getNavInflater().inflate`方法创建出一个NavGraph对象,传到第三个重载的方法中,并赋值给成员变量mGraph,最后在onGraphCreated方法中将第一个页面显示出来。

到这里我们知道,我们最开始创建的那个nav_graph.xml文件,最终会被解析成为一个NavGraph对象,然后个人NavController关联。所以即使没有该文件,我们也可以自己根据需要的参数new一个NavGraph对象,毕竟xml中配置不是很灵活。

怎么解析的,下面看一个inflate方法
```java
public NavGraph inflate(@NavigationRes int graphResId) {
Resources res = mContext.getResources();
XmlResourceParser parser = res.getXml(graphResId);
final AttributeSet attrs = Xml.asAttributeSet(parser);
......
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;
......

NavGraph是NavDestination的子类,创建出一个NavDestination对象,并判断是不是NavGraph对象,如果是强转成NavGraph并返回,如果不是,就抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
private NavDestination inflate(@NonNull Resources res, @NonNull XmlResourceParser parser,
@NonNull AttributeSet attrs, int graphResId)
throws XmlPullParserException, IOException {
Navigator<?> navigator = mNavigatorProvider.getNavigator(parser.getName());
final NavDestination dest = navigator.createDestination();

dest.onInflate(mContext, attrs);

final int innerDepth = parser.getDepth() + 1;
int type;
int depth;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& ((depth = parser.getDepth()) >= innerDepth
|| type != XmlPullParser.END_TAG)) {
if (type != XmlPullParser.START_TAG) {
continue;
}

if (depth > innerDepth) {
continue;
}

final String name = parser.getName();
//解析各种标签 argument ,deepLink action ...
if (TAG_ARGUMENT.equals(name)) {
inflateArgumentForDestination(res, dest, attrs, graphResId);
} else if (TAG_DEEP_LINK.equals(name)) {
inflateDeepLink(res, dest, attrs);
} else if (TAG_ACTION.equals(name)) {
inflateAction(res, dest, attrs, parser, graphResId);
} else if (TAG_INCLUDE.equals(name) && dest instanceof NavGraph) {
final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavInclude);
final int id = a.getResourceId(R.styleable.NavInclude_graph, 0);
((NavGraph) dest).addDestination(inflate(id));
a.recycle();
} else if (dest instanceof NavGraph) {
((NavGraph) dest).addDestination(inflate(res, parser, attrs, graphResId));
}

return dest;
}

先解析navigation根标签,它解析出来是个NavGraph对象,所以调用它的addDestination方法,将子标签解析出来的对象放入到成员你变量mNodes中。

NavGraphNavigator#navigate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public NavDestination navigate(@NonNull NavGraph destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Extras navigatorExtras) {
int startId = destination.getStartDestination();
if (startId == 0) {
throw new IllegalStateException("no start destination defined via"
+ " app:startDestination for "
+ destination.getDisplayName());
}
NavDestination startDestination = destination.findNode(startId, false);
if (startDestination == null) {
final String dest = destination.getStartDestDisplayName();
throw new IllegalArgumentException("navigation destination " + dest
+ " is not a direct child of this NavGraph");
}
Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
startDestination.getNavigatorName());
return navigator.navigate(startDestination, startDestination.addInDefaultArgs(args),
navOptions, navigatorExtras);
}

先找到第一个需要启动的页面的id,创建出一个NavDestination,通过标签的名字去mNavigatorProvider这个HashMap中拿到对应的Navigation。NavGraphNavigator并不是最终的执行者,它还是会把执行的任务交给特定的比如ActivityNavigator,FragmentNavigator和DialogFragmentNavigator去做。

总结

到这里Navigation的工作流程就走完了,总结一下:

  • 首先需要有一个承载页面的容器NavHost,系统有个默认是现实NavHostFragment它内部初始化了NavController
  • 想要完成导航必须要有一个导航控制器NavController
  • 它里面有两个非常重要的东西NavGraph和NavigatorProvider
  • NavGraph里面包含了一组NavDestination,每个NavDestination就是一个一个的页面,也就是导航目的地
  • NavigatorProvider内部有个HashMap,用来存放Navigator,Navigator它是个抽象类,有三个比较重要的子类FragmentNavigator,ActivityNavigator,DialogFragmentNavigator分别用来导航Fragment,Activity,DialogFragment,还有一个子类NavGraphNavigator,用来解析我们在xml中编写的文件,解析成一个NavGraph,并关联NavController,显示出第一个页面。
  • 这三个类都分别实现了父类的两个方法,一个是createDestination方法用来创建一个目的地,一个是navigate方法真正的用来导航

コメント

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×