本节教程我将带大家来一步步实现主页的框架,一个Bottom Navigation框架,然后介绍Navigation的相关知识。
本节教程您学习到如下主要内容:
- BottomNavigation的搭建和原理介绍
- Navigation的的传值
- Navigation跳转动画的实现
- Navigation文件的拆分
- Deeplink导航的实现
搭建 Bottom Navigation Activity
让我们开始吧,我们先搭建APP的主框架。界面如下所示:
界面主要有三部分组成,顶部的标题栏,中间的显示内容的区域和底下的页面切换的一些按钮。用专业的术语描述就是分为顶部的ActionBar(ToolBar),中间部分的NavHostFragment,底部的 BottomNavigationView 三部分。
新建5个Fragment
这5个Fragment分别是 发现, 视频
, 我的, 云村, 账号 五个模块的入口页面。
创建方式如下:
- 使用New -> Activity -> Empty Activity 新建一个名为
MainActivity.kt
的Activity
- 通过New -> Fragment -> Fragment(Blank) 新建5个
Fragment
,分别命名为DiscoveryMainFragment.kt
,VideoMainFragment.kt
,MineMainFragment.kt
,CloudMainFragment.kt
,AccountMainFragment.kt
, 。 - 删掉Fragment中不必要的代码,只留下
onCreateView
方法,示例如下:
class DiscoveryMainFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_discovery_main, container, false)
}
}
- 修改Fragment的显示内容,将FrameLayout改为ConstraintLayout,将TextView居中显示,然后文字的
text
改为对应的界面的名字
新建 Menu
新建一个Menu作为底部导航视图BottomNavigationView的Menu,用来切换5个模块的显示。
创建方式如下:
- res -> New -> Android Resources File, 在弹出框内的File Name 填入 tab_menu, Resource type 选择 Menu。这样如果没有Menu文件夹会创建一个文件夹,然后在Menu文件夹下生成一个 tab_menu.xml文件
- 给Menu 添加5个Menu Item, 设置id,文字和图片
组装 BottomNavigationView
- 在activity_main.xml中拖入一个BottomNavigationView, 约束设置为高度包裹内容宽度填充父视图。
- 设置itemBackground的值为BottomNavigationView添加背景颜色
- 给itemIconTint设置一个颜色选择器,这样选中后和未选中的图片的颜色可以切换
- 给itemTextColor设置一个颜色选择器,这样选中后和未选中的图片的颜色可以切换,和itemIconTint的值最好是一致的,这样更符合审美
- 设置 labelVisibilityMode 为 labeled,表示MenuItem文字总是显
默认的情况下超过三个,只有选中的那个Item的文字才显示,不符合我们的设计要求。
- 设置 Menu 为我们上面创立的tab_menu
设置和最后的展示效果如下图:
创建和设置Navigation
Navigation文件中定义了destination(Fragment),action(跳转路径),界面间的参数(arguments) 以及deeplink等内容,接下来我们一步步的了解这些内容。
- res -> New -> Android Resources File, 在弹出框内的File Name 填入 main_navigation, Resource type 选择 Navigation。这样如果没有Navigation文件夹会创建一个文件夹,然后在Navigation文件夹下生成一个 main_navigation.xml文件
- 将5个Fragment添加进main_navigation.xml文件来
由于5个Fragment是独立的,没有相互跳转的关系,所以直接添加进来就行。后面会介绍界面跳转的定义
注意:这里设置的id不是随便设置的哦,是需要和Menu item的id是一一对应的哦。否则会导致无法实现页面切换。
添加完成后的结果如下
添加ToolBar 和 NaviGraph
- 回到activity_main.xml文件中来,我们添加一个ToolBar做为ActionBar,放置在顶部。
说明:把ActionBar替换为ToolBar的目的是为了能更好的对标题栏进行高度定制化,且ToolBar在各种Android版本的样式也很统一。
- 在ToolBar 和 BottomNavigationView 之间的剩余空间放入 NavHostFragment,NavHostFragment的NaviGraph 选择前面建好的main_navigation
说明:NavHostFragment就是其他布局文件引入Navigation文件的容器,Navigation中的Fragment等的内容能在这里面显示
进行Navigation功能装配
先运行下程序,看看效果。
发现的页面的内容是能够正常显示了,但是点击底部的Menu没有切换效果,接下来我们就来解决这个问题。
- 打开
MainActivity
,添加两个属性 navController 和 appBarConfiguration
// 1
private val navController by lazy { findNavController(R.id.fragment) }
// 2
private val appBarConfiguration by lazy { AppBarConfiguration.Builder(bottomNavigationView.menu).build() }
-
navController
的用来负责导航的对象,也就是切换Fragment和管理导航栈(Navigation Stack),R.id.fragment
是布局文件中NavHostFragment设置的id -
appBarConfiguration
是来定义哪些Fragment处于导航栈的顶层,这样navController
就能正确的处理导航栈。bottomNavigationView.menu
就是前面创建的tab_menu
- 接下来添加ToolBar和组装Navigation
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 1
setSupportActionBar(toolbar)
// 2
setupNavigation()
}
/* 导航控件的实现 */
private fun setupNavigation() {
// 2.1
setupActionBarWithNavController(navController, appBarConfiguration)
// 2.2
bottomNavigationView.setupWithNavController(navController)
}
- 把布局文件中的
toolbar
设置为ActionBar,切换Fragment对应的标题就能自动显示在toolbar
上 -
setupNavigation
方法中的代码设置后 5个Fragment能导航到其他页面 和 底部Menu点击后5个Fragment之间的切换
再次运行下程序,发现切换的功能正常了。
知识点拾遗
细心的读者可能发现了,由于我们的主题色是白色,造成了状态栏的文字和图片都无法识别。
解决这个问题可以在style文件中添加如下的设置,然后状态栏的文字和图片就变成了深色。
- true
导航界面跳转
目前为止,实现了5个主页面之间的切换,接下来我们介绍如何实现从主界面跳转到二级界面,三级界面等其他界面。。。
- 按照前面新建Fragment的步骤,新建一个名为DiscoverySecondaryFragment的页面作为二级页面,新建一个名为DiscoveryThirdFragment的页面作为三级页面
- 将这两个文件加入到main_navigation文件中
- id分别命名为 discovery_secondary_fragment, discovery_third_fragment,
- label分别命名为发现二级界面和发现三级界面
- 将发现页面和发现二级界面之间连接起来,发现二级页面和发现三级界面之间连接起来,他们之间的连线会有一个action id
- 代码设置跳转
- 直接通过**Fragment id **进行跳转
在DiscoveryMainFragment添加如下代码
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
go_to_discovery_second.setOnClickListener {
// 1
findNavController().navigate(R.id.discovery_secondary_fragment)
}
}
提示:
R.id.discovery_secondary_fragment
就是发现二级界面的id
- 通过刚才**Action id **进行跳转
将刚才标记1处的代码替换成
findNavController().navigate(R.id.action_discovery_main_fragment_to_discovery_secondary_fragment)
提示:
R.id.action_discovery_main_fragment_to_discovery_secondary_fragment
就是刚才连线界面的时候自动生成的Action id
二级和三级界面的逻辑同理,不再做介绍. 效果如下
- 解决发现的两个问题
首先,我们虽然跳过去了,但是点击ToolBar左侧的返回按钮没法返回到上个页面,我们需要覆写MainActivity的onSupportNavigateUp
方法
override fun onSupportNavigateUp(): Boolean {
return super.onSupportNavigateUp() || navController.navigateUp()
}
还有一个问题就是,底部的BottomNavigationView应该只有在显示5个主页面的时候才会显示,其他的页面应该是不能看到和选择的。我们可以在setupNavigation
添加导航的监听器
/* 导航控件的实现 */
private fun setupNavigation() {
// ...
// 导航监听
navController.addOnDestinationChangedListener { _, destination, _ ->
if (destination.id in arrayOf(
R.id.discovery_main_fragment,
R.id.video_main_fragment,
R.id.mine_main_fragment,
R.id.cloud_main_fragment,
R.id.account_main_fragment
)) {
bottomNavigationView.visibility = View.VISIBLE
} else {
bottomNavigationView.visibility = View.GONE
}
}
}
修改后的结果如下
Navigation传值
我们接下来介绍下导航的时候如何传参。我们先修改下界面,发现页面添加一个TextView, 在这个页面输入的内容跳转到第二个页面的时候被带过去。需求如下图所示:
普通传值方式
开始吧,流程如下:
- 选中发现二级界面,在Attributes -> Arguments 添加 "name" 字段的参数名,内容可空
- 第一个界面代码中实现跳转带上参数
修改DiscoveryMainFragment中点击事件的方法
go_to_discovery_second.setOnClickListener {
val etTitle = editTextTextPersonName.text.toString()
if (TextUtils.isEmpty(etTitle)) {
// 1
findNavController().navigate(R.id.action_discovery_main_fragment_to_discovery_secondary_fragment)
} else {
// 2
Bundle().also {
it.putString("name", etTitle)
findNavController().navigate(R.id.action_discovery_main_fragment_to_discovery_secondary_fragment, it)
}
}
}
上面代码的意思是如果没有输入则不需要传参,如果有输入,则把参数封装在Bundle中传递过去。
- 第二个界面代码中实现接收参数
DiscoveryScondaryFragment中添加如下代码
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// ...
arguments?.getString("name")?.let {
name_tv.text = "您输入的名字为:$it"
} ?: let {
name_tv.text = "您未输入名字"
}
}
代码逻辑也很明白,从arguments中去取字段为name的值。
最后效果如下:
使用Safe Args传值
Safe Args是一个Gradle插件,可以强制页面跳转的时候传递正确的参数格式。
- 开始引入 Safe Args
使用Safe Args 需要在app的build.gradle中引入插件
apply plugin: 'kotlin-android-extensions'
由于需要Java8的支持,所以需要在app的build.gradle中android 函数下添加如下代码
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
此外需要的 project 的 dependncies 中指定路径
def nav_version = "2.3.0"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
- 修改代码
- 第一个界面代码中实现跳转带上参数
var etTitle = editTextTextPersonName.text.toString()
if (TextUtils.isEmpty(etTitle)) {
etTitle = ""
}
val toDirection = DiscoveryMainFragmentDirections.actionDiscoveryMainFragmentToDiscoverySecondaryFragment(etTitle)
findNavController().navigate(toDirection)
Safe Args如果没有值需要设置给一个默认值,否则会崩溃
说明:DiscoveryMainFragmentDirections 和 actionDiscoveryMainFragmentToDiscoverySecondaryFragment 都是插件自动生成,不需要手动创建
DiscoveryMainFragmentDirections = DiscoveryMainFragment + Directions,代表从 DiscoveryMainFragment 开始跳转的导航
actionDiscoveryMainFragmentToDiscoverySecondaryFragment 是 action id 的驼峰命名法,代表的导航到哪个Fragment
方法的传参就是 name, 如果需要多个参数,则是生成的需要传递多个参数的方法。
- 第二个界面代码中实现接收参数
在第二个页面定义一个args属性
private val args: DiscoverySecondaryFragmentArgs by navArgs()
说明:DiscoverySecondaryFragmentArgs 也是插件自动生成,不需要手动创建
DiscoverySecondaryFragmentArgs = DiscoverySecondaryFragment + Args, 代表DiscoverySecondaryFragment 得到的上个页面传递过来参数的类
args 里面有 name 的值
通过如下代码获取相应值。
val (picture) = args
if (picture === "") {
name_tv.text = "您未输入名字"
} else {
name_tv.text = "您输入的名字为:$picture"
}
Navigation跳转动画
为了不和前面的内容混淆,我们新建一个VideoSecondaryFragment, 然后连接一个Action从VideoMainFragment到VideoSecondaryFragment。然后修改下界面内ring,界面如下图所示:
给 Action 设置 animation
我们首先来实现平移动画,动画方式是进入下个页面时,两个页面内容从右往左移,;返回上个页面的时候两个页面内容从左往右移。
- 首先我们定义四个动画
// left_to_right_enter_anim
// left_to_right_exit_anim
// right_to_left_enter_anim
// right_to_left_exit_anim
- 将这四个动画设置给 action_video_main_fragment_to_video_secondary_fragment
选中action, 然后给enterAnim,exitAnim,popEnterAnim和popExitAnim选择相应的动画文件。
最后的效果如下 :
共享元素变换(Shared Element Transition)
共享元素变换给人某个UI元素能在两个Fragment/Activity之间共用的假象,能实现一个比较流畅的转场效果,给用户的感觉比较酷炫。
共享元素变换的概念我这里不做过多介绍,可以参考相关的开发文档。
我这里来实现一个图片从小到全屏的转场过程。
- 定义一个Transition 文件
res -> New -> Android Resources File, 在弹出框内的File Name 填入 shared_element_transition, Resource type 选择 Transition。这样如果没有Transition文件夹会创建一个文件夹,然后在Transition文件夹下生成一个 shared_element_transition.xml文件
我们这儿定义的是一个changeBounds转场。
- 在VideoMainFragment中对按钮添加点击事件
share_btn.setOnClickListener {
// 1
ViewCompat.setTransitionName(cat_iv, "cat")
// 2
val extra = FragmentNavigatorExtras(
cat_iv to "cat"
)
// 3
val mainDirection = VideoMainFragmentDirections.actionVideoMainFragmentToVideoSecondaryFragment()
// 4
findNavController().navigate(mainDirection, extra)
}
这段代码的作用是:
- 给
cat_iv
这个ImageView设置一个TranSitionName - 构造一个FragmentNavigatorExtras对象,这个对象从名称来看就知道是给Navigator提供的,而不是下个页面。因为共享元素变换是交给Navigator来处理,它需要知道哪个View需要动画,以及动画的名称
- 这个和前面传参章节介绍的一样,因为不需要传参,所以不需要传值
- 开始导航
- 在VideoSecondaryFragment中设置共享元素和动画
在VideoSecondaryFragment添加如下代码
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 1
sharedElementEnterTransition = TransitionInflater.from(context)
.inflateTransition(R.transition.shared_element_transition)
// 2
ViewCompat.setTransitionName(cat_iv, "cat")
}
这段代码的作用是:
- 设置sharedElementEnterTransition
- 给
cat_iv
这个ImageView设置同样的TranSitionName
所有的工作就完成了,动画就开始了。
提示:有些需求图片是从网络中回去的,所以会涉及到延迟动画
postponeEnterTransition()
, 等待网络请求完成再调用开始动画startPostponedEnterTransition()
这一过程。
导航文件拆分
细心的你可能会发现一个问题,如果项目越来越大,页面越来越多,所有的都放在一个文件中会非常的臃肿,当Action非常复杂的时候会非常的混乱。
所以你肯定会提出一个问题,是否能把页面拆分到多个导航文件中去。Google已经给了肯定的答案。
- 新建 Navigation文件
我们先创建一个ad_navigation.xml,里面加入一个页面AdFragment页面,界面展示如下:
- 导入到 主 Navigation 文件
导入后就可以直接使用了。使用方式和前面介绍的类似。
findNavController().navigate(R.id.ad_navigation)
Deeplink导航
Navigation也能通过Deeplink进行跳转。接下来我们介绍下如何实现Deeplink跳转。
- 首先为广告页面添加一个deeplink
为广告页面添加一个JJMusic://ad.fragment的deeplink
- 使用
findNavController().navigate(Uri.parse("JJMusic://ad.fragment"))
结尾
由于本节内容稍微有点多,提取下一节专门介绍ToolBar的功能,介绍Navigation跳转时切换时ToolBar的右上角按钮的切换和使用。