本文首發于個人博客,已授權郭霖轉載至其公眾號:guolin_blog
Navigation 是一個谷歌官方推出的一個用于 APP 內部便捷切換內容(Fragment 或 Activity)的庫。從而使得 APP 內的頁面跳轉更簡單。
我知道它的時候它的版本已經是 2.0.0 了,也是時候來學習一波了。
無論什么時候,學習的第一手資料不能缺了官方出品的 CodeLab。相信你,看了 CodeLab 之后就能對 Navigation 有一個簡單的了解。本人也是對 CodeLab 學習之后才寫下了這篇博客,主要內容都能在 CodeLab 上找到。不過 CodeLab 里面是英文的講解,而且其中的代碼是使用 Kotlin 編寫的,這篇博客是以 Java 代碼的方式進行的。
還一件事情,Navigation 的原生支持是從 Android Studio 3.3 開始的,3.2 版本的需要在設置面板的 Experimental 模塊中啟用 Navigation 編輯器。
圖片來自 CodeLab。
下面開始正題
Navigation Graph 和 NavHostFragment
首先,添加依賴。
implementation 'androidx.navigation:navigation-fragment:2.0.0'
implementation 'androidx.navigation:navigation-ui:2.0.0'
之后,在 res 文件夾下創建類型為 navigation 的資源文件夾,Android Studio 會自動在這個文件夾下生產一個名為 navigation.xml 的文件,這個文件的作用就是用于描述 Fragment 及相應的跳轉邏輯、動畫、參數等信息。這個文件也叫做 Navigation Graph。
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/navigation">
</navigation>
默認的 Navigation Graph 文件就只有一個根節點,如果我們有更多的 Fragment,添加進來,會有不同的子節點,子節點代表的就是 Fragment,fragment 節點中描述關于 Fragment 的相關信息,并且在 fragment 節點中還可以其他子節點,比如,action、argument、deepLink。他們分別用于表示 Fragment 的相關信息。往后會講到的?,F在我們現在創建一個 Fragment ,就叫 RootFragment 好了。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragment.RootFragment">
<TextView
android:layout_gravity="center"
android:gravity="center"
android:layout_width="match_parent"
android:textSize="24sp"
android:layout_height="match_parent"
android:text="Root Fragment"/>
</FrameLayout>
只是在頁面上顯示出這個 Fragment 的名字,Java 代碼中沒有做任何事情?,F在讓我們回到 Navigation Graph 中,我們是初學者,不知道或者說不了解 Fragment 節點有哪些屬性可以去使用,可以使用 Navigation Graph 的圖形化界面,剛才我們看了 Navigation Graph 的代碼,現在來看一下,圖形化編輯頁面。
左邊區域:是已經添加進來的 Fragment 以及承載這些 Fragment 的頁面;
中間區域:Fragment 的跳轉示意圖;
右邊區域:是當前選中的 Fragment 的屬性展示區;
頁面中間已經提示我們了,點擊那個圖標,添加一個目標。試試看吧,從 Android Studio 展示出的列表中,找到我們剛才創建的 RootFragment。這時,頁面已經發生了變化。我們剛才創建的 RootFragment 的樣子已經出現了,而且名稱前還有一個小圖標,這表示 RootFragment 是 Navigation 管理頁面的第一個頁面也是開始頁面。
頁面右側出現了一些屬性,我們暫時可以不用管,現在我只想先運行起來,看看效果。不過在這之前,我們還需要改造一下,之前新建項目自動生成的 MainActivity。先打開 activity_main.xml 的圖形化編輯頁面,然后在 Palette 類型列表中找到 NavHostFragment 并拖拽到頁面上,此時會彈出一個框,讓你選擇 Navigation Graph,我們選擇剛才自動創建的文件即可。
Android Studio 的布局文件的拖拽,不是太好用,需要手動切換到源代碼形式,簡單改動一下頁面代碼,我們讓這個 NavHostFrgament 組件填充滿整個容器即可。
最終的 activity_main.xml 的源文件如下:
<?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/fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
然后運行項目即可。這就是一個最簡單的使用 Navigation 的例子,而且其中根本就沒什么難度。
好,現在我們來回過頭來看看,剛剛我們都做什么。我們真正有效的內容是從把 RootFragment 添加到 Navigation Graph 中,我們去看一下,Navigation Graph 的源代碼。說不定能從那里發現點什么東西。
<?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/navigation"
app:startDestination="@id/rootFragment">
<fragment
android:id="@+id/rootFragment"
android:name="me.monster.blogtest.fragment.RootFragment"
android:label="fragment_root"
tools:layout="@layout/fragment_root" />
</navigation>
這個文件跟之前自動生成的沒什么區別,無非就是多了一個 fragment 節點,以及根節點上多了一個 startDestination 屬性。難道就是因為這個屬性?是的,沒錯,在 Navigation 中我們使用 Destination(目標)來描述 Fragment 之間的跳轉關系。這里的 startDestination 代表的就是這個是 Navigation 整個頁面跳轉管理棧的最根級頁面。
再來看看那個添加到 MainActivity 頁面的 NavHostFragment 組件。它其實就是一個布局文件中的 fragmen 組件,跟我們正常使用的沒什么不同,非要說不同,那就是其中的 name、defaultNavHost 以及 navGraph 這三個屬性了。
name 屬性我們都知道,navGraph 屬性里面的值是剛才創建 Navigation Graph,猜一下,就是把 Navigation Graph 引用到了這個 NavHostFragment 中。那最后一個 defaultNavHost 屬性呢?那就是攔截系統返回按鈕的點擊事件的。
<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/navigation" />
NavController
單單一個 Fragment 沒啥意思,不好玩,這回我們再加一個頁面(SettingsFragment)。嘗試著從 RootFragment 頁面點擊按鈕切換到 SettingsFragment 頁面。然后在 SettingsFragment 頁面點擊按鈕返回到 RootFragment 頁面。
說是 SettingsFragment,里面就一個 Button 一個 TextView,布局代碼就不貼了。
Fragment 準備好了,該往 Navigation Graph 里添加了,按照剛才添加 RootFragment 的方式再來一次,不過,這次比上次多一步。選中 RootFragment,點擊 RootFragment 右邊的小圓點然后牽引到右側的 SettingsFragment。這樣他們兩個就建立一種關系。
來看一下源代碼吧。我們發現,除了增加了一個 fragment 節點之外,原來的 RootFragment 的節點上還增加了一個子節點 action 。事實上,action 節點就是用來描述 Fragmen 之間的頁面跳轉的關系的,其中 destination 屬性的值就是目標 fragment 的 id。
<fragment
android:id="@+id/rootFragment"
android:name="me.monster.blogtest.fragment.RootFragment"
android:label="fragment_root"
tools:layout="@layout/fragment_root" />
<!--上面是原來的代碼,下面是新代碼-->
<fragment
android:id="@+id/rootFragment"
android:name="me.monster.blogtest.fragment.RootFragment"
android:label="fragment_root"
tools:layout="@layout/fragment_root" >
<action
android:id="@+id/action_rootFragment_to_settingsFragment2"
app:destination="@id/settingsFragment" />
</fragment>
<fragment
android:id="@+id/settingsFragment"
android:name="me.monster.blogtest.fragment.SettingsFragment"
android:label="SettingsFragment" />
繼續往下,我們為 RootFragment 頁面綁定點擊事件。
private void toSettings() {
btnToSettings.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Navigation.findNavController(btnToSettings)
.navigate(R.id.action_rootFragment_to_settingsFragment);
}
});
}
這一看就知道了,通過 Navigation 找到一個叫 NavController 的東西,然后執行 navigate 方法,這個方法里面傳的值就是剛才 RootFragment 子節點 action 的 id 的值。先運行一下看看效果。
- 親測點擊按鈕能跳轉到 SettingsFragment 頁面。下面的 Gif 動圖只是表示能從 RootFragment 到 SettingsFragment,閃回到 RootFragment 頁面只是 Gif 的重新播放。
- 如果你在 SettingsFragment 點擊系統的返回鍵,是能返回到 RootFragment。這就是 MainActivity 中 NavHostFragment 組件的屬性
app:defaultNavHost="true"
起到的作用,有興趣的話,可以改成 false 然后再試一下效果。
現在,讓我們再次為 SettingsFragment 添加按鈕的點擊事件吧。
private void goBack() {
btnToRoot.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Navigation.findNavController(btnToRoot)
.popBackStack();
}
});
}
和之前跳轉到這個頁面的方式差不多,只是最后執行的方法變成了 popBackStack
。
嗯,挺好的,不過,我們有些時候需要在兩個 Fragment 之間做切換動畫,這個怎么辦?這個也不難,在Navigation Graph 中跳轉的 action 內增加屬性即可。吶,這樣就行了,而且還可以用過 Java 代碼來實現。
<fragment
android:id="@+id/rootFragment"
android:name="me.monster.blogtest.fragment.RootFragment"
android:label="fragment_root"
tools:layout="@layout/fragment_root">
<action
android:id="@+id/action_rootFragment_to_settingsFragment"
app:destination="@id/settingsFragment"
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>
Java 代碼
Navigation.findNavController(btnToSettings)
.navigate(R.id.action_rootFragment_to_settingsFragment);
// 上面是原來的代碼,下面是新代碼
NavOptions options = new NavOptions.Builder()
.setEnterAnim(R.anim.slide_in_right)
.setExitAnim(R.anim.slide_out_left)
.setPopEnterAnim(R.anim.slide_in_left)
.setPopExitAnim(R.anim.slide_out_right)
.build();
Navigation.findNavController(btnToSettings)
.navigate(R.id.action_rootFragment_to_settingsFragment, null, options);
這里,我們調用了 navigate 這個方法,其中第二個參數是 Bundle 類型,我們填入了 null
,那如果正常填了值,Bundle 是不是就是傳遞到 SettingsFragment 了呢?答案是肯定的。不過 Navigation 還有另一種方式來傳值—— Safe Args。
Safe Args
為啥要用 Safe Args 呢?
我也不知道為啥學,感覺如果單純為了保證 key 安全的話,把 Bundle 里面的 key 抽取成常量值不也行嗎?不太懂為啥要通過這種形式來做,不過呢,老話說得好,技多不壓身。
Safe Args 是配合 Navigation 使用的一個 Gradle 插件。首先你得先去配置:
首先在你項目的根目錄的 build.gradle 文件中加上這些東西:
repositories {
google()
}
dependencies {
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.0.0"
}
然后還得在你的 app 或是 module 的目錄下的 build.gradle 文件夾加入:
apply plugin: "androidx.navigation.safeargs"
如果你想用 safe Args 生成的代碼時 Kotlin 的話,還需要加入:
aapply plugin: "androidx.navigation.safeargs.kotlin"
最最最重要的一點是,你要確認你的 build.properties 文件中有這么一行:
android.useAndroidX=true
當然了,如果你的項目本身就是用是 AndroidX 的依賴,就不用去確認了,肯定能通過的嘛。
現在我們就來從 RootFragmet 傳遞一個類型為 String 的備注名到 SettingsFragmen 吧。還是先通過圖形化界面進行設置吧,選中 SettingsFragment,然后再右側屬性面板上找到 Argments 點擊旁邊的?。
彈出一個框,我們填入一下信息,然后點擊 add。
完成之后的 Navigation Graph 中 SettingsFragment 節點的內容變了。
<fragment
android:id="@+id/settingsFragment"
android:name="me.monster.blogtest.fragment.SettingsFragment"
android:label="SettingsFragment"/>
<!--上面是原來的代碼,下面是新代碼-->
<fragment
android:id="@+id/settingsFragment"
android:name="me.monster.blogtest.fragment.SettingsFragment"
android:label="SettingsFragment">
<argument
android:name="nickName"
android:defaultValue="未設置"
app:argType="string"
app:nullable="true" />
</fragment>
這個時候,Gradle 會自動生成 SettingFragmentArgs 以及 RootFragmentDirections 這兩個類,在 generatedJava 這個文件夾下的包內。如果沒有自動生成的話,clean 一下或是 rebuild 項目都行。
現在就能直接通過 setNickName 的形式來設置待傳遞的值了。
String nickName = "master";
RootFragmentDirections.ActionRootFragmentToSettingsFragment action =
RootFragmentDirections.actionRootFragmentToSettingsFragment().setNickName(nickName);
Navigation.findNavController(btnToSettings)
.navigate(action);
在 SettingsFragment 我們需要把值取出來,然后顯示在屏幕上。
String nickName = SettingsFragmentArgs.fromBundle(getArguments()).getNickName();
tvNickName.setText(nickName);
怎么樣,是不是很簡單,這比之前我們用 Bundle 傳值要方便的多啦,而且再也不用擔心 Key 寫錯的問題了。真香。
好了,Navigation 的基本學習就到這了,感覺真的挺不錯的。可以考慮用用了,不過現在好像主頁面都是四個或是五個 Tab 頁面,這用 Navigation 怎么實現呀?Google 早就替我們想好了。
BottomNavigationView
來,我們新建一個 Activity,然后打開布局文件的圖形化工具頁面,用之前我們添加 NavHostFragment 組件的方式來添加一個 BottomNavigationView,然后讓這個組件位于整個頁面的底部。頁面的其余部分全部都留給 NavHostFragment。因為我們又引入了一個新的 Fragment 管理棧,所以,需要再次新建一個 Navigation Graph 文件 tab_navigation。
下面就是 activity_tab.xml 以及 tab_navigation.xml 的代碼。
<?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=".TabActivity">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nv_bottom_menu"
android:layout_width="match_parent"
android:layout_height="48dp"
app:itemHorizontalTranslationEnabled="false"
app:layout_constraintBottom_toBottomOf="parent" />
<fragment
android:id="@+id/fragment3"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@+id/nv_bottom_menu"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/tab_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tab_navigation">
</navigation>
接下來干什么呢?剛才我們創建是容器,用于容納 Fragment 的,現在來創建三個 Fragment,這三個 Fragment 是用于填充進容器的內容。
分別是 FeedFragment、TimerFragment、MineFragment。這三個 Fragment 我們還是分別顯示自己的名稱。布局文件里也就一個 TextView,Java 代碼中什么也不做,僅僅是用來顯示。
有了三個 Fragment,我們現在去 tab_navigation 把這三個 Fragment 都添加進去。
<?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/tab_navigation"
app:startDestination="@id/feedFragment">
<fragment
android:id="@+id/feedFragment"
android:name="me.monster.blogtest.tab.FeedFragment"
android:label="fragment_feed"
tools:layout="@layout/fragment_feed" />
<fragment
android:id="@+id/timerFragment"
android:name="me.monster.blogtest.tab.TimerFragment"
android:label="fragment_timer"
tools:layout="@layout/fragment_timer" />
<fragment
android:id="@+id/mineFragment"
android:name="me.monster.blogtest.tab.MineFragment"
android:label="fragment_mine"
tools:layout="@layout/fragment_mine" />
</navigation>
現在,我們容器有了,內容有了,只差一個媒介,把它們進行關聯了。打開 activity_tab 的圖形化界面,在左側有一些屬性,其中有一個屬性是 menu。menu?就是那個經常用于頁面右上角的 menu?它怎么會出現在這邊?點擊 menu 行最右邊的按鈕。
彈出一個對話框,好像和一開始創建 NavHostFragment 是一樣的,不同的是,當時有待選擇的 Navigation Graph 文件,現在我們沒有 menu 文件,那就創建一個吧。
現在我們也有了 menu 文件。
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/feedFragment"
android:icon="@drawable/ic_tab_feed"
android:title="Feed" />
<item
android:id="@+id/timerFragment"
android:icon="@drawable/ic_tab_timer"
android:title="Timer" />
<item
android:id="@+id/mineFragment"
android:icon="@drawable/ic_tab_mine"
android:title="Mine" />
</menu>
現在再回去看 tab_activity.xml 發現 preview 已經變成了這樣的。Cool
難道 Menu 就是那個把內容 (Fragment) 與容器 (NavHostFragment) 進行建立關系的媒介?是也不是,有那么一點關系,不過不太準確。還記得之前我們用與 RootFragment 和 SettingsFragment 進行切換頁面的方式嗎?一個是前進到下一個頁面,一個是返回上一個頁面,雖然最終的行為不同,但是它們都使用到了一個叫 NavController 的類,這個類實際上就是實現在 Fragment 之間進行跳轉的類。
Navigation.findNavController(btnToSettings).navigate(action);
Navigation.findNavController(btnToRoot).popBackStack();
那我們是不是可以通過 Navigation Controller 并結合底部導航菜單的點擊事件來對 Fragment 進行控制,從而實現 Fragment 之間的切換?是這樣的,沒錯,不過 Google 幫助我們完成了很多復雜的事情,我們只需要在 TabActivity 中添加下面這些代碼即可。
private void setUpNavBottom() {
NavHostFragment hostFragment = (NavHostFragment) getSupportFragmentManager()
.findFragmentById(R.id.fragment3);
BottomNavigationView navMenu = findViewById(R.id.nv_bottom_menu);
if (hostFragment != null) {
NavController navController = hostFragment.getNavController();
NavigationUI.setupWithNavController(navMenu, navController);
}
}
第一行,findFragmentById 里填寫的 id 就是我們在 tab_activity.xml 中 name 屬性是 NavHostFragment 節點的 id。
然后再通過 NavigationUI.setupWithNavController()
將二者進行想管理,這樣只要我們點擊底部導航菜單就是自動實現 Fragment 的之間的切換,完全不需要開發者自己去寫那么控制邏輯。事實上,NavigationUI.setupWithNavController()
這個方法有很多重載方法,不僅僅只是用在 BottomNavigationView,還有 NavigationView 等,在這里就不一一介紹了感興趣的可以去試試?,F在來看看效果。
DeepLink
來來來,回顧一下剛才我們介紹的 Navigation Graph,它就是用于描述 Fragment 或者說用于描述內容信息的,剛才我們嘗試了子節點 Fragment 的 action(頁面跳轉)與 arguments(Bundle 傳值)節點,其實他還有一個子節點 deepLink。
不知道,你有沒有遇到那種情況,朋友在微信上分享你一個連接,你一點開,頁面上提示你使用微信的在瀏覽器打開,你在一點開發現,發現跳轉到了一個應用的頁面上去了。這種跳轉方式在 Navigation 這個導航框架內叫做 deepLink。讓我們來實現一下吧。
我們需要準備一個 Fragment,就叫 DeepLinkFragment 好了,這個頁面我們跟之前的 Fragment 一樣只顯示 DeepLinkFragment 這個文字好了。layout 布局文件及 Java 代碼就不貼了?,F在再來看 Navigation Graph 中怎么寫。
fragment
android:id="@+id/deepLinkFragment"
android:name="me.monster.blogtest.fragment.DeepLinkFragment"
android:label="fragment_deep"
tools:layout="@layout/fragment_deep" >
<deepLink
android:id="@+id/deepLink"
app:uri="www.example.com/{myarg}" />
</fragment>
是的,你沒有看錯,在 Navigation Graph 中就多了這么點東西,然后記得一定要記得在 manifest 的承載 DeepLinkFragment 的 Activity 節點內引入你的 Navigation Graph。
- 那里填的 url 后面大括號包裹著的是傳入 DeepLinkFragment 的值,
myarg
是 key,通過 Bundle 進行傳遞;- 我在寫這篇博客的時候,有兩個 Navigation Graph 文件,一個是用于 RootFragment 與 SettingsFragment 進行跳轉的 navigation.xml,一個是用于底部導航菜單欄的 tab_navigation.xml,我把 DeepLinkFragment 放在了 navigation.xml 中,所以下面的值是 @navigation/navigation。
<nav-graph android:value="@navigation/navigation" />
來試一下,看看效果吧。
好了,我們整個 Navigation 的學習到這里也告一段落了,結束之前讓我們用一幅圖來回顧一下 Navigation。
本文首發于個人博客,文中全部源代碼已上傳至 GitHub,喜歡的麻煩點個??。
本文封面圖:Photo by Joseph Barrientos on Unsplash