去年年底,我开始着手写THUInfo。经过一个多月的磕磕绊绊,我也总算是做出了一点东西——虽然我也知道,这距离一个成熟的APP还有很长一段距离,更何况我也只做出了Android版本。
眼瞅着就要进入常规的学习状态了,各门课的老师未见其人,已见其任务,THUInfo的开发可以暂缓一段时间了。回顾这一个多月,编程水平不见得有多少提升,奇怪的知识倒是增加了不少。我觉得自己有必要将一些内容记录下来,即使没人看,也算是帮助自己温故而知新吧。(实则突然找到可以写成博客的素材了)
我毕竟刚刚接触Android,很多认识也都停留在表面,没有太多接触源码的东西,因此我现阶段写下的内容,只管能用就行。
第一部分,我打算写点当前少有教程提到的内容:页面导航(Navigation
),以及可配套食用的顶部工具栏(Toolbar
)、底部导航栏(BottomNavigationView
)和侧边抽屉(DrawerLayout
)。
引入
Navigation
能做什么?
我们经常需要处理页面之间的切换。以THUInfo
为例,主页、动态、计划之间的切换就是基本操作。幸而,Google于2018年推出Android Jetpack
,其中的Navigation
模块可以帮助开发者使用少量代码轻松地实现这一需求。
此外,我们的需求往往不止于三四个根页面之间的切换。例如,在主页页面之下,我们还要有教室资源、消费查询等子页面。并且,我们还希望,在这些子页面中,返回键能够得到正确的处理(即回到上级页面),屏幕左上角也要相应地设置一个虚拟的返回按钮。而这些需求,借助Navigation
,均可轻松得到实现。
那么,Navigation
到底是怎么一回事呢?Navigation
最基本的思路就是,设定一个容器fragment
(注:新版Navigation
推荐使用FragmentContainerView
,不过我还没研究),将其标记为宿主(NavHostFragment
),由NavController
进行管理。实际运用时如要切换页面,只需替换掉该fragment
填充的内容即可。此外,NavController
切换页面时,会自动维护返回栈,从而可进一步实现返回键的处理。
从设计理念上讲,通过Navigation
可以将不同的页面借助fragment
进行关联,而无需创建更多的activity
——这也符合了“一个APP只用一个activity
”的理念。
快速入门
懒人模式
在New Project
中选择Bottom Navigation Activity
作为模板就可以了。
不过,看例程有可能看得云里雾里的,而且如果想把它迁移到已有项目中也要费一番周折,所以下面我来对其中的关键步骤进行分解,谈谈如何具体操作。
自己动手
准备工作
添加Gradle依赖
1 | dependencies { |
创建第一个页面
IDE为我们提供了图形化操作界面,可以帮助我们轻松创建一个页面。
在res
下新建navigation
目录,右击,新建一个Navigation Resource File
(root
为navigation
),然后你应该会看见类似这样的代码:
1 |
|
接着,进入Design
或Split
视图,你应该能看见Click [icon] to add a destination
。点击它所指示的图标,你会看见Create new destination
。点击,IDE会弹出New Android Component
对话框。
在Fragment Name
一栏中,填好相应的名称,例如HomeFragment
。假如命名合理规范,下面的Fragment Layout Name
一栏也会相应地变化,例如变成fragment_home
。接下来,Create layout XML?
勾上,Include fragment factory methods?
不用勾,这样后续可以省很多事。
创建完成后,这一个导航文件的内容也会发生一些变化。你可以对它进行一些调整,例如android:label
表示该fragment
显示给用户的名称,可以根据需要改成其它值。
除此之外,你应该能在res/layout
目录下发现多了一个.xml
布局文件,你可以稍后对它进行修改。
而在代码部分,你应该会发现,多出了一个继承自Fragment
的自定义类(可能是HomeFragment
)。这个类将会负责管理你所创建的这个页面的生命周期。比如,你会重载onStart
方法,从而设定这个页面启动时的行为。
设置导航菜单
在res
下新建menu
目录,在menu
目录下创建一个bottom_nav_menu.xml
(文件名随意,这里加bottom
为了与之后的侧边抽屉区分)。里面的内容大概长这样:
1 |
|
我觉得已经非常直白了,就不再赘述了。
不过有一点,貌似这儿的id
要和你在navigation
文件中对应页面的id
保持一致。
去掉默认标题栏
Google为Android应用提供了默认的标题栏,但实践表明,预设的标题栏太丑了,且难以定制。因此,我们先要将其去掉,以便后面设置更加灵活的Toolbar
。
方法很简单,找到res/values/styles.xml
,将AppTheme
的parent
改成Theme.AppCompat.Light.NoActionBar
即可。
接着,为设置Toolbar
稍作准备,设置一下Toolbar
的主题样式:
1 | <resources> |
其中,colorControlNormal
和android:textColorPrimary
分别是工具栏按钮和标题文字的颜色,至于是white
还是black
取决于背景色。
添加宿主容器
我们一般会选择一个“模板页面”,腾出一块地方安排宿主容器。例如,在THUInfo
中,我就将宿主容器放到了主页面activity_main.xml
中(注意不是“主页”,“主页”“动态”“计划”是平行的三个模块)。
你的布局文件大概会像这样:
1 |
|
稍微提几点。
- 第16行,
fragment
的id
,会在之后指定NavController
时用到。 - 第17行,作用在于将该
fragment
标记为宿主。 - 第19行,Google的例程有点问题,在
ConstraintLayout
下会自动填满整个屏幕。我把它改成了0dp
,这样可以指定它竖直方向填充的上下限。 - 第21行,将其设定为
defaultNavHost
,这样NavController
在处理返回键时,就能知道,是在对这一个NavHost
进行操作。 - 第24行,指定该宿主执行相应
navGraph
中页面的切换。 - 第32行,将
menu
设置为刚刚设好的导航菜单。
至此,准备工作已经就绪,下面就可以看代码了。
代码
1 | class MainActivity : AppCompatActivity() { |
(有机会整一个Kotlin
的语法高亮……IDE里面五彩斑斓的,搬到网页上就全是黑的了……)
- 第7行,将它们作为
top level destinations
拎出来,既能突出其特殊地位,也方便NavController
进行处理。 - 第13、14行,设置
Toolbar
。 - 第16行,获取
NavController
。关于findNavController
有一个注意点,后面再说。 - 第17、18行,将
NavController
与标题栏(这里是toolbar
)进行绑定。 - 第19行,将
NavController
与底部导航栏进行绑定。
现在,假如一切顺利的话,程序应该就能跑起来了。
导航
讲到这里,我们已经可以在若干个top level destination
之间进行切换了。不过,要在不同层级的页面之间进行切换,又该如何操作呢?
这就需要用到NavController
的navigate
方法了。
navigate
方法的签名如下:
1 | public void navigate(int resId) |
它的一个作用是,提供一个目标的resId
,从而让该NavController
实例切换至这个目标。
这里有两个问题:目标的resId
如何提供,以及NavController
实例如何找到。
目标的resId
如何提供?
具体手法上与#创建第一个页面的操作类似,resId
就是你所创建的对应页面的id
,在此不再细讲。
NavController
实例如何找到?
在上面的示例代码中,我使用了findNavController
方法来获取NavController
实例。但仔细观察后,我们发现,这里的findNavController
是Activity
类的一个拓展方法。那么,当我们已经在某一个fragment
中时,又该如何获取到NavController
实例呢?
这时,我们看到,NavHostFragment
下也有一个findNavController
方法。它的签名如下:
1 | public static NavController findNavController(Fragment fragment) |
具体而言,提供一个fragment
,它会沿着这个fragment
的parent chain
,一直找到其对应的NavController
。因此,在实际应用中,代码一般长这样:
1 | wentu_btn.setOnClickListener { |
这里的this
指代的是代码所处的HomeFragment
类。
当然,方法不止这一种。我们同样可以用Fragment
的getActivity
方法,先获取到对应的Activity
,再调用Activity
的findNavController
方法。不过这样就需要在fragment
中用到宿主fragment
的id
,我个人认为这样写不利于降低模块间的耦合度。
处理虚拟返回键
有了NavController
,我们发现,物理的返回键已经能够得到正确的处理,即返回上级页面。然而,屏幕左上角的虚拟返回键却丝毫没有响应。这是为什么呢?
其实,这也不是什么怪事。NavController
的文档中已经指出:
You are responsible for calling
NavController.navigateUp
to handle the Navigation button.
Typically this is done inAppCompatActivity.onSupportNavigateUp
.
因此,我们需要自己重写onSupportNavigateUp
方法。于是,我们回到MainActivity
中,加上这样一行代码:
1 | override fun onSupportNavigateUp() = navController.navigateUp(appBarConfiguration) |
简简单单的一句话,就能顺利解决问题。我想,这背后的魔力,就在于NavController
的各种优秀的方法(例如这里的navigateUp
)。
侧边抽屉
这里,同样也有懒人模式……在New Project
中找到Navigation Drawer Activity
就好了。
不过,鉴于我们需要将抽屉直接引入当前的项目,我们最好还是手动进行整合。
调整布局
我们先来回顾一下THUInfo
的抽屉。
有两个部分。上面是一个渐变色背景的headerLayout
,下面是menu
。
headerLayout
如何画出一个渐变色的背景,并不是我们的重点。可以直接看Google的例程,也可以直接看THUI
的源代码(前景、背景)。
menu
与#设置导航菜单中的操作类似,不过要创建一个新的menu
文件,比如说side_nav_menu.xml
。而且还更简单,因为这里不需要设置icon
了。
修改布局文件
这里,我们就要对activity_main.xml
大动干戈了。
其实改动也不大,主要是在外面套一层DrawerLayout
,再添加一个NavigationView
。
改完之后大概长这样:
1 |
|
基本上还是比较直白的,没有多少可说的。有一点,就是Google的例程上有两处android:fitsSystemWindows="true"
。有没有这句话的区别,这里主要体现在,程序进行绘制时是否算进系统顶部的透明状态栏。
修改源代码
下面,就是修改源代码了。主要有两处要改。
第一处,是将appBarConfiguration = AppBarConfiguration(topLevelDestinationIds)
改成appBarConfiguration = AppBarConfiguration(topLevelDestinationIds, findViewById(R.id.drawer_layout))
,从而将这一DrawerLayout
绑定进去。
第二处,是要处理抽屉里的选项的点击指令。这里,我通过重写onCreateOptionsMenu
方法来实现。(这一块我也没弄清楚为什么要在这里重载。)
具体而言:
1 | override fun onCreateOptionsMenu(menu: Menu): Boolean { |
在// Do something
的部分对点击的选项进行处理。其中,itemId
为刚刚添加的menu
的项目对应的id
。
到此为止,运行程序,我们已经可以通过Navigation
实现页面之间的切换,并能够配合Toolbar
、BottomNavigationView
和DrawerLayout
一起食用。最后,我想关于定制Toolbar
做一点补充。
定制Toolbar
在实际应用中,我们有时会希望Toolbar
的标题文字和按钮图标能够动态地改变。这又该如何实现呢?
动态设置标题文字
以THUInfo
为例,在查询教室资源时,当用户进入了具体的教学楼页面(例如“六教”),标题栏也相应地变成了“六教”。代码其实很简单:
1 | (activity as? AppCompatActivity)?.supportActionBar?.title = "{Your Title}" |
此处,先获得fragment
所在的activity
,并将其转化为AppCompatActivity
,因为getSupportActionBar
是AppCompatActivity
的方法。得到了supportActionBar
后,获取到title
,我们就可以动态地进行设置了。
动态设置按钮图标
在THUInfo
中,当收到新邮件时,左上角的按钮图标会变成带小红点的三道杠。这又是如何实现的呢?
首先,我们当然是要先准备好新图标。这里比较重要的是,图标的大小要调整好,比如设置成24dp*24dp,因为Toolbar
貌似不会对图标进行自动缩放。
代码跟上面差不多,得到supportActionBar
之后,设置navigationIcon
属性,即可实现。
当然,这有点暴力修改的意味在里面。因此,要想显得不那么暴力,需要考虑一些问题。最主要的,就是设置navigationIcon
的过程是一瞬间完成的,中间的过渡动画就没有了。
还有两个小问题。
每30秒重新获取一次未读邮件情况,相应地也要刷新一下左上角的图标。然而,**我忘了还要判断当前页面是否为
top level destiniation
**,导致的结果是,明明左上角应该是一个返回键,突然就变成带小红点的三道杠了。
看一下修改之后的代码片段:1
2
3if (navController.currentDestination?.id in topLevelDestinationIds) {
// Update badge
}然而,仅仅每30秒刷新一次显然不够——当用户重新来到
top level destination
时,也应刷新一次。这时,我们就要监听NavController
的目标变化了:1
2
3navController.addOnDestinationChangedListener { _, destination, _ ->
if (destination.id in topLevelDestinationIds) refreshBadge()
}
(所以,为什么不自己做一个Toolbar
呢)
THUInfo
中处理小红点的详细代码在MainActivity.kt
中。
好了,感觉自己也写了不少了,也算是把整个Navigation
的来龙去脉都简单地梳理了一遍。
既然……我也不太会写结束语,那就,先这样结束吧~
本文链接:https://www.unidy.cn/articles/thui-a-1/