Android导航组件Navigation

相关类图

一、前言

传统应用开发,一般都是采用一个界面一个Activity的形式,但是大家都知道,Activity在Android中是属于重量级的组件,从而导致程序资源消耗大,用户体验不佳。而导航组件Navigation采用的是Fragment轻量级的组件实现的,可以节省资源,提高用户体验。

二、导航简介

导航组件是Android Jetpack的一部分,主要用途是实现用户导航、进入和退出应用中不同内容片段的交互,不论是普通按钮点击,还是应用栏、抽屉导航栏等复杂的模式,它都能轻松应对,当然,导航组件也有它既定的导航原则来确保一致且可预测的用户体验。

2.1 导航组件的组成

导航组件主要有三部分组成:

  • 导航图(NavGraph): 这是包含所有导航相关信息的XML资源,这些信息包括应用内所有内容区域个体(称为目标,一般都是Fragment),以及用户可以通过应用跳转的可能路径。
  • 导航宿主(NavHost):这是用来显示导航图中声明的目标的一个空白容器。导航组件包含一个默认的NavHost实现(NavHostFragment),可用来显示Fragment目标。
  • 导航控制器(NavController):在NavHost中管理应用导航的目标,当用户在应用中进行操作时,导航控制器会控制目标的切换。

使用导航组件有各种优势,包括以下方面:

  • 自动处理Fragment事务
  • 在默认情况下,能够正确处理往返操作
  • 支持动画和转场动画
  • 支持导航界面模式(例如:抽屉式导航栏和底部导航栏)
  • Safe Args支持(一种可在导航和目标之间传递数据时,提供类型安全的Gradle插件)
  • ViewModel支持
  • 可以使用Android Studio的Navigation Editor来编辑和查看导航图(必须使用Android Studio 3.3及以上版本)

2.2 导航的原则

在使用导航组件时,应当遵循一些原则,以提高用户体验

注意:即使您为在项目中使用Navigation组件,您的应用也应遵循这些设计原则

2.2.1 固定的起始目的地

顾名思义,您构建的应用必须有一个固定的起始目的地,这个起始目的地就是指当应用启动时显示的第一个屏幕。起始目的地也是用户按返回按钮后,在回到启动器前看到的最后一个屏幕。

以上示例中,用户登录页面就是起始目的地,点击启动器图标打开应用,第一个启动的页面就是用户登录页面,在返回过程中,最后一个呈现的页面也是登录页面。

2.2.2 导航状态表现为目的地堆栈

在用户启动应用时,系统会启动一个新任务,并且显示起始目的地,这个起始目的地是应用导航的基础。当用户在应用中进行导航时,栈顶的目标就是显示在屏幕上的,而栈内的所有目标都是历史记录。

导航组件会为你管理所有返回栈的顺序,当然你也可以自行管理,已达到某些目的。

2.2.3 在应用的任务中向上按钮和返回按钮行为相同

首先,说下什么事向上按钮,向上按钮是指在应用中的返回上一级的按钮(一般是在用户导航栏中),返回按钮则是系统导航中的返回按钮。在应用的任务中,向上按钮和返回按钮的行为相同,都是将栈顶的目标移除,返回到上一个目标。

2.2.4 向上按钮不会退出应用

在应用的任务中,向上按钮可以返回到上一个目标,但是绝不会退出应用

2.2.5 深度链接可以模拟手动导航

无论是通过深度链接至特定的目的地,还是手动导航到特定目的地,都可以使用向上按钮通过各个目的地导航回到起始目的地。当深度链接至特定的目的地时,会移除所有返回栈中的任务,并替换为深度链接的返回栈。值得注意的是,深度链接合成的返回栈是一个完整的返回栈,他跟手动导航至特定目的地具有相同的返回栈,这个是非常重要的,因为合成的返回栈必须是真实的。

三、 使用入门

3.1 添加依赖

在应用模块目录下的build.gradle文件中添加dependencies依赖声明:

dependencies {
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.appcompat:appcompat:1.3.0'
    implementation 'com.google.android.material:material:1.3.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'

    implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
    implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
    implementation 'androidx.navigation:navigation-dynamic-features-fragment:2.3.5'

    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
    androidTestImplementation 'androidx.navigation:navigation-testing:2.3.5'
}

3.2 创建导航图资源文件

导航是发生在各个目的地之间的,而这些目的地通过操作连接在一起。导航图是一种资源文件,它包含了所有的目的地和操作的声明。

创建导航图资源文件,可以按一下不走进行:

  1. 在项目程序模块下的res目录下,右键-》New-》Android Resource File
  2. 在弹出的窗口中输入文件名
  3. 在Resource type中选择Navigation
  4. 点击确定创建资源文件

新建的导航图资源文件时一个XML资源文件,以navigation为根节点,大致内容如下:

<?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/nav_graph">

</navigation>

<navigation>元素是导航图的根元素。当你向图表添加目的地和连接操作时,可以看到相应的<destination>和<action>子元素。如果有嵌套图表,也会出现<navigation>子元素。

如果是首次添加导航图资源,Android Studio会在res目录内创建一个navigation资源目录,该目录包含你的导航图资源文件。当然,如果你已经够熟练,可以直接创建目录和文件的方式创建。

3.2.1 Android Studio中的Navigation Editor

Android Studio提供了强大的导航编辑器,在这里不但可以预览你所添加的目标,还可以修改导航图,可以通过拖动的方式或者直接编码修改底层XML的方式修改导航图。为了方便项目的维护和代码可读性,但更加建议使用修改底层XML的方式,或者结合修改底层XML的方式。

温馨提示:不同版本的Android Studio的界面操作有些不一样,不少从旧版本升级到3.6之后,发现打开资源文件的时候,默认是Design模式(包括layout布局资源),一时间找不到北了,不知道如何切换成修改底层XML的模式。其实这是3.6版本之后的小改动,在旧版本只有code和design两种模式,新版本code、split、design三种模式,而且模式切换位置也变了,旧版本是在左下角,而新版本是在右上角(如下图)

3.3 向Activity添加导航宿主(HavHost)

导航宿主是导航组件的核心部分之一,导航宿主是一个空容器,用来存放和处理目的地。导航宿主必须派生于NavHost,NavHostFragment是导航组件的默认宿主实现,负责处理Fragment目的地的交互。

注意:导航组件的设计理念是用于具有一个主Activity和多个Fragment目的地的应用,主Activity与导航图相关联,并且包含一个负责根据需要交换目的地的HavHostFragment。如果你的应用需要再多个Activity上实现导航,就需要为每个Activity添加导航宿主,并在每个Activity关联其自己的导航图。

3.3.1 通过XML添加NavHostFragment

在主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"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingTop="?attr/actionBarSize">

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/nav_view"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="0dp"
        android:layout_marginEnd="0dp"
        android:background="?android:attr/windowBackground"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:menu="@menu/bottom_nav_menu" />

    <fragment
        android:id="@+id/nav_host_fragment_activity_main"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toTopOf="@id/nav_view"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/mobile_navigation" />

</androidx.constraintlayout.widget.ConstraintLayout>

详细解说:

  • android:name属性包含NavHost实现类的名称(示例中使用的是默认实现HAVHostFragment,如果有需要,可以使用自定义的Fragment类,但是必须实现HavHost或者继承NavHostFragment)
  • app:navGraph属性将导航宿主(NavHostFragment)与导航图关联,指向包含所有导航目的地的导航图资源文件
  • app:defaultNavHost=”true” 属性确保导航宿主会拦截系统返回按钮。请注意,只能有一个默认导航宿主,如果同一布局中有多个导航宿主,请务必仅指定一个默认导航宿主。

说明:导航组件是Android Jetpack的一部分,不属于Android系统组件,所以需要在布局中添加属性引入,如:xmlns:app="http://schemas.android.com/apk/res-auto"

除此之外,还可以使用Layout Editor向Activity添加导航宿主,具体步骤如下:

  1. 打开Activity布局文件,切换到Design窗口
  2. 在Palette窗口选择Containers,然后找到NavHostFragment(可直接搜索)
  3. 将NavHostFragment拖向布局中
  4. 在弹出的窗口中选择导航图,然后确定
  5. 在Properties窗口设置相关属性

3.3.2 向导航图中添加目的地

对于不想编码的小伙伴可以使用Navigation Editor向导航中添加目的地,因为这些都是用户引导模式的,没什么可说,在这里主要讲一下手动添加目的地的步骤:

  1. 新建Fragment类和布局文件,并实现相关逻辑代码
  2. 在导航图XML中新增<fragment>标签
  3. 配置<fragment>标签的相关属性,如:android:id、android:name、android:label等
<?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/mobile_navigation"
    app:startDestination="@+id/navigation_home">

    <fragment
        android:id="@+id/navigation_home"
        android:name="com.shijiusui.p.ppjoke.ui.home.HomeFragment"
        android:label="@string/title_home"
        tools:layout="@layout/fragment_home" />

    <fragment
        android:id="@+id/navigation_dashboard"
        android:name="com.shijiusui.p.ppjoke.ui.dashboard.DashboardFragment"
        android:label="@string/title_dashboard"
        tools:layout="@layout/fragment_dashboard" />

    <fragment
        android:id="@+id/navigation_notifications"
        android:name="com.shijiusui.p.ppjoke.ui.notifications.NotificationsFragment"
        android:label="@string/title_notifications"
        tools:layout="@layout/fragment_notifications" />
</navigation>

目的地属性详解

  • Type:即标签名称,指示在源代码中,该目的地是作为Fragment、Activity还是其他自定义实现的
  • android:label:这个属性指定目的地的名称
  • android:id:这个属性指定该目的地的ID,用于在代码中引用该目的地
  • android:name:这个属性用来指定目的地所关联的类

除此之外,还可以通过tools:layout属性指定预览布局文件,这样就可以在导航图编辑中看到对应的布局预览了。

3.3.3 将某个目的地指定为起始目的地

导航的原则之一就是固定的起始目的地,指定起始目的地的方法有两种:

一种是使用Navigation Editor,在Design窗口中,选中需要指定为起始目的地的目标,点击“房子”图标(如下图)即可。

另一种方法就是在XML源代码中,在<navigation>标签中添加app:startDestination属性进行指定,属性值为需要指定的目的地的ID(如下示例)

示例:通过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/mobile_navigation"
    app:startDestination="@+id/navigation_home">

    <fragment
        android:id="@+id/navigation_home"
        android:name="com.shijiusui.p.ppjoke.ui.home.HomeFragment"
        android:label="@string/title_home"
        tools:layout="@layout/fragment_home" />

    <fragment
        android:id="@+id/navigation_dashboard"
        android:name="com.shijiusui.p.ppjoke.ui.dashboard.DashboardFragment"
        android:label="@string/title_dashboard"
        tools:layout="@layout/fragment_dashboard" />

    <fragment
        android:id="@+id/navigation_notifications"
        android:name="com.shijiusui.p.ppjoke.ui.notifications.NotificationsFragment"
        android:label="@string/title_notifications"
        tools:layout="@layout/fragment_notifications" />
</navigation>

3.3.4 连接目的地

目的地之间的逻辑连接也叫做操作,操作一般是将一个目的地连接到另一个目的地,当然,你也可以定义全局操作,这类操作可以在任意位置跳转到指定目的地,这个在后面详细讲到。

我们可以使用Navigation Editor连接两个目的地,直接拖动箭头即可,在这里就不多介绍这种方式,直接介绍通过修改XML源码的方式(其实使用Navigation Editor也会自动修改XML源码),具体步骤如下:

  1. 在<fragment>标签内部新增<action>标签
  2. 配置android:id和app:destnation属性
  3. 如果需要,可以配置app:enterAnim、app:exitAnim、app:popEnterAnim、app:popExitAnim属性定义动画

详细解说:

  • Type:即<action>标签
  • android:id:这个字段是操作ID,代码中通过这个ID执行操作
  • app:destnation:这个字段是操作的目的地,用来指定操作跳转的目的地
<?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/mobile_navigation"
    app:startDestination="@+id/navigation_home">

    <fragment
        android:id="@+id/navigation_home"
        android:name="com.shijiusui.p.ppjoke.ui.home.HomeFragment"
        android:label="@string/title_home"
        tools:layout="@layout/fragment_home" >
        <action
            android:id="@+id/action_homeFragment_to_dashboard"
            app:destination="@+id/navigation_dashboard"
            app:enterAnim="@anim/nav_default_enter_anim"
            app:exitAnim="@anim/nav_default_exit_anim"
            app:popEnterAnim="@anim/nav_default_pop_enter_anim"
            app:popExitAnim="@anim/nav_default_pop_exit_anim" />
    </fragment>

    <fragment
        android:id="@+id/navigation_dashboard"
        android:name="com.shijiusui.p.ppjoke.ui.dashboard.DashboardFragment"
        android:label="@string/title_dashboard"
        tools:layout="@layout/fragment_dashboard" />

    <fragment
        android:id="@+id/navigation_notifications"
        android:name="com.shijiusui.p.ppjoke.ui.notifications.NotificationsFragment"
        android:label="@string/title_notifications"
        tools:layout="@layout/fragment_notifications" />
</navigation>

 

3.3.5 导航到目的地

完成了导航图的各种配置,那么就需要在代码中实现导航到目的地了。导航到目的地是使用NavController完成,这是在导航宿主中管理导航的对象的,每个导航宿主都有自己的相应导航控制器(NavController)。导航到目的地的步骤如下:

  1. 检索导航控制器
  2. 导航到目的地

3.3.5.1 检索导航控制器

检索导航宿主的导航控制器,可以通过以下方法:

Kotlin:

  • Fragment.findNavController()
  • View.findNavController()
  • Activity.findNavController(viewId: int)

Java:

  • NavHostFragment.findNavController(Fragment)
  • Navigation.findNavController(Activity, int viewId)
  • Navigation.findNavController(View)

说明:Kotlin可以直接在Fragment、View以及Activity使用findNavController是因为使用了扩展方法,当然,也可以直接跟Java那样调用对应的接口。

class HomeFragment : Fragment() {

    private lateinit var homeViewModel: HomeViewModel
    private var _binding: FragmentHomeBinding? = null

    // This property is only valid between onCreateView and
    // onDestroyView.
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        homeViewModel =
            ViewModelProvider(this).get(HomeViewModel::class.java)

        _binding = FragmentHomeBinding.inflate(inflater, container, false)
        val root: View = binding.root

        val textView: TextView = binding.textHome
        homeViewModel.text.observe(viewLifecycleOwner, Observer {
            textView.text = it
        })

        textView.setOnClickListener { 
            val navController = findNavController()
        }
        
        return root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

3.3.5.2 导航到目的地

检索到导航控制器之后,使用导航控制器类的NavController.navigation() API导航到指定的目的地,NavController.navigation()有多个变体,这里就以使用目的地ID进行导航为例:

class HomeFragment : Fragment() {

    private lateinit var homeViewModel: HomeViewModel
    private var _binding: FragmentHomeBinding? = null

    // This property is only valid between onCreateView and
    // onDestroyView.
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        homeViewModel =
            ViewModelProvider(this).get(HomeViewModel::class.java)

        _binding = FragmentHomeBinding.inflate(inflater, container, false)
        val root: View = binding.root

        val textView: TextView = binding.textHome
        homeViewModel.text.observe(viewLifecycleOwner, Observer {
            textView.text = it
        })

        textView.setOnClickListener { 
            val navController = findNavController()
            navController.navigate(R.id.action_homeFragment_to_dashboard)
        }
        
        return root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

3.3.5.3 返回到指定目的地

返回到指定目的地,是指返回到之前导航过的目的地,这些目的地必须是在任务栈内的,可以通过NavController.popBackStack()接口返回上一级,或者通过NavController.popBackStack(int destinationId, boolean inclusive)返回到指定某个目的地。

到这里,已经基本掌握了导航组件的使用,后续的将会进行更加深入地介绍导航组件的使用。

四、进阶之路

4.1 创建不同类型的导航目的地

在导航图中,导航目的地不局限与Fragment,其实还可以是Activity、DialogFragment甚至前台的导航图navigation(即<navigation>内部再嵌套一个<navigation>)。详细的添加导航目的地的方法已经在3.3.2 向导航图中添加目的地详细说明了,这里就不再累赘。

<?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/mobile_navigation"
    app:startDestination="@+id/navigation_home">

    <fragment
        android:id="@+id/navigation_home"
        android:name="com.shijiusui.p.ppjoke.ui.home.HomeFragment"
        android:label="@string/title_home"
        tools:layout="@layout/fragment_home" >
        <action
            android:id="@+id/action_homeFragment_to_dashboard"
            app:destination="@+id/navigation_dashboard"
            app:enterAnim="@anim/nav_default_enter_anim"
            app:exitAnim="@anim/nav_default_exit_anim"
            app:popEnterAnim="@anim/nav_default_pop_enter_anim"
            app:popExitAnim="@anim/nav_default_pop_exit_anim" />
    </fragment>
    
    <dialog 
        android:id="@+id/tips"
        ...
        />
    <activity 
        android:id="@+id/xxxx"
        ...
        />
</navigation>

注意:

  1. 如果目的地是Activity类型,转场动画必须结合Activity处理,仅仅在导航连接目的地中声明动画,弹出动画将达不到预期的效果(参考:将弹出动画应用与Activity目的地过度)
  2. 如果目的地是Activity类型,实际上就是已经离开了当前的导航组件范围
  3. 如果使用<dialog>声明导航目的地,必须是DialogFragment类型(包括其子类)

4.2 嵌套导航图

所谓的嵌套导航图,就是在导航图内再嵌入一个导航图,外部的称为父导航图,内部的叫子导航图。嵌套的导航图封装着自己的目的地,且必须标识起始目的地,父导航图访问子导航图只能通过子导航图的起始目的地(不能直接访问子导航图中的目的地),因为子导航图拥有不一样的导航控制器(NavController)。使用嵌套导航图可以对导航目的地进行分类封装,防止错误的访问。

嵌套导航图有两种表现形式,一种是在导航图<navigation>标签内部嵌套一个<navigation>标签;另一种是使用include标签引入导航图资源文件。

注意事项:

  1. 两种表现形式效果是一样的,如果导航图比较复杂,使用第二种会使得导航图资源显得更加简洁
  2. 父导航图中访问子导航图,不能直接访问子导航图中的目的地,只能通过子导航图ID访问子导航图的起始目的地

4.2.1 在导航图<navigation>标签内部嵌套一个<navigation>标签

<?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/mobile_navigation"
    app:startDestination="@+id/navigation_home">

    <navigation
        android:id="@+id/Settings"
        app:startDestination="@id/settingsFragment">
        ....
    </navigation>
</navigation>

4.2.2 使用include标签引入导航图资源文件

  • 定义一个导航图资源文件(nav_graph_settings.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/mobile_navigation"
    app:startDestination="@+id/settingsFragment">

    ...
    
</navigation>
  • 在主导航图中使用导航图资源
<?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/mobile_navigation"
    app:startDestination="@+id/navigation_home">
    
    <include app:graph="@navigation/nav_graph_settings" />

</navigation>

4.3 全局操作

操作就是目的地之间的跳转(<action>),全局操作就是指在导航图内所有目的地都能执行的操作。通常我们在目的地标签内部声明操作(<action>),但是全局操作是在导航图内声明(<navigation>标签下)。全局操作的使用跟普通的操作一样,不同的是它可以在当前导航图下所有的目的地内都可以使用。

注意事项:

  1. 全局操作只能再同一导航图内的目的地中调用,不可在所声明的导航图外部使用,即使是子导航图中的目的地也不允许
  2. 全局操作的目的地必须是当前导航图下的目的地或子导航图入口(子导航图中的目的地也是不允许的)
<?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/nav_graph">

    <!-- 
        全局操作,在navigation标签下声明
        目的地必须是当前导航图下的目的地或子导航图入口
     -->
    <action android:id="@+id/action_to_settings_more"
        app:destination="@+id/commonFragment"
        ...
        />

    <fragment
        android:id="@+id/commonFragment"
        ...
        />

</navigation>

4.4 在目的地之间传递数据

导航支持你通过定义目的地参数传递数据附加到导航操作,在不同目的地之间实现数据传递。

提示:建议仅在目的地之间传递最少量的数据,因为在Android上用于保存所有状态的总空间是有限的,如果需要传递大量数据,可以考虑其他替代方案。

4.4.1 定义参数

在导航图的操作中可以定义参数,在操作中定义的参数,有几个属性:

  • android:name: 参数名
  • android:defaultValue: 参数默认值
  • app:argType: 参数类型
  • app:nullable: 是否可为空
<?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_login"
    app:startDestination="@id/homeFragment">

    <fragment
        android:id="@+id/loginFragment"
        android:name="com.shijiusui.p.ppjoke.navigationdemo.LoginFragment"
        android:label="LoginFragment"
        tools:layout="@layout/fragment_login">
        <action
            android:id="@+id/action_loginFragment_to_registerFragment"
            app:destination="@+id/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"/>
        <argument
            android:name="type"
            app:argType="integer"
            android:defaultValue="0"
            app:nullable="false"/>
        <argument
            android:name="uname"
            app:argType="string"
            android:defaultValue="@null"
            app:nullable="true"/>

        <deepLink
            android:id="@+id/settingsDeepLink"
            app:uri="https://blog.shijiusui.com/?type={type}&amp;uname={uname}"
            android:autoVerify="false"/>
    </fragment>

</navigation>

导航库支持的参数类型包括:

类型 app:argType语法 是否支持默认值 是否支持null值
整数 app:argType=”integer”
浮点数 app:argType=”float”
长整数 app:argType=”long”
默认值必须始终以“L”后缀结尾
例如:“123L”
布尔值 app:argType=”boolean”
“true”或“false”
字符串 app:argType=”string”
资源引用 app:argType=”reference”
默认值必须为“@resourceType/resourceName”格式
例如:“@style/myCustomStyle”或“0”
自定义Parcelable app:argType=””
其中是Parcelable的完全限定类名称
支持默认值“@null”
不支持其他默认值
自定义Serializable app:argType=””
其中是Serializable的完全限定类名称
支持默认值“@null”
不支持其他默认值
自定义Enum app:argType=””
其中是Enum的完全限定名称

默认值必须与非限定名称匹配
例如:“SUCCESS”匹配MyEnum.SUCCESS

注意:

  1. 如果参数传递的是Parcelable、Serializable和Enum时,注意所传递的参数类型的类在代码混淆时需要做排除处理
  2. <argument>可以在<fragment>和<action>标签内,如果需要通过深层链接传参,务必配置在<fragment>标签内声明(更多信息参考:创建隐式深层链接)

4.4.2 在导航时传递参数

在导航时,也可以实现在目的地之间传递参数,只需要调用带有参数传递的NaviController.navigate()接口即可。

val navController = findNavController()
navController.navigate(R.id.action_homeFragment_to_loginFragment, Bundle().also {
    it.putInt("type", 2)
})

注意事项:

  1. 在导航图的操作中定义的参数,参数值是固定的,但是这个值可以被导航时传递的参数覆盖
  2. 导航的参数传递是单向的,无法实现往回传递,如果需要往回传递参数,可以通过目的地所有者Activity进行

4.5 在目的地之间添加动画效果

导航支持在目的地之间添加动画,以提高用户体验。导航动画在定义操作是添加,动画包括以下类型:

  • app:enterAnim: 进入目的地的动画(新的目的地进入的动画)
  • app:exitAnim: 退出目的地的动画(新的目的地进入,旧目的地退出的动画)
  • app:popEnterAnim: 通过弹出操作进入的目的地的动画(弹出操作时,进入的目的地进入的动画)
  • app:popExitAnim:通过弹出操作退出的目的地的动画(弹出操作时,退出的目的地退出的动画)
<?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_login"
    app:startDestination="@id/homeFragment">

    <fragment
        android:id="@+id/homeFragment"
        android:name="com.shijiusui.p.ppjoke.navigationdemo.HomeFragment"
        android:label="HomeFragment"
        tools:layout="@layout/fragment_home" >
        <action
            android:id="@+id/action_homeFragment_to_loginFragment"
            app:destination="@+id/loginFragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right" >
        </action>
    </fragment>

4.5.1 将弹出动画应用于Activity目的地过度

当导航的目的地是Activity类型时,,在操作添加的过度动画,会出现进入(打开Activity)时动画正常,但是返回时(从Activity中返回)的动画却失效了。针对目的地是Activity类型时,需要对弹出动画做特殊处理,你需要重写Activity的finish()方法,在内部调用ActivityNaviagtor.applyPopAnimationsToPendingTransition(Activity)接口即可。

    override fun finish() {
        super.finish()
        ActivityNavigator.applyPopAnimationsToPendingTransition(this)
    }

4.6 为目的地创建深层链接

在Android中,深层链接是指将用户直接转到应用内特定目的地的链接。借助导航组件,你可以创建两种不同类型的深层链接:显式深层链接和隐式深层链接。

4.6.1 创建显式深层链接

显式深层链接是深层链接的一个实例,该实例使用PendingIntent将用户转到应用内的特定位置(例如,可以在通知、应用快捷方式或应用微件中显式深层链接)。

当用户通过显式深层链接打开应用时,任务返回堆栈会被清除,并被替换为相应的深层链接目的地。当嵌套图表时,每个嵌套级别的起始目的地(即层次结构中每个<navigation>元素的起始目的地)也会添加到相应堆栈中。也就是说,当用户从深层链接目的地按下返回按钮时,就像从应用入口一步步进入到指定目的地的返回一样的效果。

创建显式深层链接,可以使用NavDeepLinkBuilder类构建PendingIntent,如下示例:

val pendingIntent = NavDeepLinkBuilder(requireContext())
    .setGraph(R.navigation.nav_graph)
    .setDestination(R.id.accountSettingFragment)
    .createPendingIntent()

如果已有NavController,则还可以通过NavController.createDeepLink() API创建深层链接,如下所示:

val pendingIntent = findNavController()
    .createDeepLink()
    .setGraph(R.navigation.nav_graph)
    .setDestination(R.id.accountSettingFragment)
    .createPendingIntent()

注意事项:

  1. 第一种创建显式深层链接的方式中,如果提供的上下文不是Activity,构造函数会使用PackageManager.getLaunchIntentForPackage()作为默认Activity来启动(如果有)
  2. 显式深层链接生成的对象时PendingIntent,适合的场景有通知、快捷方式启动、桌面小部件等

显式深层链接的使用示例(在通知中使用):

private fun showNotification(context: Context) {
    var notificationManager:NotificationManager
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        notificationManager = context.getSystemService<NotificationManager>(NotificationManager::class.java)
        if (null != notificationManager) {
            val channel = NotificationChannel("default", "default", NotificationManager.IMPORTANCE_DEFAULT
            )
            notificationManager.createNotificationChannel(channel)
        }
    } else {
        notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    }

    val pendingIntent = findNavController().createDeepLink()
        .setGraph(R.navigation.nav_graph)
        .setDestination(R.id.accountSettingFragment)
        .createPendingIntent()

    val builder: NotificationCompat.Builder = NotificationCompat.Builder(context, "default")
        .setSmallIcon(R.mipmap.ic_launcher)
        .setContentTitle("测试深层链接")
        .setContentText("测试显示深层链接打开应用")
        .setContentIntent(pendingIntent) //                .setVibrate(new long[] { 1000, 1000, 1000, 1000, 1000 })
        .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
        .setAutoCancel(true)


    notificationManager.notify(1, builder.build())
}

4.6.2 创建隐式深层链接

隐式深层链接是指应用中特定目的地的URI。调用URI(例如用户点击某个链接)时,Android可以将应用打开并自动导航到相应的目的地。

当用户触发隐式深层链接时,返回堆栈的状态取决于是否使用Intent.FLAG_ACTIVITY_NEW_TASK标记启动隐式Intent:

  • 如果改标记已设置,则任务返回堆栈会被清除,并被替换为相应的深层链接指定的目的地。就像显示深层链接,当嵌套图表时,每个嵌套级别的起始目的地(即层次结构中每个<navigation>元素的起始目的地)也会添加到相应堆栈中。也就是说,当用户从深层链接目的地按下返回按钮时,就像从应用入口一步步进入到指定目的地的返回一样的效果
  • 如果该标记未设置,则仍然位于上一个应用的任务堆栈中,该应用中的隐式深层链接已触发。在这种情况下,如果按下返回按钮,则会返回到上一个应用;如果按下向上按钮,则会在导航图中的层次父级目的地上启动应用的任务。

创建隐式深层链接请按下面步骤:

第一步:在导航图中添加隐式深层链接声明

在导航图中添加隐式深层链接声明,只需要再导航图内目的地中添加<deepLink>标签,配置相关属性:

<?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_login"
    app:startDestination="@id/homeFragment">
    
    <fragment
        android:id="@+id/loginFragment"
        android:name="com.shijiusui.p.ppjoke.navigationdemo.LoginFragment"
        android:label="LoginFragment"
        tools:layout="@layout/fragment_login">
        <action
            android:id="@+id/action_loginFragment_to_registerFragment"
            app:destination="@+id/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"/>
        <argument
            android:name="type"
            app:argType="integer"
            android:defaultValue="0"
            app:nullable="false"/>
        <argument
            android:name="uname"
            app:argType="string"
            android:defaultValue="@null"
            app:nullable="true"/>

        <deepLink
            android:id="@+id/settingsDeepLink"
            app:uri="https://blog.shijiusui.com/?type={type}&amp;uname={uname}"
            android:autoVerify="false"/>
    </fragment>

</navigation>

<deepLink>标签属性说明:

  • android:id:深层链接ID
  • app:uri:深层链接Uri
  • android:autoVerify:要求Google验证你是相应URI的所有者(可选),API 23开始有效

创建隐式深层链接需要注意的几点:

  • 没有协议的URI会假定为同时支持http和https。例如blog.shijiusui.com,会同时和http://blog.shijiusui.com与https://blog.shijiusui.com匹配
  • 深层链接的后缀中可以包含形式为{placeholder_name}的占位符,用来匹配一个或多个字符。例如,https://blog.shijiusui.com/users/{id}与https://blog.shijiusui.com/9匹配。导航件通过将占位符与深层链接所指向的目的地中已定义的参数相匹配,并尝试将占位符值解析为相应的类型。如果目的地中没有定义具有相同名称的参数,则使用默认的String类型传参数。
  • 在深层链接的后缀中,可以使用.*通配符匹配0个或多个字符
  • 如果目的地参数列表中定义了不能为null的参数,则深层链接必须包含该参数且不能为空,否则打开应用会有异常

第二步:启动导航图中的隐式深层链接

声明了隐式深层链接,接下来必须启用隐式深层链接,在应用的AndroidManifest.xml文件中,在导航图所关联的<activity>声明中添加<nav-graph>元素,如下所示:

<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>

    <!-- 启用导航图中的隐式深层链接 -->
    <nav-graph android:value="@navigation/nav_graph" />
</activity>

构建项目时,导航件会将<nav-graph>元素进行转换生成<intent-filter>元素,以匹配导航图中的所有深层链接。

注意事项:

  1. 启用隐式深层链接必须在导航图关联的Activity声明中进行
  2. <nav-graph>元素在Android Studio 3.0或3.1中不支持,使用这些版本时,必须改为手动添加intent-filter元素

第三步:测试隐式深层链接

隐式深层链接是URI,可以编写一个含有深层链接跳转的html文件到存储中,使用浏览器访问该html文件,如下所示:

<!DOCTYPE html>
<head>
    <meta charset="UTF-8" />
    <meta id="viewport" 
          name="viewport" 
          content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,minimal-ui">
</head>
<html>
    <input type="button" 
           value="点击我打开Deeplink" 
           onclick="javascrtpt:window.location.href='https://blog.shijiusui.com/?type=1&uname=sjs'">
</html>

将以上文件存储到手机存储中,在浏览器的地址中输入file:///<html_path>(<html_path>为html文件再存储中的路径,例如:file:///sdcard/test_deeplink.html),访问html之后(如下图),点击按钮就可以打开应用并进入到相应的目的地。

注意事项:

  1. 如果深层链接中使用的协议头在其他应用中也声明了,打开深层链接系统时,可能会弹出应用选择列表,这个问题可以在定义深层链接时,通过定义应用独有的协议头规避
  2. 如果导航组件自动生成的intent-filter无法正确进入到目的地,请确认深层链接是否跟自动生成的intent-filter一致,如果不一致,可以修改深层链接或采用手动添加intent-filter(如果遇到了https://blog.shijiusui.com的深层链接,生成的intent-filter中包含了android:path=”/” 导致无法正确访问到指定目的地,修改深层链接为 https://blog.shijiusui.com/,就能解决)。

手动添加intent-filter元素示例:

<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>

    <!-- 启用导航图中的隐式深层链接 -->
<!--    <nav-graph android:value="@navigation/nav_graph" />-->
    <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="https"
            android:host="blog.shijiusui.com"
            android:path="/" />
    </intent-filter>
</activity>

总结:

导航组件有非常大的优势,不但使用轻量级Fragment,还能在使用深层链接打开应用时,能自动构建返回栈(如果是隐式深层链接,启动务必使用Intent.FLAG_ACTIVITY_NEW_TASK)。

P1-Race walking

Race walking shares many fitness benefits with running, research shows, while most likely contributing to fewer injuries. It does, however, have its own problem.

翻译
尽管很可能竞走对身体的损伤更少,但是研究表明,竞走和跑步一样对健身有很多好处,然而它确实也有自身的问题

Race walkers are conditioned athletes. The longest track and field event at the Summer Olympics is the 50-kilometer race walk, which is about five miles longer than the marathon. But the sport’s rules require that a race walker’s knees stay straight through most of the leg swing and one foot remain in contact with the ground at all times. It’s this strange form that makes race walking such an attractive activity, however, says Jaclyn Norberg, an assistant professor of exercise science at Salem State University in Salem, Mass.

翻译
竞走运动员是受规则限制的运动员。夏季奥运会上最长的田径比赛项目是50公里竞走,比马拉松长约5英里。但这项运动的规则要求竞走运动员在几乎整个摆腿过程中让膝盖保持笔直,并且需要一只脚一直与地面保持接触状态。不过,麻省塞勒姆州立大学运动科学助理教授贾克林·诺伯格说,正是这种奇特的形式使得竞走成为一项如此吸引人的活动。

Like running, race walking is physically demanding, she says, According to most calculations, race walkers moving at a pace of six miles per hour would burn about 800 calories per hour, which is approximately twice as many as they would burn walking, although fewer than running, which would probably burn about 1,000 or more calories per hour.

翻译
她说,和跑步一样,竞走也需要体能。根据大多数的计算结果,竞走运动员以每小时6英里的速度竞走,每小时大约会消耗800卡路里,尽管比跑步的消耗要少,但却大约是走路时卡路里消耗量的两倍,跑步可能每小时消耗1000卡路里或更多。

However, race walking does not pound the body as much as running does, Dr. Norberg says. According to her research, runners hit the ground with as much as four times their body weight per step, while race walkers, who do not leave the ground, create only about 1.4 times their body weight with each step.

As a result, she says, some of the injuries associated with running, such as runner’s knee, are uncommon among race walkers. But the sport’s strange form does place considerable stress on the ankles and hips, so people with a history of such injuries might want to be cautious in adopting the sport. In fact, anyone wishing to try race walking should probably first consult a coach or experienced racer to learn proper technique, she says. It takes some practice.

继续阅读P1-Race walking