前言(可跳过)
关于jetpack部分不在赘述 . "Navigation 是一个框架,用于在 Android 应用中的“目标”之间导航,不论目标是作为 Fragment、Activity 还是其他组件实现,该框架都会提供一致的 API。" 主要由以下三个关键部分组成: 官网介绍
- Navigation graph:xml文件 添加导航图。
- NavHost:显示导航图中目标的空白容器。导航组件包含一个默认 NavHost 实现 (NavHostFragment),可显示 Fragment 目标。
- NavController:在 NavHost 中管理应用导航的对象。当用户在整个应用中移动时,NavController 会安排 NavHost 中目标内容的交换。
- Navigation 能出处理那些东西或者优势是什么?
1.处理 Fragment 事务
2.默认情况下,正确处理往返操作。
3.为动画和转换提供标准化资源。 (动画一会体验一哈)
4.包括导航界面模式(例如抽屉式导航栏和底部导航),用户只需完成极少的额外工作。 (以下实例为底部导航)
5.Safe Args - 可在目标之间导航和传递数据时提供类型安全的 Gradle 插件。(后面有总结)
6.ViewModel 支持 - 您可以将 ViewModel 的范围限定为导航图,以在图表的目标之间共享与界面相关的数据。(这个不适用Navigation 常规的底部导航或者Activity+多个Fragment直接也能共享数据 fragment+dialogfragment 设置targetfragment viewmodel也能实现共享数据 这个后面就不总结了)
下面是BottomNavigationView+Navigation 快速实现底部导航栏的小栗子. 关于底部导航栏的实现方式 前人已经总结的很好了这里不在赘述 依然范特稀西-Android 底部导航栏 (底部 Tab) 最佳实践
快速开始(可跳过)
注意:如果您要在 Android Studio 中使用 Navigation 组件,则必须使用 Android Studio 3.3 或更高版本。
- 添加依赖
dependencies {
def nav_version = "2.3.0-alpha05"
// Kotlin
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
// For BottomNavigationView from Material Components 演示使用
implementation 'com.google.android.material:material:1.2.0-alpha02'
}
- 创建导航图(Create a navigation graph) xml资源文件
1. Project切换Android, 右键 NEW -> Android Resource File
2. file name字段中输入名称 按照惯例或者命名规范 命名为 main_nav_graph
3. 从 Resource type 下拉列表中选择 Navigation OK.
main_nav_menu.xml如下
<?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"
tools:ignore="UnusedNavigation">
</navigation>
-
点击加号 依次添加HomeFragment ListFragment MineFragment(自己创建的)
-
切换Code视图代码自动添加成如下 注意 startDestination 默认第一个
-
回过头看一下xml 文件
<!--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">
<!--
android:name 属性包含 NavHost 实现的类名称。
app:navGraph 属性将 NavHostFragment 与导航图相关联。导航图会在此 NavHostFragment 中指定用户可以导航到的所有目的地。
app:defaultNavHost="true" 属性确保您的 NavHostFragment 会拦截系统返回按钮。
请注意,只能有一个默认 NavHost。如果同一布局(例如,双窗格布局)中有多个主机,请务必仅指定一个默认 NavHost。
个人理解
android:name="androidx.navigation.fragment.NavHostFragment" 写死 感兴趣的可以去看看源码
app:defaultNavHost="true" true 拦截返回键
app:navGraph xml文件
-->
<fragment
android:id="@+id/navHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/bottomNav"
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost="true"
app:navGraph="@navigation/main_nav_graph" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomNav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:menu="@menu/bottom_nav_menu" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!--main_nav_menu.xml-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/home"
android:icon="@drawable/ic_home"
android:contentDescription="@string/title_home"
android:title="@string/title_home" />
<item
android:id="@+id/list"
android:icon="@drawable/ic_list"
android:contentDescription="@string/title_list"
android:title="@string/title_list" />
<item
android:id="@+id/mine"
android:icon="@drawable/ic_feedback"
android:contentDescription="@string/title_mine"
android:title="@string/title_mine" />
</menu>
-
NavController 在 NavHost 中管理应用导航的对象
- NavHost 点进去看源码
public interface NavHost { //... @NonNull NavController getNavController(); }- NavController? 接着看
public class NavController { @NonNull public NavDeepLinkBuilder createDeepLink() { return new NavDeepLinkBuilder(this); } /** * Saves all navigation controller state to a Bundle. * * <p>State may be restored from a bundle returned from this method by calling * {@link #restoreState(Bundle)}. Saving controller state is the responsibility * of a {@link NavHost}.</p> * * @return saved state for this controller */ @CallSuper @Nullable public Bundle saveState() { Bundle b = null; ArrayList<String> navigatorNames = new ArrayList<>(); Bundle navigatorState = new Bundle(); for (Map.Entry<String, Navigator<? extends NavDestination>> entry : mNavigatorProvider.getNavigators().entrySet()) { String name = entry.getKey(); Bundle savedState = entry.getValue().onSaveState(); if (savedState != null) { navigatorNames.add(name); navigatorState.putBundle(name, savedState); } } if (!navigatorNames.isEmpty()) { b = new Bundle(); navigatorState.putStringArrayList(KEY_NAVIGATOR_STATE_NAMES, navigatorNames); b.putBundle(KEY_NAVIGATOR_STATE, navigatorState); } if (!mBackStack.isEmpty()) { if (b == null) { b = new Bundle(); } Parcelable[] backStack = new Parcelable[mBackStack.size()]; int index = 0; for (NavBackStackEntry backStackEntry : mBackStack) { backStack[index++] = new NavBackStackEntryState(backStackEntry); } b.putParcelableArray(KEY_BACK_STACK, backStack); } if (mDeepLinkHandled) { if (b == null) { b = new Bundle(); } b.putBoolean(KEY_DEEP_LINK_HANDLED, mDeepLinkHandled); } return b; } @CallSuper public void restoreState(@Nullable Bundle navState) { if (navState == null) { return; } navState.setClassLoader(mContext.getClassLoader()); mNavigatorStateToRestore = navState.getBundle(KEY_NAVIGATOR_STATE); mBackStackToRestore = navState.getParcelableArray(KEY_BACK_STACK); mDeepLinkHandled = navState.getBoolean(KEY_DEEP_LINK_HANDLED); } void setLifecycleOwner(@NonNull LifecycleOwner owner) { mLifecycleOwner = owner; mLifecycleOwner.getLifecycle().addObserver(mLifecycleObserver); } void setOnBackPressedDispatcher(@NonNull OnBackPressedDispatcher dispatcher) { if (mLifecycleOwner == null) { throw new IllegalStateException("You must call setLifecycleOwner() before calling " + "setOnBackPressedDispatcher()"); } // Remove the callback from any previous dispatcher mOnBackPressedCallback.remove(); // Then add it to the new dispatcher dispatcher.addCallback(mLifecycleOwner, mOnBackPressedCallback); } void setViewModelStore(@NonNull ViewModelStore viewModelStore) { if (!mBackStack.isEmpty()) { throw new IllegalStateException("ViewModelStore should be set before setGraph call"); } mViewModel = NavControllerViewModel.getInstance(viewModelStore); }主要是处理一些返回事件 关联LifecycleObserver 状态恢复restoreState Bundle saveState数据传参处理 感兴趣的可以看一下。继续看官网文档
- Navigate to a destination 导航实现跳转的几个方法如下
Kotlin: Fragment.findNavController() View.findNavController() Activity.findNavController(viewId: Int) Java: NavHostFragment.findNavController(Fragment) Navigation.findNavController(Activity, @IdRes int viewId) Navigation.findNavController(View)
demo走起 MainActivity中
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
bottomNav.setOnNavigationItemSelectedListener{item->
when(item.itemId){
R.id.home->{
//方法点进入会发现kotlin 内联方法
findNavController(R.id.navHostFragment).navigate(R.id.homeFragment)
}
R.id.list->{
findNavController(R.id.navHostFragment).navigate(R.id.listFragment)
}
R.id.mine->{
findNavController(R.id.navHostFragment).navigate(R.id.mineFragment)
}
}
true
}
}
}
不等你提出质疑 就这?就这?就这?
果断先跑一下
跑出了hello world的开心
- 顺着文档往下看 发现NavigationUI组件 使用也很简单将上述代码改为
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// bottomNav.setOnNavigationItemSelectedListener{item->
// when(item.itemId){
// R.id.homeFragment->{
// findNavController(R.id.navHostFragment).navigate(R.id.homeFragment)
// }
// R.id.listFragment->{
// findNavController(R.id.navHostFragment).navigate(R.id.listFragment)
// }
// R.id.mineFragment->{
// findNavController(R.id.navHostFragment).navigate(R.id.mineFragment)
// }
// }
// true
// }
bottomNav.setupWithNavController(findNavController(R.id.navHostFragment))
}
}
一开始发现点击死活都没反应 后来发现要将menu中的item id要和navigation中fragment中的id保持一致。这点需要注意哈
- 回过头处理小细节性的东西
main_nav_graph.xml中
fragment android:label 标签是actionBar显示的情况下标题,如果看到切换标题未随之切换 还需要在activity中设置 setupActionBarWithNavController(navController) 即可。不过国内这种设计几乎可以不考虑 大部分隐藏了supportActionBar?.hide() 即可。
-
跳转处理
通过小栗子我们知道了activity跳转fragment的场景 其他场景怎么跳转
1.fragment跳转fragment
-
1.进入nav_graph.xml 导航视图文件,把要跳转的fragment加入进来 然后在homefragment 右侧拖动箭头指向要跳转的fragment.切到code视图xml如下
会发现导航fragment里多了action标签指向目的地fragment.同时我们在切换Design会发现action已经有内容了
点开内容如下
既然如此 我们试着直接点击action添加试试看 首先我们新建Mine2Fragment 然后点击加号 NEW Destination.点击导航fragment Actions 在Destinations找到我们刚才添加进来的目的地fragment选中 之后Id 会自动帮我们填上.同时我们会发现我们可以设置转场动画 启动方式等。感兴趣的可以自己试试 跳转代码 findNavController().navigate(R.id.list2Fragment) 具体见demo
-
-
关于动画
- 1.action 添加动画
<fragment android:id="@+id/listFragment" android:name="com.pan.navigationdemo.ListFragment" android:label="ListFragment" tools:layout="@layout/list_frag"> <!-- 值得注意的是api跳转的时候 findNavController().navigate(R.id.action_listFragment_to_list2Fragment) id 指的是action id 而非 目的地fragment的id 否则动画效果无效 app:enterAnim="@anim/fragment_open_enter" app:exitAnim="@anim/fragment_close_exit" --> <action android:id="@+id/action_listFragment_to_list2Fragment" app:destination="@id/list2Fragment" /> </fragment>2.在目的地之间添加过渡效果 代码示例:
ListFragment 中 doAction.setOnClickListener { // Fragment 目的地共享元素过渡 // https://developer.android.com/guide/navigation/navigation-animate-transitions#fragment_destination_shared_element_transitions val extras = FragmentNavigatorExtras(doAction to "textView") findNavController().navigate(R.id.list2Fragment, null, // Bundle of args null, // NavOptions extras) // findNavController().navigate(R.id.action_listFragment_to_list2Fragment) 转场动画 } list_frag.xml <TextView android:id="@+id/doAction" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="点我跳List2Fragment" android:transitionName="textView" android:textSize="25sp" /> list2_frag.xml <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_marginTop="100dp" android:text="List2Fragment" android:textSize="25sp" android:transitionName="textView" /> 注意android:transitionName="textView" 两边都要设置相同的键 ``` -
然后到传参这块
传参这块自从有了livedata+viewmodel之后方便了太多
1.单个Activity+多个fragment viewmodel实现订阅 2.fragment+dialogfragment setTargetFragment viewmodel订阅Navigation怎么传呢?
1.xml 目的地fragment 设置argument 然后点击rebuild <fragment android:id="@+id/listFragment" android:name="com.pan.navigationdemo.ListFragment" android:label="ListFragment" tools:layout="@layout/list_frag"> <action android:id="@+id/action_listFragment_to_list2Fragment" app:destination="@id/list2Fragment" app:enterAnim="@anim/fragment_open_enter" app:popExitAnim="@anim/fragment_close_exit" > </action> </fragment> <!--目的地fragment 设置argument 然后点击rebuild--> <fragment android:id="@+id/list2Fragment" android:name="com.pan.navigationdemo.List2Fragment" android:label="List2Fragment" > <!-- 具体参考 https://developer.android.google.cn/guide/navigation/navigation-pass-data?hl=zh-cn#Safe-args--> <argument android:name="testArgs" app:argType="string" android:defaultValue="hello pp." />2.使用 Safe Args 传递安全的数据 配置环境不说了 文档很详细配置完 在xml添加完argument clear rebuild project.传参代码如下: class ListFragment : BaseFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) doAction.setOnClickListener { //==================== 传参 方式一 // ListFragmentDirections 编译生成的 不行的话 退出重新打开Android Studio或 clear rebuild一下试试. //如果还不行 // apply plugin: 'com.android.application' //apply plugin: 'kotlin-android' //apply plugin: 'kotlin-android-extensions' //apply plugin: 'kotlin-kapt' //apply plugin: 'androidx.navigation.safeargs.kotlin' 顺序放在最下面 // val action = ListFragmentDirections.actionListFragmentToList2Fragment("方式一") // findNavController().navigate(action) //==================== 传参 方式二 Bundle val bundleOf = bundleOf("testArgs" to "方式二", "xx" to "xxx") findNavController().navigate(R.id.action_listFragment_to_list2Fragment,bundleOf) } } } 目的地fragment如下: X+Args 自动生成的文件 前提是你在目的地fragment xml文件中先设置过argument才行 //by navArgs() 错误提示 Cannot inline bytecode built with JVM target 1.8 into bytecode that is being built with JVM target 1.6. Please specify proper '-jvm-target' option 需要在gradle中设置如下 compileOptions { targetCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8 } //Cannot inline bytecode built with JVM target 1.8 into bytecode that is being built with JVM target 1.6. Please specify proper '-jvm-target' option kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } // val args:List2FragmentArgs by navArgs() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) //方式一 Bundle 直接拿 textView.text = arguments?.getString("testArgs") //方式二 List2FragmentArgs // textView.text = args.testArgs } -
deep link 为目的地创建深层链接
在 Android 中,深层链接是指将用户直接转到应用内特定目的地的链接。借助 Navigation 组件,您可以创建两种不同类型的深层链接:显式深层链接和隐式深层链接。 先抛开理论性的东西。我们知道了这块大概有显示和隐式链接两个概念。
1.Create an explicit deep link(创建显示链接)
显式深层链接是深层链接的一个实例,该实例使用 PendingIntent 将用户转到应用内的特定位置。 例如,您可以在通知、应用快捷方式或应用微件中显示显式深层链接。
您可以使用 NavDeepLinkBuilder 类构建 PendingIntent,如下例所示。请注意,如果提供的上下文不是 Activity,构造函数会使用 PackageManager.getLaunchIntentForPackage() 作为默认 Activity 来启动(如果有)。(官网)
应用场景: 通知、应用快捷方式或应用微件中显示显式深层链接。
关键词: NavDeepLinkBuilder 类构建 PendingIntent.
PendingIntent类相信大家都不陌生(都是知识点啊 陌生的话花个几分钟查一下),主要三个方法PendingIntent.getActivities(), PendingIntent.getService(),PendingIntent.getBroadcast().大家用的最多的就是通知场景了 下面一段通知的小代码:
list2_frag.xml中 添加一个点击事件 点击生成通知 点击通知跳转AlertDetailsActivity <!-- 为目标创建深层链接 --> <TextView android:id="@+id/deepLink" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="deep link" android:textSize="25sp" android:transitionName="textView" /> List2Fragment中生成点击事件 //为目标创建深层链接 val channel_name = getString(R.string.app_name) deepLink.setOnClickListener { val notificationId = System.currentTimeMillis().toInt() val intent = Intent() val mainIntent = Intent() intent.setClass(requireContext(), AlertDetailsActivity::class.java) mainIntent.setClass(requireContext(), MainActivity::class.java) mainIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP val pendingIntent = PendingIntent.getActivities( requireContext(), 0, arrayOf(mainIntent, intent), PendingIntent.FLAG_UPDATE_CURRENT ) var builder: NotificationCompat.Builder if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val importance = NotificationManager.IMPORTANCE_HIGH val channel = NotificationChannel(notificationId.toString(), channel_name, importance).apply { description = "我曾经听人讲过 当你不可以再拥有的时候 你唯一可以做的 就是令自己莫忘记" } val notificationManager: NotificationManager = requireActivity().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.createNotificationChannel(channel) builder = NotificationCompat.Builder(requireContext(), notificationId.toString()) .setSmallIcon(android.R.drawable.ic_popup_reminder) .setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.ic_launcher_background)) .setContentTitle("My notification") .setContentText("Hello World!") .setStyle(NotificationCompat.BigTextStyle().bigText("我曾经听人讲过 当你不可以再拥有的时候 你唯一可以做的 就是令自己莫忘记")) .setContentIntent(pendingIntent) .setAutoCancel(true) //点击自动取消 不设置需要滑动删除 } else { builder = NotificationCompat.Builder(requireContext(), channel_name) .setSmallIcon(android.R.drawable.ic_popup_reminder) .setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.ic_launcher_background)) .setContentTitle("My notification") .setContentText("Hello World!") .setPriority(NotificationCompat.PRIORITY_MAX) //设置优先级 .setStyle(NotificationCompat.BigTextStyle().bigText("我曾经听人讲过 当你不可以再拥有的时候 你唯一可以做的 就是令自己莫忘记")) .setContentIntent(pendingIntent) .setAutoCancel(true) //点击自动取消 不设置需要滑动删除 } with(NotificationManagerCompat.from(requireContext())) { notify(notificationId, builder.build()) } }上面同样的需求用NavDeepLinkBuilder生成的pendingIntent实现
-
创建名为 x_nav_garpg.xml(命名规范)的导航视图文件 将目的地activity加进来即可。传参代码如下
<?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/alertdetails_nav_graph.xml" app:startDestination="@id/alertDetailsActivity" tools:ignore="UnusedNavigation"> <activity android:id="@+id/alertDetailsActivity" android:name="com.pan.navigationdemo.AlertDetailsActivity" android:label="AlertDetailsActivity"> <argument android:name="textView" android:defaultValue="hello pp." app:argType="string" /> </activity> </navigation> 通知代码pendingIntent替换为 val bundle = Bundle().apply { putString("textView","我曾经听人讲过 当你不可以再拥有的时候 你唯一可以做的 就是令自己莫忘记") } val pendingIntent = NavDeepLinkBuilder(requireContext()) .setGraph(R.navigation.main_nav_graph) .setDestination(R.id.mineFragment) .setArguments(bundle) .createPendingIntent() 目的地activity接受参数为 private val args by navArgs<AlertDetailsActivityArgs>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.alertdetails_act) // textView.text = intent.getStringExtra("textView") textView.text = args.textView } -
-
创建隐式深层链接
- 隐式深层链接是指应用中特定目的地的 URI。调用 URI(例如,当用户点击某个链接)时,Android 可以将应用打开到相应的目的地。
- 这块之前看官方文档即可。测试参考这篇文章 Deeplinks with Auth and Backstack Management on Android
-
Demo地址 NavigationDemo
总结
1.下次尽量录屏
2.还是不太深入 比如如何动态化实现导航 如果全部都要在xml中添加的话感觉不太舒服
感谢
2.官方文档
3.底部Tab的历史演变以及前人总结 值得花两分钟浏览 依然范特稀西-Android 底部导航栏 (底部 Tab) 最佳实践