前言
学习记录系列是通过阅读学习《Android Jetpack应用指南》对书中内容学习记录的Blog,《Android Jetpack应用指南》京东天猫有售,本文是学习记录的第三篇。
定义
Navigation 是一个可简化 Android 导航的库和插件,用于构建和组织应用内界面,处理深层链接以及在屏幕之间导航。
更确切的来说,Navigation 是用来在单个 Activity 嵌套多个 Fragment 的UI架构模式中管理 Fragment 的切换,并且可以通过可视化的方式,看见App的交互流程。旨在方便管理页面(页面包含 Fragment 和 Activity,主要值Fragment )和 App bar(ActionBar、ToolBar、CollapsingToolbarLayout)
优势
1、可视化的页面导航图,类似于 Apple Xcode 中的 StoryBoard,便于我们理清页面间的关系。
2、通过 destination 和 action 完成页面间的导航。
3、方便添加页面切换动画。
4、页面间类型安全的参数传递
5、通过 NavigationUI 类,对菜单、底部导航、抽屉菜单导航进行统一的管理
6、支持深层链接 DeepLink
Navigation的主要元素
1、Navigation Graph:这是一种新型的 XML 资源文件,其中包含应用程序所有的页面,以及页面间的关系
2、NavHostFragment:这是一个特殊的 Fragment。你可以认为它是其他 Fragmeng 的“容器”,Navigation Graph 中的Fragment 正是通过 NavHostFragment 进行展示的。
3、NavController:这是一个 Java/Kotlin 对象,用于在代码中完成 Navigation Graph 中具体的页面切换工作。
请认真阅读下面这句话,更好的理解上述 3 种元素之间的关系
当你想切换 Fragment 时,使用 NavController 对象,告诉它你想要去 Navigation Graph 中的 哪个 Fragment,NavController 会将你想去的 Fragment 展示在NavHostFragment 中。
使用Navigation
1.创建 Navigation Graph
新建一个 Android 项目后,依次选中 res 文件夹 → New → Android Resource File,新建一个 Navigation Graph 文件。如图所示: image.png 需要注意的点:使用 Navigation 需要依赖于相关支持库,因此当你没有添加相关依赖库的时候,Android Studio可能会询问你,是否自动帮你添加相关依赖。如图所示: image.png也可以手动添加相关依赖库
dependencies {
// Navigation 相关依赖
implementation 'androidx.navigation:navigation-fragment:2.3.1'
implementation 'androidx.navigation:navigation-ui:2.3.1'
}
2.添加 NavHostFragment
NavHostFragment 是一个特殊的Fragment,需要将其添加到 Activity 的布局文件中,作为其他 Fragment 的容器。
<?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=".navigation.NavigationActivity">
<fragment
android:id="@+id/nav_host_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph"/>
</androidx.constraintlayout.widget.ConstraintLayout>
告诉系统,这是一个特殊的Fragment
android:name="androidx.navigation.fragment.NavHostFragment"
表示该Fragment会自动处理系统返回键,即当用户按下手机的返回按钮时,系统能自动将当前所展示的Fragment退出
app:defaultNavHost="true"
用于设置该Fragment对应的导航图
app:navGraph="@navigation/nav_graph"
添加 NavHostFragment 之后,在回到导航图上。此时,在 Destinations 面板中可以看见我们刚才设置的 NavHostFragment。如图所示:
image.png
3.创建 destination
1.点击加号按钮,"Create new destination" 按钮,创建一个 destination。如图所示: image.png 2.destination 是“目的地”的意思,代表着你想去的页面。它可以是 Fragment 或 Activity,但最常见的是 Fragment,因为 Navigation 组件的作用是方便开发者在一个 Activity 中管理多个 Fragment。在此,通过 destination 创建一个名为 MainFragment 的 Fragment image.png 3.面板中出现了一个 mainFragment,“Start”表示该 MainFragment 是起始 Fragment,即 NavHostFragment 容器首先展示的 Fragment。 image.png 4.查看nav_graph.xml布局文件内容,可以看到,在 navigation 标签下有一个 startDestination 属性,该属性指定起始 destination 为 mainFragment。<?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/mainFragment">
<fragment
android:id="@+id/mainFragment"
android:name="com.jinxin.navigation.MainFragment"
android:label="fragment_main"
tools:layout="@layout/fragment_main" />
</navigation>
5.运行程序,可以看到一个空白的 Fragment, 即 destination 所指定的 mainFragment
image.png
4. 完成 Fragment 页面切换
1.创建 SecondFragment,创建完成之后,在导航面板中单击 mainFragment,用鼠标选中其右侧的圆圈,并拖拽至右边 secondFragment,松开鼠标之后会出现一个从 mainFragment 指向 secondFragment 的箭头 image.png查看布局文件,可以看到多了一个 <action/> 标签,app:destination 属性表示它的目的地是 secondFragment
<?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/mainFragment">
<fragment
android:id="@+id/mainFragment"
android:name="com.jinxin.navigation.MainFragment"
android:label="fragment_main"
tools:layout="@layout/fragment_main" >
<action
android:id="@+id/action_mainFragment_to_secondFragment"
app:destination="@id/secondFragment" />
</fragment>
<fragment
android:id="@+id/secondFragment"
android:name="com.jinxin.navigation.SecondFragment"
android:label="fragment_second"
tools:layout="@layout/fragment_second" />
</navigation>
5. 使用 NavController 完成导航
在 MainFragment 的布局文件中添加两个Button,分别对应两种跳转页面的方式。
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_main, container, false);
// 方法1
view.findViewById(R.id.btn_to_second_fragment_1).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Navigation.findNavController(v).navigate(R.id.action_mainFragment_to_secondFragment);
}
});
// 方法二
view.findViewById(R.id.btn_to_second_fragment_2)
.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.action_mainFragment_to_secondFragment));
return view;
}
运行应用程序可以看到 Fragment 完成了切换,但切换没有动画效果,显示很生硬。
6. 添加页面切换动画效果
首先,在 res/anim 文件夹下加入常见的动画文件 image.pngslide_in_left.xml
<?xml version="1.0" encoding="utf-8"?>
<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="700"/>
</set>
slide_in_right.xml
<?xml version="1.0" encoding="utf-8"?>
<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="700"/>
</set>
slide_out_left.xml
<?xml version="1.0" encoding="utf-8"?>
<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="700"/>
</set>
slide_out_right.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="100%" android:toXDelta="100%"
android:fromYDelta="0%" android:toYDelta="0%"
android:duration="700"/>
</set>
接着打开导航面板,选中箭头,并在右边 Animations 面板中为其设置动画文件
image.png
查看布局文件,可以看到它在<action/>标签中自动添加了动画的相关代码。实际上,我们可以在布局文件中编写代码,Design 面板只是使用了可视化的方式以方便操作。
<?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/mainFragment">
<fragment
android:id="@+id/mainFragment"
android:name="com.jinxin.navigation.MainFragment"
android:label="fragment_main"
tools:layout="@layout/fragment_main" >
<action
android:id="@+id/action_mainFragment_to_secondFragment"
app:destination="@id/secondFragment"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"/>
</fragment>
<fragment
android:id="@+id/secondFragment"
android:name="com.jinxin.navigation.SecondFragment"
android:label="fragment_second"
tools:layout="@layout/fragment_second" />
</navigation>
使用safe args 插件传递参数
1.常见的传递参数的方式
Fragment 的切换经常需要伴随着参数的传递,为了配合 Navigation 组件在切换 Fragment 时传递参数,Android Studio 为开发者提供了 safe args 插件。在介绍 safe args 插件之前,Fragment 间最常见的传递参数和接收参数的方式
MainFragment 传递参数:
Bundle bundle = new Bundle();
bundle.putString("user_name", "Michael");
bundle.putInt("age", 30);
Navigation.findNavController(v).navigate(R.id.action_mainFragment_to_secondFragment, bundle);
SecondFragment 接收参数:
TextView tvUserName = view.findViewById(R.id.tv_user_name);
TextView tvAge = view.findViewById(R.id.tv_age);
Bundle arguments = getArguments();
if (arguments != null) {
String userName = arguments.getString("user_name");
int age = arguments.getInt("age");
tvUserName.setText(userName);
tvAge.setText(String.valueOf(age));
}
2.使用 safe args 传递参数
首先,需要安装 safe args 插件。在 Project 的 build.gradle 文件中添加 safe args 插件。
dependencies {
classpath 'com.android.tools.build:gradle:4.0.2'
// Navigation 使用Safe Arg 插件传递参数
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.3.1'
}
接着,需要引用该插件。在 app 的 build.gradle 文件中添加对 safe args 的依赖
apply plugin: 'androidx.navigation.safeargs'
在 nav_graph.xml 布局文件中添加 <argument/> 标签。可以在布局文件中编写代码,也可以通过 Design 面板进行添加,因为是 secondFragment 接收参数展示,所以在 secondFragment 中添加参数
<fragment
android:id="@+id/secondFragment"
android:name="com.jinxin.navigation.SecondFragment"
android:label="fragment_second"
tools:layout="@layout/fragment_second" >
<!-- 添加参数 -->
<argument
android:name="userName"
app:argType="string"
android:defaultValue='"unknown'/>
<!-- 添加参数 -->
<argument
android:name="age"
app:argType="integer"
android:defaultValue="0"/>
</fragment>
添加 <argument/>标签之后,build一下工程,便可以在 app/generatedJava 目录下看到 safe args 插件生产的代码文件了,这些代码文件中包含了参数所对应的 Getter 和 Setter 方法。
image.png
最后,在Fragment 中利用所生产的代码文件,在 Fragment 之间进行参数传递。
使用 safe args 传递参数
Bundle bundle = new SecondFragmentArgs.Builder()
.setUserName("Michael")
.setAge(30)
.build().toBundle();
Navigation.findNavController(v).navigate(R.id.action_mainFragment_to_secondFragment, bundle);
使用 safe args 接收参数方式
Bundle arguments = getArguments();
if (arguments != null) {
SecondFragmentArgs secondFragmentArgs = SecondFragmentArgs.fromBundle(arguments);
String userName = secondFragmentArgs.getUserName();
int age = secondFragmentArgs.getAge();
tvUserName.setText(userName);
tvAge.setText(String.valueOf(age));
}
总结:正如 插件 safe args 名称所代表的意思,它的主要好处在于安全的参数类型。Getter 和 Setter 的方式令参数的操作更友好,更直观,且更安全
NavigationUI的使用方法
1.NavigationUI存在的意义
导航图是 Navigation 组件中很重要的一部分,它可以帮助快速了解页面之间的关系,再通过 NavController 便可以完成页面的切换工作。而在页面的切换过程中,通常还伴随着 App bar 中 menu 菜单的变化。对于不同的页面,App bar 中的 menu 菜单很可能是不一样的。 App bar 中的各种按钮和菜单,同样承担着页面切换的工作。例如,当 ActionBar 左边的返回按钮被单击时,需要响应该事件,返回到上一个页面,既然 Navigation 和 App bar 都需要处理页面切换事件,那么,为了方便管理, Jetpack 引入了 NavigationUI 组件,使 App bar 中的按钮和菜单能够于导航图中的页面关联起来。
2.案例分析
假设有两个页面:OneTestFragment 和 TwoTestFragment。这两个 Fragment 同属于 TestActivity。OneTestFragment 的 ActionBar 右边有一个按钮,通过该按钮,可以跳转到 TwoTestFragment。而在 TwoTestFragment 的 ActionBar 左侧有一个返回按钮,通过该按钮,可以返回 OneTestFragment。
<?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_test"
app:startDestination="@id/oneTestFragment">
<fragment
android:id="@+id/oneTestFragment"
android:name="com.jinxin.navigation.test.OneTestFragment"
android:label="fragment_one_test"
tools:layout="@layout/fragment_one_test" />
<fragment
android:id="@+id/twoTestFragment"
android:name="com.jinxin.navigation.test.TwoTestFragment"
android:label="fragment_two_test"
tools:layout="@layout/fragment_two_test" />
</navigation>
在 menu_settitngs.xml 文件中,为 ActionBar 添加菜单。注意,<item/>的 id 与 导航图中 TwoTestFragment的 id 是一致的,这表示,当该<item/>被单击时,将会跳转到 id 所对应的 Fragment,即 TwoTestFragment。
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/twoTestFragment"
android:icon="@drawable/ic_launcher_background"
android:title="第二个界面"/>
</menu>
在 TestActivity 中实例化菜单并使用 NavigationUI 组件处理被单击的菜单项的跳转逻辑
public class TestActivity extends AppCompatActivity {
private static final String TAG = "TestActivity";
private NavController navController;
private AppBarConfiguration appBarConfiguration;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
// NavController 用于页面的导航和切换
navController = Navigation.findNavController(this, R.id.nav_host_fragment_test);
// AppBarConfiguration 用于 Appbar 的配置
appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build();
// 将 Appbar 和 NavController绑定起来
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
// 实例化菜单
getMenuInflater().inflate(R.menu.menu_setting, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
Log.d(TAG, "onOptionsItemSelected: ");
// 由于再导航图和菜单的布局文件中,已经为TwoTestFragment设置好了相同的id(即twoTestFragment)
// 因此,在onOptionsItemSelected()方法中,通过NavigationUI便可以自动完成页面跳转
return NavigationUI.onNavDestinationSelected(item, navController) || super.onOptionsItemSelected(item);
}
@Override
public boolean onSupportNavigateUp() {
Log.d(TAG, "onSupportNavigateUp: ");
// 覆盖onSupportNavigationUp()方法,当在SettingsFragment中单击ActionBar左边的返回按钮时,
// NavigationUI可以帮助settingsFragment回到MainFragment
return NavigationUI.navigateUp(navController, appBarConfiguration) || super.onSupportNavigateUp();
}
}
需要注意的是,在示例中,App bar 是在 TestActivity 中进行管理的。当从 OneTestFragment 跳转到 TwoTestFragment 时,需要在 TwoTestFragment 中覆盖 onCreateOptionsMenu()方法,并在该方法中清除 TwoTestFragment 所对应的menu。
public class TwoTestFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_two_test, container, false);
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
// 清除menu
menu.clear();
super.onCreateOptionsMenu(menu, inflater);
}
}
Jetpack 提供了 OnDestinationChangedListener 接口,用来监听页面切换事件
navController.addOnDestinationChangedListener(new NavController.OnDestinationChangedListener() {
@Override
public void onDestinationChanged(@NonNull NavController controller, @NonNull NavDestination destination, @Nullable Bundle arguments) {
Log.d(TAG, "onDestinationChanged: 切换事件");
}
});
3.扩展延伸
NavigationUI 对 3 种类型的 App bar 提供了支持,以上代码一 ActionBar 为例,稍作修改,便可以支持另外两种 App bar。3 种 App bar:
ActionBar、Toolbar、CollapsingToolbarLayout
除了最常见的 menu 菜单,NavigationUI 还可以配合另外两种菜单使用。
App bar 左侧的抽屉菜单(DrawLayout + NavigationView)、底部菜单(ButtonNavigationView)
深层链接DeepLink
1.DeepLink 的两种应用场景
Navigation 组件还有一个非常重要和使用的特性 DeepLink,通过该特性,可以利用 PendingIntent 或一个真实的 URL 链接,直接跳转到应用程序中的某个页面(Activity/Fragment)
- PendingIntent 的方式。当应用程序接收到某个通知推送,你希望点击该通知时,能够直接跳转到展示该通知内容的页面,那么可以通过 PendingIntent 来完成此操作。
- URL 的方式。当用户通过手机浏览器网站上的某个页面时,可以在网页上放置一个类似于 “在应用内打开” 的按钮。如果用户的手机安装有对应的应用程序,那么通过 DeepLink 就能打开相应的页面;如果没有安装,那么网站可以导航到应用程序的下载页面,从而引导用户按钮应用程序
2.PendingIntent的方式
向通知栏发送一条通知,模拟用户收到一条推送的情况,当通知被点击时,系统会自动打开在 PendingIntent 中设置到的目的地
public class MainFragment extends Fragment {
private static final String CHANNEL_ID = "1";
private static final int notificationId = 8;
public MainFragment() {
// Required empty public constructor
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_main, container, false);
view.findViewById(R.id.btn_send_notification).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sendNotification();
}
});
return view;
}
/**
* 向通知栏发送一条通知,模拟用户收到一条推送的情况
*/
private void sendNotification() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
int importanceDefault = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "ChannleName", importanceDefault);
channel.setDescription("description");
NotificationManager notificationManager = getActivity().getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(getActivity(), CHANNEL_ID)
.setSmallIcon(R.drawable.ic_launcher_background)
.setContentTitle("DeepLinkDemo")
.setContentText("Test")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
// 设置 PendingIntent
.setContentIntent(getPendingIntent())
.setAutoCancel(true);
NotificationManagerCompat notificationManagerCompat = NotificationManagerCompat.from(getActivity());
notificationManagerCompat.notify(notificationId, builder.build());
}
/**
* 构建PendingIntent对象
* 在其中其中设置,当通知被点击时需要跳转到的目的地(destination),以及传递的参数
* @return PendingIntent
*/
private PendingIntent getPendingIntent() {
Bundle bundle = new Bundle();
bundle.putString("userName", "Michael");
bundle.putInt("age", 30);
return Navigation
.findNavController(requireActivity(), R.id.btn_send_notification)
.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.secondFragment)
.setArguments(bundle)
.createPendingIntent();
}
}
2.URL的方式
1.在导航图中为页面添加<deepLink/>标签。在 app:uri 属性中填入页面的相应Web地址,后面的参数会通过 Bundle 对象传递到页面中。
<fragment
android:id="@+id/secondFragment"
android:name="com.jinxin.navigation.SecondFragment"
android:label="fragment_second"
tools:layout="@layout/fragment_second" >
<!-- 添加参数 -->
<argument
android:name="userName"
app:argType="string"
android:defaultValue='"unknown'/>
<!-- 添加参数 -->
<argument
android:name="age"
app:argType="integer"
android:defaultValue="0"/>
<!-- 为destination 添加<deepLink/>标签 -->
<deepLink app:uri="test.deeplink.com/{userName}/{age}" />
</fragment>
2.为 Activity 设置<nav-graph/>标签。当用户在 Web 页面中访问Web地址时应用程序便能得到监听
<activity android:name=".NavigationActivity" >
<!-- 为Activity 设置<nav-graph/>标签-->
<nav-graph android:value="@navigation/nav_graph"/>
</activity>
3.模拟使用URL访问应用程序特定的界面
image.png总结
Navigation 组件为页面切换 和 App bar 的变化提供了统一的解决方案。配合Android Studio,可以通过图形化的方式管理配置页面切换,甚至加入动画效果。页面切换通常还会伴随着参数传递,Android Studio 提供了 safe args插件,通过该插件,可以以更安全的方式在页面间传递参数。对于 App bar 中的菜单,Jetpack 提供了 NavigationUI 组件,该组件使 App bar 中的菜单能够与页面切换对应起来。最后,通过 DeepLink,可以使用 PendingIntent 或 URL 的方式跳转到应用程序中的某个特定的页面。
网友评论