本文首發於我的博客,已受權郭霖轉載至其公衆號:guolin_blogphp
Navigation 是一個谷歌官方推出的一個用於 APP 內部便捷切換內容(Fragment 或 Activity)的庫。從而使得 APP 內的頁面跳轉更簡單。java
我知道它的時候它的版本已是 2.0.0 了,也是時候來學習一波了。android
不管何時,學習的第一手資料不能缺了官方出品的 CodeLab。相信你,看了 CodeLab 後就能對 Navigation 有一個簡單的瞭解。本人也是對 CodeLab 學習以後才寫下了這篇博客,主要內容都能在 CodeLab 上找到。不過 CodeLab裏面是英文的講解,並且其中的代碼是使用 Kotlin 編寫的,這篇博客是以 Java 代碼的方式進行的。git
還一件事情,Navigation 的原生支持是從 Android Studio 3.3 開始的,3.2 版本的須要在設置面板的 Experimental 模塊中啓用 Navigation 編輯器。github
圖片來自 CodeLab。瀏覽器
下面開始正題安全
首先,添加依賴。微信
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。app
<?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 的相關信息。日後會講到的。如今咱們如今建立一個 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 代碼中沒有作任何事情。如今讓咱們回到 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" />
複製代碼
單單一個 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 呢?
我也不知道爲啥學,感受若是單純爲了保證 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 早就替咱們想好了。
來,咱們新建一個 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 等,在這裏就不一一介紹了感興趣的能夠去試試。如今來看看效果。
來來來,回顧一下剛纔咱們介紹的 Navigation Graph,它就是用於描述 Fragment 或者說用於描述內容信息的,剛纔咱們嘗試了子節點 Fragment 的 action(頁面跳轉)與 arguments(Bundle 傳值)節點,其實他還有一個子節點 deepLink。
不知道,你有沒有遇到那種狀況,朋友在微信上分享你一個鏈接,你一點開,頁面上提示你使用微信的在瀏覽器打開,你在一點開發現,發現跳轉到了一個應用的頁面上去了。這種跳轉方式在 Navigation 這個導航框架內叫作 deepLink。讓咱們來實現一下吧。
咱們須要準備一個 Fragment,就叫 DeepLinkFragment 好了,這個頁面咱們跟以前的 Fragment 同樣只顯示 DeepLinkFragment 這個文字好了。layout 佈局文件及 Java 代碼就不貼了。如今再來看 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,喜歡的麻煩點個🌟。
Navigation 的第二篇也已經完成了,主要是講解 Navigation 中 Fragment 的切換,並經過自定義的形式達到想要的效果。第二篇:Navigation 之 Fragment 切換
本文封面圖:Photo by Joseph Barrientos on Unsplash