《Android编程权威指南》第14章了,开始接触应用栏了,把应用做好看,非常实用。市面上成熟的App也应该设计成统一的风格、塑造好品牌形象滴!当然它也可以被叫做操作栏或工具栏,都行的啦,不同的叫法而已。
一、AppCompat默认应用栏
Android Studio在创建新项目时,会为所有继承AppCompatActivity的activity添加一个默认应用栏。
在app/build.gradle文件中,会看到依赖
implementation 'androidx.appcompat:appcompat:1.3.0'。
“AppCompat”是 “application compatibility”(应用兼容性)的缩写,包含很多核心类和资源,从而让各个Android系统版本的应用UI保持风格统一,而且大多数都符合 Material Design 准则。
具体各个版本更新请参考:https://developer.android.com/jetpack/androidx/releases/appcompat
或许组件更新了更好用的功能呢,那就二话不说用起来呐。
新建项目,AS都会给个默认主题,打开 AndroidManifest.xml,找到为 application 设置的 theme。
...
按住 ctrl (windows) 或者 command (mac) 跳转过去,会跳转到 res -> value/value-night(夜间模式) -> themes.xml,这里为整个应用的主题设置了一些默认的样式。
二、应用栏菜单
应用栏菜单由菜单项(又称操作项)组成,在应用栏右上方区域,现在加个新增crime的菜单项。
- 1、在XML文件中定义菜单
在res里面新建menu文件夹,再新建fragment_crime_list.xml,资源类型为menu。
showAsAction 用于指定菜单项是显示在应用栏上,还是隐藏于溢出菜单(overfow menu)中。
上述组合表示只要空间足够,菜单项图标及其文字描述都会显示在应用栏上。如果空间仅够显示菜单项图标,文字描述就不会显示;如果空间大小不够显示任何项,菜单项就会隐藏到溢出菜单中。
溢出菜单是以三个点表示。
showAsAction 还有另外两个可选值:always和never。不推荐使用always,应尽量使用ifRoom属性值,让操作系统决定如何显示菜单项。对于那些很少用到的菜单项,never属性值是个不错的选择。
出于兼容性考虑,AppCompat库需要使用app命名空间。
- 2、创建菜单
Activity 类提供了管理菜单的回调函数 onCreateOptionsMenu(Menu) ,Fragment 也有一套自己的选项菜单回调函数。
class CrimeListFragment : Fragment() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
...
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.fragment_crime_list, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return super.onOptionsItemSelected(item)
}
...
}
长按 + 号,可弹出 New Crime 标题。横屏的话,应用栏就会把menu的图标和标题都显示出来。
- 3、响应菜单项选择
在 CrimeListViewModel.kt 中新增函数 addCrime:
fun addCrime(crime: Crime) {
crimeRepository.insertCrimes(crime)
}
在 CrimeListFragment.kt 中,实现 onOptionsItemSelected(MenuItem) 函数,以响应菜单项的选择事件。
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId){
R.id.new_crime->{
val crime = Crime()
crimeListViewModel.addCrime(crime)
callBacks?.onCrimeSelected(crime.id)
true
}
else -> return super.onOptionsItemSelected(item)
}
}
onOptionsItemSelected(MenuItem) 函数返回的是布尔值。一旦完成菜单项事件处理,该函数应该返回true值以表明任务已完成。如果返回false值,就调用托管activity的onOptionsItemSelected(MenuItem)函数继续。(如果托管activity托管了其他fragment,那么它们也会调用onOptionsItemSelected函数。)另外,默认情况下,如果菜单项ID不存在,超类版本函数会被调用。
三、使用Android Asset Studio
应用使用的图标:
- 系统图标(system icon):Android 操作系统内置的图标
- 项目资源图标
不同设备或操作系统版本间,系统图标的显示风格差异很大,不推荐使用系统图标「系统图标可在Android SDK的安装目录下找到」,应自己找个合适的图标,复制到项目的 drawable 资源目录中。
还有一种方式统一图标风格,使用 Android Studio 内置的 Android Asset Studio 工具,为应用栏创建或定制图片。
drawable -> New -> Image Asset
然后就生成了适配各个屏幕尺寸的图标,不错!
然后去menu那里把图标路径换了!
四、深入学习:应用栏、操作栏与工具栏
应用栏、工具栏和操作栏的UI设计元素本身就叫应用栏。
Android 5.0(Lollipop,API 21级)之前,应用栏使用 ActionBar 类来实现,“操作栏”和“应用栏”是完全一样的概念。
Android 5.0 开始,应用栏优先使用新引入的 Toolbar 类实现。
ActionBar 和 Toolbar 很相似,工具栏建立在操作栏基础之上。除了UI视觉上调整,在使用上,工具栏比操作栏更灵活。
操作栏的使用限制很多,比如,整个应用只能配置一个操作栏且位置及尺寸必须固定(在屏幕顶部)。工具栏无此限制,还能支持内嵌视图和调整高度,这极大地丰富了应用的交互模式。
五、深入学习:AppCompat 版应用栏
使用 AppCompat 版应用栏,需引用 AppCompatActivity 的 supportFragmentManager 属性。
这里照着书上的代码:
val appCompatActivity = activity as AppCompatActivity
val appBar = appCompatActivity.supportActionBar as Toolbar
appBar.setTitle(R.string.some_cool_title)
会报错:
说明啊,版本又有更新啦,这里得到的 actionBar 已经是 androidx 中的了,用法会有点不一样,有时间再来研究下。这里如果只是想修改个标题的话,可以直接添加:
val appCompatActivity = activity as AppCompatActivity
val appBar = appCompatActivity.supportActionBar
appBar?.title = appCompatActivity.resources.getString(R.string.some_cool_title)
注意,如果需要修改当前activity用户界面上的应用栏菜单内容,可以调用invalidateOptionsMenu()函数,让它触发onCreateOptionsMenu(Menu, MenuInfater)回调函数来达到目的。在onCreateOptionsMenu回调函数里,编码修改菜单内容后,回调一结束,所有修改立即生效。
有关 Toolbar API 参考:
https://developer.android.com/reference/kotlin/android/widget/Toolbar
六、挑战练习:RecyclerView空视图
现在是要删除初始化给的默认列表数据库,不再在应用启动的时候,就给数据库插入一些数据了。那么,列表就是空的,现要求无数据时给列表添加个空视图,开始啦!
首先模仿之前的代码,给recyclerview新增一种 itemType 为空布局类型
enum class ITEM_TYPE {
ITEM_TYPE_NOMAL,
ITEM_TYPE_POLICE,
ITEM_TYPE_EMPTY
}
然后再给 CrimeAdapter 新增个是否显示空布局的字段,用于标记当前是否需要显示空布局,默认为不显示。给外界提供一个方法,用于修改该字段为提供显示空布局的功能,在Adapter的getItemCount中,当数据集size为0且提供显示空布局功能的时候,就返回1,然后再在getItemViewType中判断position为0且是空布局,就返回空布局类型,由于空布局也没有要绑定的数据,所以在onBindViewHolder中也要进行一下判断。代码大致如下:
private inner class CrimeAdapter(var crimes: List) :
RecyclerView.Adapter() {
// 是否显示空布局,默认不显示
private var showEmptyView = false
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ITEM_TYPE.ITEM_TYPE_POLICE.ordinal ->
CrimePoliceHolder(
ItemCrimePoliceBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
ITEM_TYPE.ITEM_TYPE_NOMAL.ordinal ->
CrimeHolder(
ItemCrimeBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
else ->
EmptyHolder(
ItemEmptyViewBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
}
override fun getItemViewType(position: Int): Int {
return if (position == 0 && isEmptyPosition()) {
ITEM_TYPE.ITEM_TYPE_EMPTY.ordinal
} else {
if (crimes[position].requiresPolice) {
ITEM_TYPE.ITEM_TYPE_POLICE.ordinal
} else {
ITEM_TYPE.ITEM_TYPE_NOMAL.ordinal
}
}
}
override fun getItemCount(): Int {
val count = crimes.size
return if (count > 0) {
showEmptyView = false
count
} else {
if (showEmptyView) {
1
} else {
0
}
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (!(position == 0 && isEmptyPosition())) {
val crime = crimes[position]
if (holder is CrimeHolder) {
holder.bind(crime)
} else if (holder is CrimePoliceHolder) {
holder.bind(crime)
}
}
}
/**
* Show empty view
* 显示空布局
*/
fun showEmptyView() {
showEmptyView = true
notifyDataSetChanged()
}
/**
* 判断是否是空布局
*/
fun isEmptyPosition(): Boolean {
val count = if (crimes.isEmpty()) 0 else crimes.size
return showEmptyView && count == 0
}
}
private inner class EmptyHolder(val itemEmptyViewBinding: ItemEmptyViewBinding) :
RecyclerView.ViewHolder(itemEmptyViewBinding.root) {
init {
itemEmptyViewBinding.imgAddCrime.setOnClickListener {
val crime = Crime()
crimeListViewModel.addCrime(crime)
callBacks?.onCrimeSelected(crime.id)
}
}
}
上述代码将 DiffUtil 去掉了,因为在显示空布局的时候,也是需要返回有一个item的,只是此 item 是占满屏幕显示的空布局,然后与其他类型的不一样,也不会有 crime 按照原先使用 DiffUtil.ItemCallback 对比数据变化的方式,会报如下错误。
因此这里将代码稍做了还原,成功加上了空布局显示。
七、其他
CriminalIntent 项目 Demo 地址:
https://github.com/visiongem/AndroidGuideApp/tree/master/CriminalIntent