1 前言
Android Jetpack 的導航組件Navigation可以很方便的管理fragment/activity的導航。
注意:如果您要在 Android Studio 中使用 Navigation 組件,則必須使用 Android Studio 3.3 或更高版本。
導航組件有三個關鍵部分
- NavGraph:導航圖,包含一組頁面和它們之間的跳轉關係,比如A頁面跳到B頁面 B頁面跳到C頁面這樣的關係信息
- NavHost:一個可以顯示導航頁面的空白容器,系統默認實現了一個NavHostFragment
- NavController:管理應用導航的對象,用來控制NavHost容器中當前應該顯示的頁面
2 頁面跳轉簡單使用
下面先來一個簡單的小例子瞭解一下基本使用效果如下:FirstFragment->SecondFragment;SecondFragment->ThirdFragment;ThirdFragment->FirstFragment;ThirdFragment->SecondFragment
添加最新依賴:
implementation "androidx.navigation:navigation-fragment:2.1.0"
implementation "androidx.navigation:navigation-ui:2.1.0"
2.1 創建導航圖
- 右鍵res文件夾
- 依次選擇New->Android Resource File
- 第一行File name 中輸入一個文件名 比如 nav_graph
- 第二行在Resource type 下拉列表中選擇 Navigation,然後點擊 OK。
點擊OK之後,Android Studio 會在 res 目錄內創建一個 navigation 資源目錄,這裏面有我們剛纔創建的文件。
雙擊該文件可以打開該文件如下圖,我們可以使用圖像化界面編輯,也可以使用代碼來編輯,右下角可以切換。
點擊上圖1處的加號,把剛纔創建的3個Fragment都添加進來,頁面跳轉關係可以直接手動連線,比如從FirstFragment連一個箭頭到SecondFragment,就表示從FirstFragment跳轉到SecondFragment。
如果點解了某條線,上圖最右邊會顯示該線的屬性信息,我們可以自己定義,包括線的id,將要導航的目的地,頁面切換的動畫,動畫系統內置了幾個,我們也可以自己定義補間畫。彈出的時候切換到哪個頁面等。
切換到編碼欄可以看到最後生成的代碼如下:
<?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_demo_1"
app:startDestination="@id/firstFragment">
<fragment
android:id="@+id/firstFragment"
android:name="com.chs.androiddailytext.jetpack.navigation.FirstFragment"
android:label="fragment_frist"
tools:layout="@layout/fragment_frist">
<action
android:id="@+id/action_firstFragment_to_secondFragment"
app:destination="@id/secondFragment"
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>
<fragment
android:id="@+id/secondFragment"
android:name="com.chs.androiddailytext.jetpack.navigation.SecondFragment"
android:label="fragment_second"
tools:layout="@layout/fragment_second">
<action
android:id="@+id/action_secondFragment_to_thirdFragment"
app:destination="@id/thirdFragment"
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>
<fragment
android:id="@+id/thirdFragment"
android:name="com.chs.androiddailytext.jetpack.navigation.ThirdFragment"
android:label="fragment_third"
tools:layout="@layout/fragment_third">
<action
android:id="@+id/action_thirdFragment_to_firstFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:popUpTo="@id/firstFragment" />
<action
android:id="@+id/action_thirdFragment_to_secondFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:popUpTo="@+id/secondFragment" />
<deepLink app:uri="www.chs.com/{userName}" />
</fragment>
</navigation>
- 根標籤是navigation,它需要有一個屬性startDestination,表示默認第一個顯示的界面,這裏設置FirstFragment
- 每個fragment標籤代表一個fragment類,其實不止可以寫fragment標籤,還可以寫activity/dialog標籤,代表着Naviagtion的默認能力,既可以導航Fragment,也可以導航Activity,DialogFragment。
- 每個action標籤就相當於上圖中的每條線,代表了執行切換的時候的目的地,切換動畫等信息。
- 頁面切換的動畫就是簡單的補間動畫非常簡單,比如定義一個從右邊滑入的動畫如下
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="-100%" android:toXDelta="0%"
android:fromYDelta="0%" android:toYDelta="0%"
android:duration="200"/>
</set>
2.2 給activity添加NavHost
創建一個Activity,在其xml文件中添加FragmentNavHost
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<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/nav_graph" />
</FrameLayout>
- 標籤爲fragment,android:name就是NavHost的實現類,這裏是NavHostFragment
app:navGraph
屬性就是我們前面在res文件夾下創建的文件app:defaultNavHost="true"
意思是可以攔截系統的返回鍵,這樣我們點擊手機返回按鈕的時候就能跟activity一樣回到上一個頁面了。
2.3 開啓導航
view.findViewById(R.id.button).setOnClickListener(v -> {
Bundle bundle = new Bundle();
bundle.putString("title","我是前面傳過來的");
Navigation.findNavController(v).navigate(R.id.action_firstFragment_to_secondFragment,bundle);
});
開啓導航非常簡單,就一句話,通過Navigation#findNavController
方法找到NavController,調用它的navigate方法開始導航。
- 第一個參數是action的id。
- 第二個參數是bundle,用於兩個界面之間傳遞參數,可以不傳。在目標頁面通過
getArguments()
方法來檢索是否有bundle並獲取數據。
除了直接使用Bundle來傳遞數據,Google更推薦我們使用Safe Args來傳遞數據,因爲他可以確保數據的類型安全。
使用afe Args,首先在工程的build.gradle文件夾中添加afe Args的gradle插件的依賴
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0"
然後在app下的build.gradle文件中引入插件
apply plugin: "androidx.navigation.safeargs"
//kotlin使用下面方式引入
apply plugin: "androidx.navigation.safeargs.kotlin"
然後在最開始創建的nav_graph導航圖文件中添加argument標籤如下
<fragment
android:id="@+id/firstFragment"
android:name="com.chs.androiddailytext.jetpack.navigation.FirstFragment"
android:label="fragment_frist"
tools:layout="@layout/fragment_frist">
<action
android:id="@+id/action_firstFragment_to_secondFragment"
app:destination="@id/secondFragment"
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="title"
android:defaultValue="i am title"
app:argType="string"/>
</fragment>
argument標籤中添加參數名,參數類型和默認值。添加完之後重新編譯,插件就會自動給我們生成幾個類,位置在 \app\build\generated\source\navigation-args\debug 如下圖
下面開始使用這幾個類,首先在FirstFragment中,直接通過FirstFragmentArgs.Builder()方法來創建bundle對象
view.findViewById(R.id.button).setOnClickListener(v -> {
Bundle bundle = new FirstFragmentArgs.Builder().setTitle("我是前面傳過來的").build().toBundle();
Navigation.findNavController(v).navigate(R.id.action_firstFragment_to_secondFragment,bundle);
});
然後在SecondFragment中使用FirstFragmentArgs.fromBundle方法來接收值
Bundle arguments = getArguments();
if(arguments!=null){
String title = FirstFragmentArgs.fromBundle(getArguments()).getTitle();
tvTitle.setText(title);
}
這樣也能正常接收到正確的傳參,並且是類型安全的。
對於傳遞參數這塊,由於這幾個fragment在同一個activity中,所以我們還可以使用Jetpack組件庫中的ViewModel和LiveData來共享參數。這樣不僅可以保證類型安全,還可以在屏幕旋轉導致的activity重建的時候保持數據不丟失。數據共享之前文章有Android Jetpack之ViewModel。
2.3 Activity跳轉
NavController navController = new NavController(this);
NavigatorProvider navigatorProvider = navController.getNavigatorProvider();
NavGraph navGraph = new NavGraph(new NavGraphNavigator(navigatorProvider));
ActivityNavigator navigator = navigatorProvider.getNavigator(ActivityNavigator.class);
ActivityNavigator.Destination destination = navigator.createDestination();
//id可以隨便一個資源id
destination.setId(R.id.bottom_nav_activity);
destination.setComponentName(new ComponentName(getApplication().getPackageName(),
"com.chs.androiddailytext.jetpack.botton_navigation.BottomNavActivity"));
navGraph.addDestination(destination);
navGraph.setStartDestination(destination.getId());
navController.setGraph(navGraph);
NavController並不直接執行具體的導航操作,而是交給Navigator的子類具體的ActivityNavigator,FragmentNavigator,DialogFragmentNavigator去做,甚至可以自己繼承一個Navigator來完成一個不一樣的跳轉。
這裏跳轉Activity就創建一個ActivityNavigator,使用它的全類名進行跳轉。
Activity的跳轉看起來還是挺麻煩的,直接一個startActivity不就完事了。那有啥用啊。
首先這部分代碼可以封裝一下會簡單很多,其次把Activity和Fragment的跳轉方式統一,最後,在組件化項目開發中,不同模塊之間如果沒有依賴關係,兩者之間如果想要相互跳轉的時候,就需要使用Activity的全類名來進行跳轉了。之前組件化開發項目的時候可能會引入阿里的ARouter路由框架或者別的或者自己寫路由框架,現在其實也可以直接使用Navigation,稍微改造一下也能很方便的實現組件之間跳轉。
2.4 deepLink
deepLink:深層鏈接,就是直接跳轉到應用中的某個頁面,比如從通知欄直接跳轉到詳情頁面。
Navigation 可以創建兩種不同的深層鏈接:顯示深層鏈接和隱式深層鏈接
顯示深層鏈接使用PendingIntent來導航到特定頁面,比如可以在通知欄,快捷方式等地方跳轉,比如下面的通過通知跳轉。
view.findViewById(R.id.button1).setOnClickListener(v -> {
Bundle bundle = new Bundle();
bundle.putString("userName","大海");
// PendingIntent pendingIntent = Navigation.findNavController(v).createDeepLink().setArguments(bundle)
// .setDestination(R.id.thirdFragment)
// .createPendingIntent();
PendingIntent pendingIntent = new NavDeepLinkBuilder(requireContext())
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.thirdFragment)
.setArguments(bundle)
.createPendingIntent();
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(requireContext());
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
notificationManager.createNotificationChannel(new NotificationChannel(
"deepLink","deepLinkName", NotificationManager.IMPORTANCE_HIGH
));
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(requireContext(), "deepLink")
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentTitle("測試deepLink")
.setContentText("哈哈哈")
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_DEFAULT);
notificationManager.notify(10,builder.build());
});
可以使用Navigation.findNavController(v).createDeepLink()
或者new NavDeepLinkBuilder(context)
兩種方法來創建一個深層鏈接的PendingIntent。
注意使用顯示深層鏈接打開應用的時候,當前堆棧會被清空,替換爲當前深層上的結點。當爲嵌套圖表的時候(就是標籤裏面還有標籤)那麼每個都會添加到相應的堆棧中。
隱式鏈接是當用戶點擊某個鏈接的時候,通過URI跳轉到某個頁面。
建立隱式鏈接,首先在最開始創建的nav_graph.xml文件中給某個fragment添加deepLink標籤。前面nav_graph.xml文件中已經添加
<deepLink app:uri="www.chs.com/{userName}" />
該uri沒有聲明是http還是https,那麼這兩個都能匹配。大括號內的是傳遞的參數。
然後去manifest.xml 文件中給當前的activity添加一個<nav-graph>
屬性
<activity android:name=".jetpack.navigation.NavigationActivity">
<nav-graph android:value="@navigation/nav_graph"/>
</activity>
在build的時候,Navigation 組件會將 <nav-graph>
元素替換爲生成的 <intent-filter>
元素來匹配深層鏈接。其實我們在studio中雙擊打開apk就能在看到manifest.xml中替換完成的樣子如下:
apk路徑:app - > build - > outputs - > apk - > debug - > app-debug.apk
<activity
android:name="com.chs.androiddailytext.jetpack.navigation.NavigationActivity">
<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="http" />
<data
android:scheme="https" />
<data
android:host="www.chs.com" />
<data
android:pathPrefix="/" />
</intent-filter>
</activity>
可以通過adb來測試隱式深層鏈接的效果,打開命令行輸入
adb shell am start -a android.intent.action.VIEW -d "http://www.chs.com/Divad"
在系統彈出的窗口中,選擇使用我們的應用打開,就能跳轉到對應的頁面了。
2.5 底部導航
使用Navigation還可以完成頂部應用欄的導航,側滑抽屜的導航,底部導航。
下面來完成一個最常用的底部導航,底部導航非常容易實現,因爲有現成的模板,只需在某個包下面右擊鼠標,new->activity->Bottom Navigaton Activity就能直接創建一個帶有底部導航的Activity。
不過我們學習階段就不這麼搞了,自己建議空白的Activity,一點點的添加進去,走一遍流程更容易熟悉。
創建一個BottomNavActivity和三個fragment:OneFragment,TowFragment,ThreeFragment。
- 在res->navigation文件夾下新創建一個導航圖命名爲nav_bottom_graph.xml。這個圖裏的fragment彼此沒有關係,所以也不用寫action,最終很簡單:
<?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_bottom_graph"
app:startDestination="@id/oneFragment">
<fragment
android:id="@+id/oneFragment"
android:name="com.chs.androiddailytext.jetpack.botton_navigation.OneFragment"
android:label="fragment_one"
tools:layout="@layout/fragment_one" />
<fragment
android:id="@+id/towFragment"
android:name="com.chs.androiddailytext.jetpack.botton_navigation.TowFragment"
android:label="fragment_tow"
tools:layout="@layout/fragment_tow" />
<fragment
android:id="@+id/threeFragment"
android:name="com.chs.androiddailytext.jetpack.botton_navigation.ThreeFragment"
android:label="fragment_three"
tools:layout="@layout/fragment_three" />
</navigation>
如果引用有ActionBar,android:label
屬性的內容就會顯示在標題欄,成爲該頁面的標題。
- 創建一個menu,menu中就是我們的底部菜單的各個子項的信息
- 右鍵res文件夾
- 依次選擇New->Android Resource File
- 第一行File name 中輸入一個文件名 比如 bottom_nav.xml
- 第二行在Resource type 下拉列表中選擇 Menu,然後點擊 OK。
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/oneFragment"
android:icon="@drawable/ic_home"
android:contentDescription="首頁"
android:title="首頁" />
<item
android:id="@+id/towFragment"
android:icon="@drawable/ic_list"
android:contentDescription="二頁"
android:title="二頁" />
<item
android:id="@+id/threeFragment"
android:icon="@drawable/ic_feedback"
android:contentDescription="三頁"
android:title="三頁" />
</menu>
- 在BottomNavActivity的佈局文件中引入BottomNavigationView和NavHostFragment
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/bottom_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_bottom_graph" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:menu="@menu/bottom_nav"/>
</LinearLayout>
NavHostFragment通過app:navGraph
屬性關聯導航圖,BottomNavigationView通過app:menu
屬性關聯前面創建的menu
- 最後去Activity中使用
public class BottomNavActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_bottom_navigation);
BottomNavigationView bottomNavigationView = findViewById(R.id.bottom_nav);
//導航控制器
NavController controller = Navigation.findNavController(this,R.id.bottom_fragment);
//底部導航的配置
AppBarConfiguration configuration = new AppBarConfiguration.Builder(bottomNavigationView.getMenu()).build();
//關聯控制器和底部導航的配置
NavigationUI.setupActionBarWithNavController(this,controller,configuration);
//關聯底部bottomNavigationView和控制器
NavigationUI.setupWithNavController(bottomNavigationView,controller);
}
}
OK 底部導航視圖建立完畢,效果
3 原理分析
下面根據第一個跳轉的小例子來看一下Navigation的工作流程。
3.1 NavHostFragment#onCreate
從Activity中開始,Activity中就一個佈局文件,內部有一個NavHostFragment,它是頁面的承載區域,那就從它的onCreate方法開始分析。
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Context context = requireContext();
mNavController = new NavHostController(context);
mNavController.setLifecycleOwner(this);
mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());
// Set the default state - this will be updated whenever
// onPrimaryNavigationFragmentChanged() is called
mNavController.enableOnBackPressed(
mIsPrimaryBeforeOnCreate != null && mIsPrimaryBeforeOnCreate);
mIsPrimaryBeforeOnCreate = null;
mNavController.setViewModelStore(getViewModelStore());
onCreateNavController(mNavController);
Bundle navState = null;
if (savedInstanceState != null) {
navState = savedInstanceState.getBundle(KEY_NAV_CONTROLLER_STATE);
if (savedInstanceState.getBoolean(KEY_DEFAULT_NAV_HOST, false)) {
mDefaultNavHost = true;
getParentFragmentManager().beginTransaction()
.setPrimaryNavigationFragment(this)
.commit();
}
mGraphId = savedInstanceState.getInt(KEY_GRAPH_ID);
}
if (navState != null) {
// Navigation controller state overrides arguments
mNavController.restoreState(navState);
}
if (mGraphId != 0) {
// Set from onInflate()
mNavController.setGraph(mGraphId);
} else {
// See if it was set by NavHostFragment.create()
final Bundle args = getArguments();
final int graphId = args != null ? args.getInt(KEY_GRAPH_ID) : 0;
final Bundle startDestinationArgs = args != null
? args.getBundle(KEY_START_DESTINATION_ARGS)
: null;
if (graphId != 0) {
mNavController.setGraph(graphId, startDestinationArgs);
}
}
}
創建了一個NavHostController類,並關聯Fragment的生命週期,設置各種屬性,先看一下NavHostController的構造方法
public NavHostController(@NonNull Context context) {
super(context);
}
public NavController(@NonNull Context context) {
mContext = context;
while (context instanceof ContextWrapper) {
if (context instanceof Activity) {
mActivity = (Activity) context;
break;
}
context = ((ContextWrapper) context).getBaseContext();
}
mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider));
mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
}
往NavigatorProvider中添加了兩個Navigator:NavGraphNavigator和ActivityNavigator
- NavGraphNavigator:我們前面創建一個導航圖NavGraph的時候會指定一個默認的第一個顯示的頁面(startDestination)該導航器就是用來導航到這個頁面,
- ActivityNavigator:顧名思義,用來給Activity導航的
回到NavHostFragment的onCreate方法中繼續往下看,可以看到onCreateNavController(mNavController);
方法
protected void onCreateNavController(@NonNull NavController navController) {
navController.getNavigatorProvider().addNavigator(
new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
}
這裏又往NavigatorProvider添加了兩個Navigator:
- DialogFragmentNavigator:用來給DialogFragment導航
- FragmentNavigator:用來給Fragment導航
3.2 Navigator
NavigatorProvider內部有個HashMap用來存儲這幾個Navigator。NavGraphNavigator,ActivityNavigator,DialogFragmentNavigator,FragmentNavigator,這幾個類是專門用來導航的,那就先來了解一下這幾個類,首先看其父類:
public abstract class Navigator<D extends NavDestination> {
@Retention(RUNTIME)
@Target({TYPE})
@SuppressWarnings("UnknownNullness") // TODO https://issuetracker.google.com/issues/112185120
public @interface Name {
String value();
}
@NonNull
public abstract D createDestination();
@Nullable
public abstract NavDestination navigate(@NonNull D destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Extras navigatorExtras);
public abstract boolean popBackStack();
@Nullable
public Bundle onSaveState() {
return null;
}
public void onRestoreState(@NonNull Bundle savedState) {}
public interface Extras { }
}
- 該類的泛型是NavDestination的子類,NavDestination其實就是一個一個的頁面。
- Navigator子類需要添加Name註解,比如ActivityNavigator中天街了@Navigator.Name(“activity”) ,FragmentNavigator中添加了@Navigator.Name(“fragment”),用來往NavigatorProvider中的HashMap中存放的時候最爲key來使用
- createDestination創造一個Destination(目標),也就是一個頁面,抽象方法由子類實現
- navigate方法,導航到目標頁面,抽象方法由子類實現
- onSaveState,onRestoreState 保存狀態,恢復狀態
- Extras 提供一些額外的行爲,比如轉場動畫等
父類看完,在來看看子類 就看比較常用的ActivityNavigator和FragmentNavigator,NavGraphNavigator,並且主要看它們實現的父類的哪兩個抽象方法:createDestination和navigate
3.3 ActivityNavigator
ActivityNavigator#createDestination方法創建了一個ActivityNavigator.Destination對象
public static class Destination extends NavDestination {
private Intent mIntent;
private String mDataPattern;
public Destination(@NonNull NavigatorProvider navigatorProvider) {
this(navigatorProvider.getNavigator(ActivityNavigator.class));
}
public Destination(@NonNull Navigator<? extends Destination> activityNavigator) {
super(activityNavigator);
}
@CallSuper
@Override
public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
super.onInflate(context, attrs);
TypedArray a = context.getResources().obtainAttributes(attrs,
R.styleable.ActivityNavigator);
String targetPackage = a.getString(R.styleable.ActivityNavigator_targetPackage);
if (targetPackage != null) {
targetPackage = targetPackage.replace(NavInflater.APPLICATION_ID_PLACEHOLDER,
context.getPackageName());
}
setTargetPackage(targetPackage);
String className = a.getString(R.styleable.ActivityNavigator_android_name);
if (className != null) {
if (className.charAt(0) == '.') {
className = context.getPackageName() + className;
}
setComponentName(new ComponentName(context, className));
}
setAction(a.getString(R.styleable.ActivityNavigator_action));
String data = a.getString(R.styleable.ActivityNavigator_data);
if (data != null) {
setData(Uri.parse(data));
}
setDataPattern(a.getString(R.styleable.ActivityNavigator_dataPattern));
a.recycle();
}
@NonNull
public final Destination setIntent(@Nullable Intent intent) {
mIntent = intent;
return this;
}
@NonNull
public final Destination setTargetPackage(@Nullable String packageName) {
if (mIntent == null) {
mIntent = new Intent();
}
mIntent.setPackage(packageName);
return this;
}
@NonNull
public final Destination setComponentName(@Nullable ComponentName name) {
if (mIntent == null) {
mIntent = new Intent();
}
mIntent.setComponent(name);
return this;
}
@NonNull
public final Destination setAction(@Nullable String action) {
if (mIntent == null) {
mIntent = new Intent();
}
mIntent.setAction(action);
return this;
}
@NonNull
public final Destination setData(@Nullable Uri data) {
if (mIntent == null) {
mIntent = new Intent();
}
mIntent.setData(data);
return this;
}
@NonNull
public final Destination setDataPattern(@Nullable String dataPattern) {
mDataPattern = dataPattern;
return this;
}
......
}
構造方法中把當前Navigator的全類名保存到Destination的父類的成員變量中。
ActivityNavigator.Destination類內部還有很多跟Intent相關的方法比如setAction,setData等,用來創建Intent和給Intent設置參數。
ActivityNavigator#navigate
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
if (destination.getIntent() == null) {
throw new IllegalStateException("Destination " + destination.getId()
+ " does not have an Intent set.");
}
Intent intent = new Intent(destination.getIntent());
if (args != null) {
intent.putExtras(args);
String dataPattern = destination.getDataPattern();
if (!TextUtils.isEmpty(dataPattern)) {
// Fill in the data pattern with the args to build a valid URI
StringBuffer data = new StringBuffer();
Pattern fillInPattern = Pattern.compile("\\{(.+?)\\}");
Matcher matcher = fillInPattern.matcher(dataPattern);
while (matcher.find()) {
String argName = matcher.group(1);
if (args.containsKey(argName)) {
matcher.appendReplacement(data, "");
//noinspection ConstantConditions
data.append(Uri.encode(args.get(argName).toString()));
} else {
throw new IllegalArgumentException("Could not find " + argName + " in "
+ args + " to fill data pattern " + dataPattern);
}
}
matcher.appendTail(data);
intent.setData(Uri.parse(data.toString()));
}
}
if (navigatorExtras instanceof Extras) {
Extras extras = (Extras) navigatorExtras;
intent.addFlags(extras.getFlags());
}
if (!(mContext instanceof Activity)) {
// If we're not launching from an Activity context we have to launch in a new task.
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
if (navOptions != null && navOptions.shouldLaunchSingleTop()) {
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
}
if (mHostActivity != null) {
final Intent hostIntent = mHostActivity.getIntent();
if (hostIntent != null) {
final int hostCurrentId = hostIntent.getIntExtra(EXTRA_NAV_CURRENT, 0);
if (hostCurrentId != 0) {
intent.putExtra(EXTRA_NAV_SOURCE, hostCurrentId);
}
}
}
final int destId = destination.getId();
intent.putExtra(EXTRA_NAV_CURRENT, destId);
if (navOptions != null) {
// For use in applyPopAnimationsToPendingTransition()
intent.putExtra(EXTRA_POP_ENTER_ANIM, navOptions.getPopEnterAnim());
intent.putExtra(EXTRA_POP_EXIT_ANIM, navOptions.getPopExitAnim());
}
if (navigatorExtras instanceof Extras) {
Extras extras = (Extras) navigatorExtras;
ActivityOptionsCompat activityOptions = extras.getActivityOptions();
if (activityOptions != null) {
ActivityCompat.startActivity(mContext, intent, activityOptions.toBundle());
} else {
mContext.startActivity(intent);
}
} else {
mContext.startActivity(intent);
}
if (navOptions != null && mHostActivity != null) {
int enterAnim = navOptions.getEnterAnim();
int exitAnim = navOptions.getExitAnim();
if (enterAnim != -1 || exitAnim != -1) {
enterAnim = enterAnim != -1 ? enterAnim : 0;
exitAnim = exitAnim != -1 ? exitAnim : 0;
mHostActivity.overridePendingTransition(enterAnim, exitAnim);
}
}
return null;
}
Activity的啓動當然需要一個Intent對象,上面代碼中可以看到,ActivityNavigator的navigate方法中就是通過Destination獲取到Intent對象,然後設置傳遞的參數,設置Activity的啓動模式,設置切換動畫等,最後通過startActivity方法來啓動一個Activity。
3.4 FragmentNavigator
FragmentNavigator#createDestination創建了一個FragmentNavigator.Destination對象
public static class Destination extends NavDestination {
private String mClassName;
public Destination(@NonNull NavigatorProvider navigatorProvider) {
this(navigatorProvider.getNavigator(FragmentNavigator.class));
}
public Destination(@NonNull Navigator<? extends Destination> fragmentNavigator) {
super(fragmentNavigator);
}
@CallSuper
@Override
public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
super.onInflate(context, attrs);
TypedArray a = context.getResources().obtainAttributes(attrs,
R.styleable.FragmentNavigator);
String className = a.getString(R.styleable.FragmentNavigator_android_name);
if (className != null) {
setClassName(className);
}
a.recycle();
}
@NonNull
public final Destination setClassName(@NonNull String className) {
mClassName = className;
return this;
}
......
}
這裏面的代碼比ActivityNavigator中的少了很多,主要是設置ClassName的方法。後面根據ClassName反射創建出一個fragment對象。
FragmentNavigator#navigate方法
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
if (mFragmentManager.isStateSaved()) {
Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
+ " saved its state");
return null;
}
String className = destination.getClassName();
if (className.charAt(0) == '.') {
className = mContext.getPackageName() + className;
}
final Fragment frag = instantiateFragment(mContext, mFragmentManager,
className, args);
frag.setArguments(args);
final FragmentTransaction ft = mFragmentManager.beginTransaction();
int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = enterAnim != -1 ? enterAnim : 0;
exitAnim = exitAnim != -1 ? exitAnim : 0;
popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
}
ft.replace(mContainerId, frag);
ft.setPrimaryNavigationFragment(frag);
final @IdRes int destId = destination.getId();
final boolean initialNavigation = mBackStack.isEmpty();
// TODO Build first class singleTop behavior for fragments
final boolean isSingleTopReplacement = navOptions != null && !initialNavigation
&& navOptions.shouldLaunchSingleTop()
&& mBackStack.peekLast() == destId;
boolean isAdded;
if (initialNavigation) {
isAdded = true;
} else if (isSingleTopReplacement) {
// Single Top means we only want one instance on the back stack
if (mBackStack.size() > 1) {
// If the Fragment to be replaced is on the FragmentManager's
// back stack, a simple replace() isn't enough so we
// remove it from the back stack and put our replacement
// on the back stack in its place
mFragmentManager.popBackStack(
generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
FragmentManager.POP_BACK_STACK_INCLUSIVE);
ft.addToBackStack(generateBackStackName(mBackStack.size(), destId));
}
isAdded = false;
} else {
ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
isAdded = true;
}
if (navigatorExtras instanceof Extras) {
Extras extras = (Extras) navigatorExtras;
for (Map.Entry<View, String> sharedElement : extras.getSharedElements().entrySet()) {
ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue());
}
}
ft.setReorderingAllowed(true);
ft.commit();
// The commit succeeded, update our view of the world
if (isAdded) {
mBackStack.add(destId);
return destination;
} else {
return null;
}
}
通過Destination拿到ClassName,instantiateFragment方法內通過ClassName加載出class對象並通過反射創建出fragment,最後通過FragmentTransaction的replace方法將創建出來的fragment放到相應的容器裏面。
這裏使用的是replace,我們知道replace方法每次都會重新創建fragment,所以使用Navigation創建的底部導航頁面,每次點擊切換頁面當前fragment都會重建,這就不太友好了。解決思路可以繼承FragmentNavigator 重寫navigate方法,自己將replace改爲hide和show
3.5 NavGraphNavigator
NavGraphNavigator#createDestination
public NavGraph createDestination() {
return new NavGraph(this);
}
public class NavGraph extends NavDestination implements Iterable<NavDestination> {
final SparseArrayCompat<NavDestination> mNodes = new SparseArrayCompat<>();
......
}
可以看到NavGraphNavigator的createDestination,並沒有直接創建一個NavDestination對象,而是創建了一個NavGraph對象,它內部有個集合mNodes來保存了一組的NavDestination對象。
這個NavGraph對象其實就是最開始創建的那個nav_graph.xml解析並創建出來的。mNodes集合中保存的就是nav_graph.xml中的一個一個的結點,其實就是一個一個的頁面。
那麼nav_graph.xml文件是在哪裏被解析的呢?這就得回到我們最開始看源碼的地方NavHostFragment的onCreate方法中,有一句話 mNavController.setGraph(mGraphId)
參數是nav_graph.xml文件的id
public void setGraph(@NavigationRes int graphResId) {
setGraph(graphResId, null);
}
public void setGraph(@NavigationRes int graphResId, @Nullable Bundle startDestinationArgs) {
setGraph(getNavInflater().inflate(graphResId), startDestinationArgs);
}
public void setGraph(@NonNull NavGraph graph, @Nullable Bundle startDestinationArgs) {
if (mGraph != null) {
// Pop everything from the old graph off the back stack
popBackStackInternal(mGraph.getId(), true);
}
mGraph = graph;
onGraphCreated(startDestinationArgs);
}
可以看到在其第二個重載的方法中,通過getNavInflater().inflate
方法創建出一個NavGraph對象,傳到第三個重載的方法中,並賦值給成員變量mGraph,最後在onGraphCreated方法中將第一個頁面顯示出來。
到這裏我們知道,我們最開始創建的那個nav_graph.xml文件,最終會被解析成爲一個NavGraph對象,然後個人NavController關聯。所以即使沒有該文件,我們也可以自己根據需要的參數new一個NavGraph對象,畢竟xml中配置不是很靈活。
怎麼解析的,下面看一個inflate方法
public NavGraph inflate(@NavigationRes int graphResId) {
Resources res = mContext.getResources();
XmlResourceParser parser = res.getXml(graphResId);
final AttributeSet attrs = Xml.asAttributeSet(parser);
......
String rootElement = parser.getName();
NavDestination destination = inflate(res, parser, attrs, graphResId);
if (!(destination instanceof NavGraph)) {
throw new IllegalArgumentException("Root element <" + rootElement + ">"
+ " did not inflate into a NavGraph");
}
return (NavGraph) destination;
......
NavGraph是NavDestination的子類,創建出一個NavDestination對象,並判斷是不是NavGraph對象,如果是強轉成NavGraph並返回,如果不是,就拋出異常。
private NavDestination inflate(@NonNull Resources res, @NonNull XmlResourceParser parser,
@NonNull AttributeSet attrs, int graphResId)
throws XmlPullParserException, IOException {
Navigator<?> navigator = mNavigatorProvider.getNavigator(parser.getName());
final NavDestination dest = navigator.createDestination();
dest.onInflate(mContext, attrs);
final int innerDepth = parser.getDepth() + 1;
int type;
int depth;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& ((depth = parser.getDepth()) >= innerDepth
|| type != XmlPullParser.END_TAG)) {
if (type != XmlPullParser.START_TAG) {
continue;
}
if (depth > innerDepth) {
continue;
}
final String name = parser.getName();
//解析各種標籤 argument ,deepLink action ...
if (TAG_ARGUMENT.equals(name)) {
inflateArgumentForDestination(res, dest, attrs, graphResId);
} else if (TAG_DEEP_LINK.equals(name)) {
inflateDeepLink(res, dest, attrs);
} else if (TAG_ACTION.equals(name)) {
inflateAction(res, dest, attrs, parser, graphResId);
} else if (TAG_INCLUDE.equals(name) && dest instanceof NavGraph) {
final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavInclude);
final int id = a.getResourceId(R.styleable.NavInclude_graph, 0);
((NavGraph) dest).addDestination(inflate(id));
a.recycle();
} else if (dest instanceof NavGraph) {
((NavGraph) dest).addDestination(inflate(res, parser, attrs, graphResId));
}
return dest;
}
先解析navigation根標籤,它解析出來是個NavGraph對象,所以調用它的addDestination方法,將子標籤解析出來的對象放入到成員你變量mNodes中。
NavGraphNavigator#navigate
public NavDestination navigate(@NonNull NavGraph destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Extras navigatorExtras) {
int startId = destination.getStartDestination();
if (startId == 0) {
throw new IllegalStateException("no start destination defined via"
+ " app:startDestination for "
+ destination.getDisplayName());
}
NavDestination startDestination = destination.findNode(startId, false);
if (startDestination == null) {
final String dest = destination.getStartDestDisplayName();
throw new IllegalArgumentException("navigation destination " + dest
+ " is not a direct child of this NavGraph");
}
Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
startDestination.getNavigatorName());
return navigator.navigate(startDestination, startDestination.addInDefaultArgs(args),
navOptions, navigatorExtras);
}
先找到第一個需要啓動的頁面的id,創建出一個NavDestination,通過標籤的名字去mNavigatorProvider這個HashMap中拿到對應的Navigation。NavGraphNavigator並不是最終的執行者,它還是會把執行的任務交給特定的比如ActivityNavigator,FragmentNavigator和DialogFragmentNavigator去做。
總結
到這裏Navigation的工作流程就走完了,總結一下:
- 首先需要有一個承載頁面的容器NavHost,系統有個默認是現實NavHostFragment它內部初始化了NavController
- 想要完成導航必須要有一個導航控制器NavController
- 它裏面有兩個非常重要的東西NavGraph和NavigatorProvider
- NavGraph裏面包含了一組NavDestination,每個NavDestination就是一個一個的頁面,也就是導航目的地
- NavigatorProvider內部有個HashMap,用來存放Navigator,Navigator它是個抽象類,有三個比較重要的子類FragmentNavigator,ActivityNavigator,DialogFragmentNavigator分別用來導航Fragment,Activity,DialogFragment,還有一個子類NavGraphNavigator,用來解析我們在xml中編寫的文件,解析成一個NavGraph,並關聯NavController,顯示出第一個頁面。
- 這三個類都分別實現了父類的兩個方法,一個是
createDestination
方法用來創建一個目的地,一個是navigate
方法真正的用來導航