Android 官方架構(gòu)組件 Navigation 使用詳解,構(gòu)建一本Fragment的故事書赤屋!

前言

前段時(shí)間,我在做項(xiàng)目開發(fā)的時(shí)候?qū)ragment的管理遇到幾個(gè)小問題莺奔,總覺得在現(xiàn)階段封裝好的Fragment管理器不太優(yōu)雅令哟。這成為我下決心學(xué)習(xí)Jetpack在很早之前推出的Navigation庫,該庫的誕生就是為了能夠更加優(yōu)雅的管理Fragment狠半。在學(xué)習(xí)新知識(shí)時(shí)神年,我比較喜歡將我遇到的知識(shí)點(diǎn)與難點(diǎn)寫在紙上。但有時(shí)由于時(shí)間比較緊飘千,記在紙上的東西往往沒有那么的詳細(xì)與具體护奈。所以我決定以后通過以寫博客的方式對(duì)知識(shí)進(jìn)行一個(gè)歸納總結(jié),也希望能幫助到在看這篇博客的你奖慌。讓我們一起來完善Android的知識(shí)體系吧简僧!

image

使用條件

如果您要在 Android Studio 中使用 Navigation 組件,則必須使用 Android Studio 3.3 或更高版本啦逆。

添加依賴

要在您的項(xiàng)目中添加 Navigation 支持,請向應(yīng)用的 build.gradle 文件添加以下依賴項(xiàng):
dependencies {
    //...
    implementation "androidx.navigation:navigation-fragment-ktx:2.2.1"
    implementation "androidx.navigation:navigation-ui-ktx:2.2.1"
}

使用Navigation的具體流程

項(xiàng)目演示

image

新建三個(gè)Framgnet

在配置Navigation之前湿诊,我們創(chuàng)建三個(gè)Framgnet用于測試厅须。代碼如下所示:

//第一個(gè)Fragment
class Page1Fragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View? 
    {
        return inflater.inflate(R.layout.fragment_page_1, container, false)
    }
}
//第二個(gè)Framgnet
class Page2Fragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View? 
    {
        return inflater.inflate(R.layout.fragment_page_2, container, false)
    }
}
//第三個(gè)Fragment
class Page3Fragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View? 
    {
        return inflater.inflate(R.layout.fragment_page_3, container, false)
    }
}

配置導(dǎo)航

  • 我們需要在res文件夾下新建一個(gè)navigaton文件夾。
  • navigaton文件夾下創(chuàng)建一個(gè)navigation資源文件
  • 我們將它起名為mobile_navigaion.xml镀层。

如下圖所示:


image

<navigation>標(biāo)簽里可以嵌套另一個(gè)<navigation>標(biāo)簽,也可以新建一個(gè)navigation xml文件,通過<include>將其引進(jìn)來坞古。

在mobile_navigation.xml中添加上我們剛創(chuàng)建好的三個(gè)Fragment。

<fragment>標(biāo)簽下要有id奶陈,name吃粒,labellayout,這四個(gè)屬性一定要齊全!代碼如下所示:

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

    <fragment
        android:id="@+id/fragment_page_1_id"
        android:name="com.johnlion.navigation.Page1Fragment"
        android:label="fragment_page_1_label"
        tools:layout="@layout/fragment_page_1" />

    <fragment
        android:id="@+id/fragment_page_2_id"
        android:name="com.johnlion.navigation.Page2Fragment"
        android:label="fragment_page_2_label"
        tools:layout="@layout/fragment_page_2" />

    <fragment
        android:id="@+id/fragment_page_3_id"
        android:name="com.johnlion.navigation.Page3Fragment"
        android:label="fragment_page_1_label"
        tools:layout="@layout/fragment_page_3" />

</navigation>

此時(shí)我們剛添加完Fragment的mobile_navigation.xml中,<navigation>標(biāo)簽在android studio編譯器中會(huì)報(bào)紅色警告,這是由于我們并未在Activity布局文件中添加好NavHostFragment臀脏。

我們打開Activity布局文件秒啦,并在布局文件中添加一個(gè)NavHostFragment帝蒿。代碼如下所示:

<?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=".MainActivity">

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

</androidx.constraintlayout.widget.ConstraintLayout>

請注意以下幾點(diǎn):

在添加NavHostFragment的時(shí)候編譯器并未對(duì)NavHostFragment的文件路徑延塑、defaultNavHost與navGraph屬性進(jìn)行智能提示关带。請你不要慌,不要懷疑是否有這些屬性或值磨总,堅(jiān)定準(zhǔn)確的把它敲完或復(fù)制粘貼成功蚪燕!
  • android:name 屬性包含 NavHost 實(shí)現(xiàn)的類名稱。
  • app:navGraph 屬性將 NavHostFragment 與導(dǎo)航圖相關(guān)聯(lián)鲁驶。導(dǎo)航圖會(huì)在此 NavHostFragment 中指定用戶可以導(dǎo)航到的所有目的地。
  • app:defaultNavHost="true" 屬性確保您的 NavHostFragment 會(huì)攔截系統(tǒng)返回按鈕寿羞。請注意,只能有一個(gè)默認(rèn) NavHost玖院。如果同一布局(例如试溯,雙窗格布局)中有多個(gè)主機(jī)遇绞,請務(wù)必僅指定一個(gè)默認(rèn) NavHost。

NavHostFragment 簡單來講就是一個(gè)導(dǎo)航界面容器付鹿,用來展示導(dǎo)航中一系列的 Fragment。

NavHostFragment在Activity布局文件中添加成功后坐梯,mobile_navigation.xml中navigation標(biāo)簽的紅色警告消失了,取而代之的是一個(gè)黃色的提醒践瓷,這是由于我們并未在navigation的標(biāo)簽中添加一個(gè)“起始目的地"!即我們打開應(yīng)用的第一張頁面淋肾。在<navigation>標(biāo)簽中添加上:

app:startDestination="@id/fragment_page_1_id"

此時(shí)在Navigation Edit中,作為起始目的地的Fragment頂部就會(huì)多了一個(gè)小房子碌尔。如下圖所示:

image
這樣我們創(chuàng)建好的Page1Fragment就會(huì)變成我們打開應(yīng)用顯示的第一個(gè)屏幕。

“起始目的地”配置好之后熊镣,我們需要為每一個(gè)fragment配置其的“目的地”。配置“目的地”的方式有兩種毕箍,一種是在Navigation Edit中文捶,拖拉可視化Fragment的箭頭來指向它的“目的地”粹排;第二種是直接在xml中坠敷,在每個(gè)Fragment中添加<action>標(biāo)簽,并配置好其“目的地”限次。代碼如下所示:

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

    <fragment
        android:id="@+id/fragment_page_1_id"
        android:name="com.johnlion.navigation.Page1Fragment"
        android:label="fragment_page_1_label"
        tools:layout="@layout/fragment_page_1">
        <action
            android:id="@+id/action_page_1_to_page_2"
            app:destination="@id/fragment_page_2_id" />
    </fragment>

    <fragment
        android:id="@+id/fragment_page_2_id"
        android:name="com.johnlion.navigation.Page2Fragment"
        android:label="fragment_page_2_label"
        tools:layout="@layout/fragment_page_2">
        <action
            android:id="@+id/action_page_2_to_page_3"
            app:destination="@id/fragment_page_3_id" />
        <action
            android:id="@+id/action_page_2_to_page_1"
            app:popUpTo="@id/fragment_page_1_id" />
    </fragment>

    <fragment
        android:id="@+id/fragment_page_3_id"
        android:name="com.johnlion.navigation.Page3Fragment"
        android:label="fragment_page_1_label"
        tools:layout="@layout/fragment_page_3">
        <action
            android:id="@+id/action_page_3_to_page_2"
            app:popUpTo="@id/fragment_page_2_id" />
    </fragment>

</navigation>
應(yīng)用導(dǎo)航圖如下圖所示:
image

實(shí)現(xiàn)跳轉(zhuǎn)

接下來我們給每個(gè)Fragment配置好其對(duì)應(yīng)的跳轉(zhuǎn)事件羊始。代碼如下所示:

//第一個(gè)Fragment
class Page1Fragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View? 
    {
        return inflater.inflate(R.layout.fragment_page_1, container, false)
    }
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        btn_page1.setOnClickListener {
            Navigation.findNavController(it).navigate(R.id.action_page_1_to_page_2)
        }
    }
}
//第二個(gè)Fragment
class Page2Fragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View? 
    {
        return inflater.inflate(R.layout.fragment_page_2, container, false)
    }
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        btn_page2_1.setOnClickListener {
            Navigation.findNavController(it).navigateUp()
        }
        btn_page2_2.setOnClickListener {
            Navigation.findNavController(it).navigate(R.id.action_page_2_to_page_3)
        }
    }
}
//第三個(gè)Fragment
class Page3Fragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View? 
    {
        return inflater.inflate(R.layout.fragment_page_3, container, false)
    }
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        btn_page3.setOnClickListener {
            Navigation.findNavController(it).navigate(R.id.action_page_3_to_page_2)
        }
    }
}

從中我們可以看出Fragment之間的跳轉(zhuǎn)用到的API為:

  • Navigation.findNavController(view).navigate(actionID)
  • Navigation.findNavController(view).navigateUp()查描。

使用Bundle傳遞參數(shù)

代碼如下所示:

btn_bundle.setOnClickListener {
            val bundle = Bundle()
            bundle.putString("key", "value")
            Navigation.findNavController(it).navigate(R.id.action_page_1_to_page_2, bundle)
        }

通過創(chuàng)建一個(gè)Bundle()對(duì)象,把所要傳遞的key:value放進(jìn)bundle里面突委,然后我們在的navigate(...)方法中把bundle傳進(jìn)去速警,這樣我們就可以通過bundle來實(shí)現(xiàn)參數(shù)的傳遞了。

界面切換動(dòng)畫

我們可以在目的地之間添加上動(dòng)畫的過渡效果鸯两,使Fragment與Fragment之前切換不生硬。那么我們來嘗試做個(gè)右進(jìn)左出的動(dòng)畫吧钧唐。

我們先創(chuàng)建所需要的xml文件忙灼,在res目錄下新建一個(gè)anim文件夾,用來存放實(shí)現(xiàn)動(dòng)畫的xml文件钝侠,然后新建兩個(gè)動(dòng)畫資源文件该园,起名為slide_in_right.xmlslide_out_left.xml帅韧,代碼如下所示:

//slide_in_right.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="@android:integer/config_mediumAnimTime"
        android:fromXDelta="100%"
        android:interpolator="@android:anim/accelerate_interpolator"
        android:toXDelta="0" />
</set>

//slide_out_left
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="@android:integer/config_mediumAnimTime"
        android:fromXDelta="0"
        android:interpolator="@android:anim/accelerate_interpolator"
        android:toXDelta="-100%"/>
</set>

添加動(dòng)畫文件完成之后我們可以通過兩種方法來配置Fragment與Fragment之間跳轉(zhuǎn)的動(dòng)畫過渡里初,一種是在導(dǎo)航中的<action>標(biāo)簽下配置,另一種是通過代碼中的navOptions方法進(jìn)行配置忽舟,動(dòng)畫配置代碼如下所示:

//我們對(duì)一張F(tuán)ragment跳轉(zhuǎn)到第二張F(tuán)ragment進(jìn)行動(dòng)畫配置
//第一種:xml靜態(tài)配置
    <action
        android:id="@+id/action_page_1_to_page_2"
        app:destination="@id/fragment_page_2_id"
        app:enterAnim="@anim/slide_in_right"
        app:exitAnim="@anim/slide_out_left" />
            
//第二種:代碼動(dòng)態(tài)配置
    val option = navOptions {
        anim {
            enter = R.anim.slide_in_right
            exit = R.anim.slide_out_left
            }
    }
    btn_page1.setOnClickListener {
        Navigation.findNavController(it).navigate(R.id.action_page_1_to_page_2, null, option)
    }

在代碼中配置需要注意navigate(...)中第一個(gè)參數(shù)傳入的是actionID双妨,第二個(gè)參數(shù)傳入的是bundle對(duì)象,由于我們并沒有新建一個(gè)bundle叮阅,所以選擇傳入null刁品,第三個(gè)參數(shù)則是navOptions

這樣我們目的地與目的地之間的過渡動(dòng)畫就添加完成了~

image
到這里浩姥,通過運(yùn)行預(yù)覽挑随,基本和示例一樣效果了

使用 Safe Args 傳遞安全的數(shù)據(jù)

官方文檔原話:
Navigation 組件具有一個(gè)名為 Safe Args 的 Gradle 插件,該插件可以生成簡單的 object 和 builder 類勒叠,以便以類型安全的方式瀏覽和訪問任何關(guān)聯(lián)的參數(shù)兜挨。我們強(qiáng)烈建議您將 Safe Args 用于導(dǎo)航和數(shù)據(jù)傳遞,因?yàn)樗梢源_保類型安全眯分。

先配置安全插件拌汇。

//頂級(jí)的build.gradle
    buildscript {
        repositories {
            //...
            google()
        }
        dependencies {
            //...
            def nav_version = "2.1.0"
            classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
        }
    }
//應(yīng)用或模塊級(jí)的build.gradle
    //...
    apply plugin: 'com.android.application'
    apply plugin: "androidx.navigation.safeargs.kotlin"
    android{
        //...
        kotlinOptions {
            jvmTarget = JavaVersion.VERSION_1_8
        }   
    }

添加安全插件完成后,它會(huì)自動(dòng)生成末尾帶有Directions的類颗搂,該類的名稱是在源目的地的名稱后面加上“Directions”担猛,該類里面帶有原目的地action的方法。

image

在官方文檔中介紹丢氢,Navigation 庫支持以下參數(shù)類型:
image

那我們來挑上幾種類型嘗試一遍吧傅联。

首先我們需要在mobile_navigation.xml中添加上我們自定義的參數(shù),假如我們打算從Page1中把參數(shù)傳遞給Page2疚察,那么我們則需要在Page2(接收目的地)的<fragment>標(biāo)簽下添加上<argument>標(biāo)簽并添加上名字蒸走、默認(rèn)值類型三種屬性貌嫡。代碼如下所示:

<fragment
        android:id="@+id/fragment_page_2_id"
        android:name="com.johnlion.navigation.Page2Fragment"
        android:label="fragment_page_2_label"
        tools:layout="@layout/fragment_page_2">
        <!--
            ...
        -->
        <argument
            android:name="myInteger"
            android:defaultValue="0"
            app:argType="integer" />
        <argument
            android:name="myString"
            android:defaultValue="value"
            app:argType="string" />
        <argument
            android:name="myBoolean"
            android:defaultValue="false"
            app:argType="boolean" />
    </fragment>

添加完成后一定要reBuild一下項(xiàng)目比驻,這樣安全插件會(huì)為我們生成末尾帶有“Args”的類该溯,里面有我們接收參數(shù)目的地即Page2獲取參數(shù)的方法。

image

而且在Page1FragemntDirections類中别惦,用來實(shí)現(xiàn)action的方法在reBuild后會(huì)更新成傳遞三個(gè)默認(rèn)參數(shù)給Page2接收狈茉。代碼如下所示:

class Page1FragmentDirections private constructor() {
    //...
    companion object {
        fun actionPage1ToPage2(
            myInteger: Int = 0,
            myString: String = "value",
            myBoolean: Boolean = false
        ): NavDirections = ActionPage1ToPage2(myInteger, myString, myBoolean)
    }
}

接下來我們看下如何在代碼中傳遞安全的數(shù)據(jù)。代碼如下所示:

//Page1Fragment(傳遞安全數(shù)據(jù))
class Page1Fragment : Fragment() {
    //...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        btn_page1.setOnClickListener {
            val action = Page1FragmentDirections.actionPage1ToPage2(1, "hello", true)
            Navigation.findNavController(it).navigate(action)
        }
}
//Page2Fragment(接收安全數(shù)據(jù))
class Page2Fragment : Fragment() {
    val args: Page2FragmentArgs by navArgs()
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        //...
        //獲取的參數(shù)名為<argument>中配置好的name
        Log.d("data", "integer:" + args.myInteger)
        Log.d("data", "string:" + args.myString)
        Log.d("data", "boolean:" + args.myBoolean)
    }
}

如果我們在actionPage1ToPage2(...)方法里什么都不加的話掸掸,就會(huì)把在xml設(shè)置好的android:defaultValue傳遞過去氯庆。

從Page1傳遞Page2的參數(shù)要求不能為null,但如果參數(shù)類型支持 null 值扰付,那么可以在<argument>中堤撵,使用 android:defaultValue="@null"app:nullable="true"結(jié)合聲明默認(rèn)值 null。

總的來講羽莺,safe args給我的感覺就是实昨,通過其用來傳遞數(shù)據(jù),就是為了避免接收數(shù)據(jù)時(shí)會(huì)報(bào)空指針異常盐固!

使用 NavigationUI 更新界面組件

導(dǎo)航架構(gòu)組件包含 NavigationUI 類荒给。此類包含使用頂部應(yīng)用欄、抽屜式導(dǎo)航欄和底部導(dǎo)航欄管理導(dǎo)航的靜態(tài)方法闰挡。

NavigationUI支持以下類型控件:

  • Toolbar
  • CollapsingToolbarLayout
  • ActionBar
  • DrawerLayout
  • BottomNavigationView

我們選擇BottomNavigationView結(jié)合Navigation做一個(gè)示例:

  • 在res目錄下下新建一個(gè)menu文件夾锐墙。
  • 在menu文件夾中創(chuàng)建一個(gè)menu資源文件起名為:menu.xml礁哄。
  • 在此文件中添加上如下代碼:
注意:item中的id必須要與在mobile_navigation.xml中你要顯示的Fragment id一致
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/fragment_page_1_id"
        android:icon="@drawable/message"
        android:title="Page1" />
    <item
        android:id="@+id/fragment_page_2_id"
        android:icon="@drawable/search"
        android:title="Page2" />
    <item
        android:id="@+id/fragment_page_3_id"
        android:icon="@drawable/setting"
        android:title="Page3" />
</menu>

然后我們在Activity布局文件中添加BottomNavigationView控件长酗,并把剛剛新創(chuàng)建好的menu文件關(guān)聯(lián)其中。代碼如下所示:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    ...>
    <fragment
        android:id="@+id/nav_host_fragment"
        ... />
    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_navigation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:menu="@menu/menu" />
</androidx.constraintlayout.widget.ConstraintLayout>

最后我們在activity的代碼中需要先把NavController給取出來桐绒,再把它傳進(jìn)BottomNavigationView中的setupWithNavController(...)方法夺脾,而NavController是要在NavHostFragment里面取,所以第一步就是要把NavHostFragment找出來先茉继,然后取出NavController咧叭,最后把它傳遞到setupWithNavController(...)方法里就完成Navigation與BottomNavigationView的綁定了。代碼如下所示:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val host: NavHostFragment = supportFragmentManager
            .findFragmentById(R.id.nav_host_fragment) as NavHostFragment? ?: return
        val navController = host.navController
        setupBottomNavMenu(navController)
    }
    private fun setupBottomNavMenu(navController: NavController) {
        val bottomNav = findViewById<BottomNavigationView>(R.id.bottom_navigation)
        bottomNav?.setupWithNavController(navController)
    }
}

演示效果如下:

image

但是烁竭!看演示的效果會(huì)發(fā)現(xiàn)菲茬,我們依次點(diǎn)擊BottomNavigationView上面的item,Page2 -> Page3派撕,然后停留在Page3中點(diǎn)擊真機(jī)上面的back按鍵則會(huì)先跳回到Page1婉弹,再點(diǎn)一次才退出應(yīng)用,再試幾遍才發(fā)現(xiàn)只要你不是停留在Page1中點(diǎn)擊back按鍵都會(huì)先返回到Page1中终吼,再點(diǎn)擊一次back按鍵才退出應(yīng)用镀赌。

我們要的效果是:只要點(diǎn)擊BottomNavigation選擇的Fragment,返回棧中只留它一個(gè)际跪,再點(diǎn)擊back按鈕就可退出應(yīng)用商佛。
image

定位問題:

  • 進(jìn)入bottomNav?.setupWithNavController(navController)方法里喉钢。
  • 發(fā)現(xiàn)里面只有一個(gè)NavigationUI.setupWithNavController(this, navController)方法,并進(jìn)入該方法良姆。
  • 里面有一個(gè)對(duì)BottomNavigationView實(shí)現(xiàn)的監(jiān)聽setOnNavigationItemSelectedListener里返回了一個(gè)onNavDestinationSelected(item, navController);肠虽,這個(gè)方法是用來關(guān)聯(lián)Navigation與BottomNavigationView的MenuItem的。
  • 問題就是出自這個(gè)方法里的setPopUpTo(int destinationId, boolean inclusive)玛追,源碼如下所示:
 public static boolean onNavDestinationSelected(@NonNull MenuItem item,@NonNull NavController navController) {
        NavOptions.Builder builder = new NavOptions.Builder()
        //...
        if ((item.getOrder() & Menu.CATEGORY_SECONDARY) == 0) {
            builder.setPopUpTo(findStartDestination(navController.getGraph()).getId(), false);
        }
        NavOptions options = builder.build();
        try {
            navController.navigate(item.getItemId(), null, options);
            return true;
        } catch (IllegalArgumentException e) {
            return false;
        }
    }

當(dāng)我們每次點(diǎn)擊BottomNavigationView的Item的時(shí)候舔痕,都會(huì)走setPopUpTo(...)方法,由于里面只指定“初始目的地”的id豹缀,所以每次都會(huì)彈出在其之上的目的地伯复,且inclusive="false",這樣“初始目的地”在棧中得到保留邢笙,并沒有被移除啸如!

這就是為什么:只要你不是停留在Page1中點(diǎn)擊back按鍵都會(huì)先返回到Page1!

image

解決問題:

  • 問題出自一個(gè)叫onNavDestinationSelected(item, navController);的方法里氮惯。
  • 該方法里面只實(shí)現(xiàn)了導(dǎo)航跳轉(zhuǎn)目的地叮雳,與設(shè)置跳轉(zhuǎn)時(shí)的進(jìn)出動(dòng)畫和目的地的啟動(dòng)模式
  • 此方法在BottomNavigationView的setOnNavigationItemSelectedListener監(jiān)聽里面妇汗。
  • 該監(jiān)聽只實(shí)現(xiàn)onNavDestinationSelected(item, navController)一個(gè)方法帘不!

那我們可以試著重寫BottomNavigationView的setOnNavigationItemSelectedListener監(jiān)聽,照著onNavDestinationSelected(...)方法里面的內(nèi)容杨箭,修改成我們需要的效果不就行了寞焙!

activity代碼如下所示:

class MainActivity : AppCompatActivity() {
    //...
    private fun setupBottomNavMenu(navController: NavController) {
        val bottomNav = findViewById<BottomNavigationView>(R.id.bottom_navigation)
        bottomNav?.setupWithNavController(navController)
        //重寫監(jiān)聽
        bottomNav.setOnNavigationItemSelectedListener { item: MenuItem ->
            val options = NavOptions.Builder()
                //從放回棧中移除指定目的地
                .setPopUpTo(navController.currentDestination!!.id, true)
                .setLaunchSingleTop(true)
                .build()
            try {
                //TODO provide proper API instead of using Exceptions as Control-Flow.
                navController.navigate(item.itemId, null, options)
                true
            } catch (e: IllegalArgumentException) {
                false
            }
        }
    }
}

演示效果如下:

image
完成!測試結(jié)果與我們想要的效果一致互婿!

動(dòng)態(tài)加載Navigation

先把Activity布局文件中的app:navGraph="@navigation/mobile_navigation"去掉先捣郊。代碼如下所示:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    ...>
    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
    />
</androidx.constraintlayout.widget.ConstraintLayout>

接著在Activity文件中,通過navController將Navigation的xml文件給inflater出來慈参,并設(shè)置進(jìn)navControllergraph中呛牲。代碼如下所示:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val host: NavHostFragment = supportFragmentManager
            .findFragmentById(R.id.nav_host_fragment) as NavHostFragment? ?: return
        val navController = host.navController
        val navGraph: NavGraph =
            navController.navInflater.inflate(R.navigation.mobile_navigation)
        navController.graph = navGraph
        //...
    }
    //...
}

測試一遍,動(dòng)態(tài)加載成功驮配!

清空返回棧

假如我們打算從Page2跳到Page3時(shí)先把返回棧清空娘扩,然后跳轉(zhuǎn)到Page3,這時(shí)返回棧應(yīng)該就只有一個(gè)Page3的實(shí)例壮锻,當(dāng)我們點(diǎn)擊back'按鍵后琐旁,直接退出應(yīng)用而不是放回Page2。

這是我在stackoverflow上找到了清空navigation返回棧的方法躯保。

實(shí)現(xiàn)代碼如下所示:

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    ...>
        <!--
            ...
        -->
    <fragment
        android:id="@+id/fragment_page_2_id"
        ...>
        <action
            android:id="@+id/action_page_2_to_page_3"
            app:destination="@id/fragment_page_3_id"
            app:launchSingleTop="true"
            app:popUpTo="@+id/mobile_navigation"
            app:popUpToInclusive="true" />
        <!--
            ...
        -->
    </fragment>
</navigation>

在Page2跳轉(zhuǎn)到Page3的<action>標(biāo)簽下添加上app:launchSingleTop="true"旋膳、app:popUpTo="@+id/mobile_navigation"app:popUpToInclusive="true"就能實(shí)現(xiàn)先清空放回棧然后跳轉(zhuǎn)

我們根據(jù)xml中添加的這些屬性嘗試在代碼中實(shí)現(xiàn)途事。代碼如下所示:

class Page2Fragment : Fragment() {
    //...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        //...
        btn_page2_2.setOnClickListener {
            val navOption = NavOptions.Builder()
                //將xml中三個(gè)屬性設(shè)置進(jìn)去验懊,就能實(shí)現(xiàn)先清棧后跳轉(zhuǎn)
                .setLaunchSingleTop(true)
                .setPopUpTo(R.id.mobile_navigation, true)
                .build()
            Navigation.findNavController(it).navigate(R.id.action_page_2_to_page_3, null, navOption)
        }
    }
}
最后在Activity中實(shí)現(xiàn)一個(gè)全局清棧功能擅羞。

代碼很簡單,取出NavController义图,然后在調(diào)用navController.navigate(xxx)方法前調(diào)用navController.popBackStack(R.id.mobile_navigation, true)方法就能實(shí)現(xiàn)清棧减俏。代碼如下所示:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val host: NavHostFragment = supportFragmentManager
            .findFragmentById(R.id.nav_host_fragment) as NavHostFragment? ?: return
        val navController = host.navController
        setupBottomNavMenu(navController)
    }
    private fun clearStack(navController: NavController) {
        navController.popBackStack(R.id.mobile_navigation, true)
    }
    //...
}

當(dāng)然“目的地”之間的跳轉(zhuǎn),應(yīng)該統(tǒng)一放在activity里面管理碱工,這里就不詳細(xì)說明了娃承。

只要拿到了navController,就能調(diào)用navController.navigate(xxx)進(jìn)行“目的地”跳轉(zhuǎn)怕篷。

總結(jié)

Android JetPack推出的Navigation架構(gòu)組件历筝,用來作為構(gòu)建應(yīng)用內(nèi)界面的框架,其重點(diǎn)是讓單Activity應(yīng)用成為首選架構(gòu)廊谓。此控件處理了FragmentTransaction 的復(fù)雜性梳猪,并提供了幫助程序,用于將導(dǎo)航關(guān)聯(lián)到合適的 UI 小部件蒸痹,例如抽屜式導(dǎo)航欄和底部導(dǎo)航春弥。

image

項(xiàng)目鏈接

示例:android-navigation

參考文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市叠荠,隨后出現(xiàn)的幾起案子匿沛,更是在濱河造成了極大的恐慌,老刑警劉巖榛鼎,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件逃呼,死亡現(xiàn)場離奇詭異,居然都是意外死亡借帘,警方通過查閱死者的電腦和手機(jī)蜘渣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肺然,“玉大人,你說我怎么就攤上這事腿准〖势穑” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵吐葱,是天一觀的道長街望。 經(jīng)常有香客問我,道長弟跑,這世上最難降的妖魔是什么灾前? 我笑而不...
    開封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮孟辑,結(jié)果婚禮上哎甲,老公的妹妹穿的比我還像新娘蔫敲。我一直安慰自己,他們只是感情好炭玫,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開白布奈嘿。 她就那樣靜靜地躺著,像睡著了一般吞加。 火紅的嫁衣襯著肌膚如雪裙犹。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天衔憨,我揣著相機(jī)與錄音叶圃,去河邊找鬼。 笑死践图,一個(gè)胖子當(dāng)著我的面吹牛盗似,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播平项,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼赫舒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了闽瓢?” 一聲冷哼從身側(cè)響起接癌,我...
    開封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎扣讼,沒想到半個(gè)月后缺猛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡椭符,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年荔燎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片销钝。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡有咨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蒸健,到底是詐尸還是另有隱情座享,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布似忧,位于F島的核電站渣叛,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏盯捌。R本人自食惡果不足惜淳衙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧箫攀,春花似錦肠牲、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至汤求,卻和暖如春俏险,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背扬绪。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來泰國打工竖独, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人挤牛。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓莹痢,卻偏偏與公主長得像,于是被迫代替她去往敵國和親墓赴。 傳聞我的和親對(duì)象是個(gè)殘疾皇子竞膳,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容