Navigation使用

Navigation组件,作为构建您的应用内界面的框架,重点是让单 Activity 应用成为首选架构。利用Navigation组件对Fragment的原生支持,您可以获得架构组件的所有好处(例如生命周期和 ViewModel),同时让此组件为您处理FragmentTransaction的复杂性。此外,Navigation组件还可以让您声明我们为您处理的转场。它可以自动构建正确的向上返回行为,包含对深层链接的完整支持,并提供了帮助程序,用于将导航关联到合适的UI小部件,例如抽屉式导航栏和底部导航。


了解Navigation

参考资料

资料 地址
官方文档 https://developer.android.com/topic/libraries/architecture/navigation/
Sample源码 https://github.com/googlecodelabs/android-navigation
使用教程 https://codelabs.developers.google.com/codelabs/android-navigation/#0

Sample展示

Navigation的sample,它最终的效果是这样:

安卓_Navigation使用_内容1.gif

这是3个简单的Fragment之间跳转的情景,经过转场动画的修饰,它们之前的切换非常流畅自然。在展示的最后,可以看到,Fragment2->Fragment1的时候,实际上是由用户点击手机Back键触发的。

项目结构图如下,可以帮助尽快了解sample的结构:

安卓_Navigation使用_内容2.png

该sample的源码,请点击这里查看


使用Navigation

Navigation目前仅AndroidStudio 3.2以上版本支持,如果您的版本不足3.2,请点此下载最新版Android Studio

首先介绍Navigation的使用:

  1. 在Module下的build.gradle中添加以下依赖
dependencies {
    def nav_version = "2.1.0"
    // 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"
}
  1. 新建三个Fragment
//3个Fragment,它们除了layout不同,没有其它区别
class MainPage1Fragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        return inflater.inflate(R.layout.fragment_main_page1, container, false)
    }
}

class MainPage2Fragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_main_page2, container, false)
    }
}

class MainPage3Fragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_main_page3, container, false)
    }
}
  1. 新建导航视图文件(nav_graph)

在res目录下新建navigation文件夹,然后新建一个navigation的resource文件nav_graph_main.xml

安卓_Navigation使用_内容3.png

打开导航视图文件,我们可以在AndroidStudio 3.2版本上,进行可视化编辑,包括选择新增Fragment,或者拖拽,连接Fragment:

安卓_Navigation使用_内容4.png
  1. 编辑导航视图文件

我们打开Text标签,进入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"
    app:startDestination="@id/page1Fragment">

    <fragment
        android:id="@+id/page1Fragment"
        android:name="com.qingmei2.samplejetpack.ui.main.MainPage1Fragment"
        android:label="fragment_page1"
        tools:layout="@layout/fragment_main_page1">
        <action
            android:id="@+id/action_page2"
            app:destination="@id/page2Fragment" />
    </fragment>

    <fragment
        android:id="@+id/page2Fragment"
        android:name="com.qingmei2.samplejetpack.ui.main.MainPage2Fragment"
        android:label="fragment_page2"
        tools:layout="@layout/fragment_main_page2">
        <action
            android:id="@+id/action_page1"
            app:popUpTo="@id/page1Fragment" />
        <action
            android:id="@+id/action_page3"
            app:destination="@id/nav_graph_page3" />
    </fragment>

    <navigation
        android:id="@+id/nav_graph_page3"
        app:startDestination="@id/page3Fragment">
        <fragment
            android:id="@+id/page3Fragment"
            android:name="com.qingmei2.samplejetpack.ui.main.MainPage3Fragment"
            android:label="fragment_page3"
            tools:layout="@layout/fragment_main_page3" />
    </navigation>
</navigation>

注意:请保证fragment标签下,android:name属性内包名的正确声明。

  1. 编辑MainActivity

在Activity中配置Navigation非常简单,我们首先编辑Activity的布局文件,并在布局文件中添加一个NavHostFragment :

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <fragment
        android:id="@+id/my_nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph_main" />
</android.support.constraint.ConstraintLayout>

这是一个宽和高都match_parent的Fragment,它的作用就是导航界面的容器

在Activity中通过Navigation展示一系列的Fragment,但需要告诉Navigation和Activity,这一系列的Fragment展示在哪——NavHostFragment应运而生,即它的作用归纳为导航界面的容器。在Activity中添加如下代码:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
    override fun onSupportNavigateUp() = findNavController(this, R.id.my_nav_host_fragment).navigateUp()
}

onSupportNavigateUp()方法的重写,意味着Activity将它的back键的点击事件委托出去,如果当前并非栈中顶部的Fragment,那么点击back键,返回上一个Fragment。

  1. 配置不同Fragment对应的跳转事件
class MainPage1Fragment : Fragment() {
     //隐藏了onCreateView()方法的实现,下同
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        btn.setOnClickListener {
            //点击跳转page2
            Navigation.findNavController(it).navigate(R.id.action_page2)
        }
    }
}

class MainPage2Fragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        btn.setOnClickListener {
           //点击返回page1
            Navigation.findNavController(it).navigateUp()
        }
        btn2.setOnClickListener {
            //点击跳转page3
            Navigation.findNavController(it).navigate(R.id.action_page3)
        }
    }
}

class MainPage3Fragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        //点击返回page2
        btn.setOnClickListener { Navigation.findNavController(it).navigateUp() }
    }
}

可以看到,对于Fragment 并非是通过原生的FragmentManagerFragmentTransaction进行控制的。而是通过以下API进行的控制:

  • Navigation.findNavController(params).navigateUp()
  • Navigation.findNavController(params).navigate(actionId)

到这里,Navigation最基本的使用已经讲解完毕了。您可以通过运行预览和示例基本一致的效果,如果遇到问题,或者有疑问,可以点我查看源码


理解Navigation

如果实现一个Fragment的导航库,需要实现以下内容

导航界面的容器:NavGraphFragment

展示一个Fragment,我们首先也需要定义一个容器承载它。以往,它可能是一个RelativeLayout或者FrameLayout,而现在,它被替换成了NavGraphFragment

实际上它做了什么呢?来看一下NavGraphFragment的onCreateView()方法:

public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                         @Nullable Bundle savedInstanceState) {
    FrameLayout frameLayout = new FrameLayout(inflater.getContext());
    frameLayout.setId(getId());
    return frameLayout;
}

NavGraphFragment内部实例化了一个FrameLayout, 作为ViewGroup的载体,导航并展示其它Fragment。除此之外,你在layout文件中,它还声明了另外两个属性:

app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph_main"

app:defaultNavHost="true"这个属性意味着你的NavGraphFragment将会拦截系统Back键的点击事件(因为系统的back键会直接关闭Activity而非切换Fragment),因此同时必须重写Activity的onSupportNavigateUp()方法,类似这样:

override fun onSupportNavigateUp() = findNavController(R.id.nav_host_fragment).navigateUp()

app:navGraph="@navigation/nav_graph_main"这个属性就很好理解了,它会指向一个navigation_graph的xml文件,这之后,NavGraphFragment就会导航并展示对应的Fragment

在我们使用Navigation的第一步,我们需要:在Activity的布局文件中显示声明NavGraphFragment,并配置app:defaultNavHost和app:navGraph属性

声明导航结构图:nav_graph.xml

NavGraphFragment作为Activity导航的容器 ,然后,其app:navGraph属性指向一个navigation_graph的xml文件,以声明其导航的结构。

NavGraphFragment在获取解析完这个xml资源文件后,它首先需要知道的是:类似APP的home界面,NavGraphFragment首先要导航到哪里?

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

    <fragment
        android:id="@+id/page1Fragment"
        android:name="com.qingmei2.samplejetpack.ui.main.MainPage1Fragment"
        android:label="fragment_page1"
        tools:layout="@layout/fragment_main_page1">
        <action
            android:id="@+id/action_page2"
            app:destination="@id/page2Fragment" />
    </fragment>
    ...
</navigation>

在navigation的根节点下,我们需要处理属性app:startDestination="@id/page1Fragment"Destination是一个很关键的单词,它的直译是目的地app:startDestination属性便是声明这个id对应的Destination会被作为默认布局加载到Activity中。这也就说明了,为什么我们的sample,默认会显示MainPage1Fragment

声明导航的行为:Action标签

前面声明了这样一个Action标签,这是一个导航的行为:

<action
    android:id="@+id/action_page2"
    app:destination="@id/page2Fragment" />

app:destination的属性,声明了这个行为导航的目的地,我们可以看到,它会指印跳转到id为page2Fragment的Fragment(也就是 MainPage2Fragment)。

android:id这个id作为Action唯一的标识,在Fragment的某个点击事件中,我们通过id指向对应的行为,就像这样:

btn.setOnClickListener {
       //点击跳转page2Fragment
       Navigation.findNavController(it).navigate(R.id.action_page2)
}

Navigation还提供了一个app:popUpTo属性,它的作用是声明导航行为返回到id对应的Fragment,比如直接从Page3返回到Page1。

此外,Navigation对导航行为还提供了转场动画的支持,它可以通过代码这样实现:

<action
        android:id="@+id/confirmationAction"
        app:destination="@id/confirmationFragment"
        app:enterAnim="@anim/slide_in_right"
        app:exitAnim="@anim/slide_out_left"
        app:popEnterAnim="@anim/slide_in_left"
        app:popExitAnim="@anim/slide_out_right" />

其实Navigation还提供了对Destination之间参数传递的支持,以及对SubNavigation标签的支持,以方便开发者在xml文件中复用fragment标签——甚至是对Deep Link的支持,但这些拓展功能本文不再叙述。

通过代码声明导航:Fragment

其实在3中我们已经讲解了导航代码的使用,我们以Page2为例,它包含了2个按钮,分别对应返回Page1和进入Page3两个事件:

btn.setOnClickListener {
    Navigation.findNavController(it).navigateUp()
}
btn2.setOnClickListener {
    Navigation.findNavController(it).navigate(R.id.action_page3)
}

Navigation.findNavController(View)返回了一个NavController,它是整个Navigation架构中最重要的核心类,我们所有的导航行为都由NavController处理。通过获取NavController,然后调用NavController.navigate()方法进行导航。

安卓_Navigation使用_内容5.png

我们更多情况下通过传入ActionId,指定对应的导航行为;同时可以通过传入Bundle以数据传递;或者是再传入一个NavOptions配置更多(比如转场动画,它也可以通过这种方式进行代码的动态配置)。

NavController.navigate()方法更多时候应用在向下导航或者指定向上导航(比如Page3直接返回Page1,跳过返回Page2的这一步);如果我们处理back事件,我们应该使用NavController.navigateUp()。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容