原文地址:
https://developer.android.google.cn/topic/libraries/architecture/navigation/
Jetpack是Android软件组件的集合,可以使你更轻松地开发出色的Android应用程序。这些组件可帮助你遵循最佳实践,免除编写样板代码并简化复杂任务,因此你可以专注于开发者更关系的业务代码。Jetpack包含androidx.*库中,与平台API分开。这意味着它提供向后兼容性并且比Android平台更频繁地更新,确保你始终可以访问最新和最好的Jetpack组件版本。
Navigation组件简化了Android应用程序中导航的实现。
任何应用内导航的目标应该是为用户提供一致且可预测的体验。为了实现这一目标,Navigation架构组件可帮助你构建符合以下每个导航原则的应用程序。
应用应该具有固定起点,即用户从启动器启动应用时看到的界面。此起点也应该是用户在按下后退按钮后返回启动器时看到的最后一个界面。
应用可能存在第一次使用时的设置界面或者登陆界面,这种特殊性的界面不应该被视为应用的起点。
应用的导航状态应使用后进先出的结构表示。此“导航堆栈”中堆栈底部为应用程序的起始界面,而栈顶为当前界面。
改变此堆栈的操作必须全部集中在栈顶,要么“pushing”一个新的目标到栈顶,要们从栈顶“poping"一个目标出栈。
起点界面中不应该出现向上按钮。当应用是通过其他应用使用deeplink的方式启动时,向上按钮应该将用户带回上层界面而不是当时启动此应用的其他应用。
当系统返回键不会导致应用程序退出时,如在应用程序的任务栈中,当前用户不处于起点界面,这个时候系统返回键就不会退出应用。这种情况下呢,我们的Up按钮操作应该和系统返回键的操作效果相同。
用户可以在起始界面进入应用程序并导航到一个目标界面。如果可以的话,用户同样可以通过deeplink,跳转到相同的目标界面。上面这两种情况下,针对目标界面,我们应该产生相同的目标堆栈。说白了就是,无论他们如何到达目标界面,用户应该能够使用“Back”或“Up”按钮,都可以在目标界面导航回到起始界面。清除已有导航栈,取而代之的是deeplink的导航栈。
Navigation架构组件简化了应用中destinations之间导航的实现。一组目标界面组成应用程序的navigation graph。
目的地是你可以在应用中导航到的任何位置。虽然目标通常是代表特定屏幕的Fragment,但Navigation架构组件支持其他目标类型:
除目标之外,导航图还在称为actions的目标之间建立连接。图1显示了一个示例应用程序的导航图的直观表示,该应用程序包含由5个操作连接的6个目标。
在创建导航图之前,必须为项目配置导航架构组件。要在Android Studio中设置项目,请执行以下步骤:
dependencies {
def nav_version = "1.0.0-alpha06"
implementation "android.arch.navigation:navigation-fragment:$nav_version" // use -ktx for Kotlin
implementation "android.arch.navigation:navigation-ui:$nav_version" // use -ktx for Kotlin
// optional - Test helpers
androidTestImplementation "android.arch.navigation:navigation-testing:$nav_version" // use -ktx for Kotlin
}
<navigation xmlns:android="http://schemas.android.com/apk/res/android">
在导航编辑器中,你可以快速构建导航图,而不是手动构建图形的XML。如下图所示,导航编辑器有三个部分:
导航编辑器的三部分如下:
创建导航图的第一步是确定应用的目的地。你可以创建空白目标或从现有项目中的Fragment和Activity创建目标。
要确定应用的目标,请使用以下步骤:
<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
app:startDestination="@id/blankFragment">
<fragment
android:id="@+id/blankFragment"
android:name="com.example.cashdog.cashdog.BlankFragment"
android:label="Blank"
tools:layout="@layout/fragment_blank" />
navigation>
有多个目的地时才能建立连接。以下是包含两个空白目标的导航图的XML:
<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
app:startDestination="@id/blankFragment">
<fragment
android:id="@+id/blankFragment"
android:name="com.example.cashdog.cashdog.BlankFragment"
android:label="fragment_blank"
tools:layout="@layout/fragment_blank" />
<fragment
android:id="@+id/blankFragment2"
android:name="com.example.cashdog.cashdog.BlankFragment2"
android:label="Blank2"
tools:layout="@layout/fragment_blank_fragment2" />
navigation>
我们通过actions来连接目的地。如下步骤连接目的地:
<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
app:startDestination="@id/blankFragment">
<fragment
android:id="@+id/blankFragment"
android:name="com.example.cashdog.cashdog.BlankFragment"
android:label="fragment_blank"
tools:layout="@layout/fragment_blank" >
<action
android:id="@+id/action_blankFragment_to_blankFragment2"
app:destination="@id/blankFragment2" />
fragment>
<fragment
android:id="@+id/blankFragment2"
android:name="com.example.cashdog.cashdog.BlankFragment2"
android:label="fragment_blank_fragment2"
tools:layout="@layout/fragment_blank_fragment2" />
navigation>
导航图编辑器在应用程序的第一个目标名称旁边放置一个房屋图标。此图标表示这是导航图中的起始目标。你可以使用以下步骤将另一个目标指定为起始目标:
Activity通过在布局中添加NavHost界面来托管应用程序的导航。 NavHost是一个空视图,当用户浏览应用程序时,目的地会被换入和换出。
在Navigation架构组件中的默认NavHost实现是NavHostFragment。
在布局中添加NavHost以后,我们需要使用navGraph属性来将我们的导航图和NavHostFragment联系起来。
<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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<fragment
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/my_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="@navigation/nav_graph"
app:defaultNavHost="true"
/>
android.support.constraint.ConstraintLayout>
上面的例子中包含“app:defaultNavHost="true"属性,这个属性保证来NavHostFragment可拦截系统返回键的事件。你可以覆写AppCompatActivity.onSupportNavigateUp()方法并调用NavController.navigateUp,如下所示:
@Override
public boolean onSupportNavigateUp() {
return Navigation.findNavController(this, R.id.nav_host_fragment).navigateUp();
}
你也可以使用NavHostFragment.create()以编程方式创建具有特定图形资源的NavHostFragment,如下例所示:
NavHostFragment finalHost = NavHostFragment.create(R.navigation.example_graph);
getSupportFragmentManager().beginTransaction()
.replace(R.id.nav_host, finalHost)
.setPrimaryNavigationFragment(finalHost) // this is the equivalent to app:defaultNavHost="true"
.commit();
使用NavController类导航到目标。可以使用以下静态方法之一拿到NavController:
拿到NavController以后,我们可以通过使用navigate()方法跳转到对应的目的地。navigate()方法接受一个resId作为参数。ID可以是导航图中特定目的地的ID或导航图中的 action ID。使用Action ID,相较于使用目的地ID有一些优势,如和导航相关的过渡效果。如下代码展示如何跳转到ViewTransactionsFragment:
viewTransactionsButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Navigation.findNavController(view).navigate(R.id.viewTransactionsAction);
}
});
Android系统维护一个包含最后访问目的地的后栈。当用户打开应用程序时,应用程序的第一个目标位于堆栈中。每次调用navigate()方法都会将另一个目标放在堆栈顶部。相反,按向上或向后按钮分别调用NavController.navigateUp()和NavController.popBackStack()方法,以从堆栈中弹出顶部目标。
对于按钮,你还可以使用Navigation类的createNavigateOnClickListener()便捷方法导航到目标:
button.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.next_fragment, null));
你可以通过在抽屉导航菜单或者是溢出菜单的xml中配置对应的目标ID来关联两者。以下代码段显示了详细信息界面目标,其ID为details_page_fragment
<fragment android:id="@+id/details_page_fragment"
android:label="@string/details"
android:name="com.example.android.myapp.DetailsFragment" />
目的地和菜单项使用同一个ID将会让两者自动关联。如下代码将展示如何让一个目的地和一个抽屉菜单关联(如menu_nav_drawer.xml)
<item
android:id="@id/details_page_fragment"
android:icon="@drawable/ic_details"
android:title="@string/details" />
下面这段xml将展示如何将一个目的地和一个溢出菜单项关联(如menu_overflow.xml)
<item
android:id="@id/details_page_fragment"
android:icon="@drawable/ic_details"
android:title="@string/details"
android:menuCategory:"secondary" />
Navigation架构组件包含一个NavigationUI类。这个类中有几个静态方法可以用来关联菜单和目的地。例如,以下代码显示如何使用setupWithNavController()方法将抽屉菜单项连接到NavigationView。
NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
NavigationUI.setupWithNavController(navigationView, navController);
有必要使用这些NavigationUI方法设置菜单驱动的导航组件,以便这些UI元素的状态与NavController的更改保持同步。
你可以通过两种方式在目标之间传递数据:使用Bundle对象或使用safe args Gradle插件以类型安全的方式传递数据。按照以下步骤使用Bundle对象在目标之间传递数据:
<fragment
android:id="@+id/confirmationFragment"
android:name="com.example.cashdog.cashdog.ConfirmationFragment"
android:label="fragment_confirmation"
tools:layout="@layout/fragment_confirmation">
<argument android:name="amount" android:defaultValue=”0” />
Bundle bundle = new Bundle();
bundle.putString("amount", amount);
Navigation.findNavController(view).navigate(R.id.confirmationAction, bundle);
接收方采用如下方式
TextView tv = view.findViewById(R.id.textViewAmount);
tv.setText(getArguments().getString("amount"));
你可以使用addOnNavigatedListener()方法将OnNavigatedListener添加到NavController。OnNavigatedListener在控制器导航到新目标时接收事件。你可以使用此处理程序进行特定于目标的更改,例如显示或隐藏某些UI元素。调用addOnNavigatedListener()时,如果当前目标存在,则立即将其发送给您的侦听器。
Navigation 架构组件有一个Gradle插件,称为safeargs,它生成简单的对象和构建器类,以便对目标和操作指定的参数进行类型安全访问。Safe args建立在Bundle方法的基础之上,但需要一些额外的代码来换取更多类型的安全性。如果你使用的是Gradle,则可以使用safe args插件。要添加此插件,请将“androidx.navigation.safeargs”插件添加到build.gradle。
apply plugin: 'com.android.application'
apply plugin: 'androidx.navigation.safeargs'
android {
//...
}
配置Gradle插件后,请按照以下步骤使用类型安全的args:
<fragment
android:id="@+id/confirmationFragment"
android:name="com.example.buybuddy.buybuddy.ConfirmationFragment"
android:label="fragment_confirmation"
tools:layout="@layout/fragment_confirmation">
<argument android:name="amount" android:defaultValue="1" app:type="integer"/>
fragment>
使用safeargs插件生成代码时,会为action以及发送和接收目标创建简单对象和构建器类。这些类是:
以下代码显示如何使用这些方法设置参数并将其传递给navigate()方法。
@Override
public void onClick(View view) {
EditText amountTv = (EditText) getView().findViewById(R.id.editTextAmount);
int amount = Integer.parseInt(amountTv.getText().toString());
ConfirmationAction action =
SpecifyAmountFragmentDirections.confirmationAction()
action.setAmount(amount)
Navigation.findNavController(view).navigate(action);
}
在接收目标的代码中,使用getArguments()方法拿到bundle并使用其内容。
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
TextView tv = view.findViewById(R.id.textViewAmount);
int amount = ConfirmationFragmentArgs.fromBundle(getArguments()).getAmount();
tv.setText(amount + "")
}
可以将一系列目的地分组为导航图中的子图。子图称为嵌套图,而包含图称为“根图”。嵌套图对于组织和重用应用程序UI的各个部分非常有用,例如单独的登录流程。与根图一样,嵌套图必须也有一个起始目标。嵌套图形封装了它的目的地;嵌套图外部的目标(例如根图上的目标)仅通过其起始目标访问嵌套图。下图显示了简单汇款应用程序的导航图。该图有两个流程:允许用户汇款的流程和允许用户查看其余额的流程。
分组目的地为嵌套图,可进行如下步骤:
<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
app:startDestination="@id/mainFragment">
<fragment
android:id="@+id/mainFragment"
android:name="com.example.cashdog.cashdog.MainFragment"
android:label="fragment_main"
tools:layout="@layout/fragment_main" >
<action
android:id="@+id/action_mainFragment_to_chooseRecipient"
app:destination="@id/sendMoneyGraph" />
<action
android:id="@+id/action_mainFragment_to_viewBalanceFragment"
app:destination="@id/viewBalanceFragment" />
fragment>
<fragment
android:id="@+id/viewBalanceFragment"
android:name="com.example.cashdog.cashdog.ViewBalanceFragment"
android:label="fragment_view_balance"
tools:layout="@layout/fragment_view_balance" />
<navigation android:id="@+id/sendMoneyGraph" app:startDestination="@id/chooseRecipient">
<fragment
android:id="@+id/chooseRecipient"
android:name="com.example.cashdog.cashdog.ChooseRecipient"
android:label="fragment_choose_recipient"
tools:layout="@layout/fragment_choose_recipient">
<action
android:id="@+id/action_chooseRecipient_to_chooseAmountFragment"
app:destination="@id/chooseAmountFragment" />
fragment>
<fragment
android:id="@+id/chooseAmountFragment"
android:name="com.example.cashdog.cashdog.ChooseAmountFragment"
android:label="fragment_choose_amount"
tools:layout="@layout/fragment_choose_amount" />
navigation>
navigation>
Navigation.findNavController(view).navigate(R.id.action_mainFragment_to_sendMoneyGraph);
<include app:graph="@navigation/included_graph"/>
在Android中,deep link是指向应用中特定目标的URI。当你希望跳转到特定目的地以在应用中执行某项任务
时,这些URI非常有用,例如汇款流程,允许用户快速汇款给某人。
要在导航图中为目标添加Deep Link,请执行以下操作:
<deepLink app:uri="https://cashdog.com/sendmoney"/>
你必须在manifest.xml文件增加额外内容来启用Deep Link。
<activity name=".MainActivity">
<nav-graph android:value="@navigation/main_nav" />
</activity>
作为清单合并构建步骤的一部分,此元素将替换为匹配导航图中所有deeplink所需的生成的元素
你可以使用NavDeepLinkBuilder类构造PendingIntent,将用户带到特定目标界面。
触发此deep link时,将清除任务后台堆栈并替换为deep link目标界面。
嵌套图形时,每个嵌套级别的起始目标(即层次结构中每个元素的起始目标)也会添加到堆栈中。
你可以使用NavDeepLinkBuilder(Context)直接构造PendingIntent,如下例所示。请注意,如果提供的上下文不是Activity,则构造函数使用PackageManager.getLaunchIntentForPackage()作为要启动的默认Activity(如果可用)。
PendingIntent pendingIntent = new NavDeepLinkBuilder(context)
.setGraph(R.navigation.mobile_navigation)
.setDestination(R.id.android)
.setArguments(args)
.createPendingIntent();
添加方式和上面添加deep link或者action操作类似,操作结果代码如下:
<fragment
android:id="@+id/specifyAmountFragment"
android:name="com.example.buybuddy.buybuddy.SpecifyAmountFragment"
android:label="fragment_specify_amount"
tools:layout="@layout/fragment_specify_amount">
<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" />
fragment>
除了过渡动画之外,Navigation架构组件还支持在目标之间添加共享元素过渡。
与动画不同,共享元素是以编程方式提供的,而不是通过导航XML文件提供的,因为它们需要引用你希望包含在共享元素转换中的View实例。
每种类型的目标都通过Navigator.Extras接口的子类实现此编程API。 Extras被传递给navigate()的调用。
FragmentNavigator.Extras类允许您将共享元素附加到对Fragment目标的navigate()调用,如下例所示:
FragmentNavigator.Extras extras = new FragmentNavigator.Extras.Builder()
.addSharedElement(imageView, "header_image")
.addSharedElement(titleView, "header_title")
.build();
Navigation.findNavController(view).navigate(R.id.details,
null, // Bundle of args
null, // NavOptions
extras);
活依赖于ActivityOptionsCompat来控制共享元素转换,详见使用共享元素文档启动活动,如下例所示