《Android编程权威指南》之应用栏篇

790 阅读7分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

《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 准则。

具体各个版本更新请参考:developer.android.com/jetpack/and… 或许组件更新了更好用的功能呢,那就二话不说用起来呐。

新建项目,AS都会给个默认主题,打开 AndroidManifest.xml,找到为 application 设置的 theme。

    <application
        ...
        android:theme="@style/Theme.AndroidGuideApp">
        ...
    </application>

按住 ctrl (windows) 或者 command (mac) 跳转过去,会跳转到 res -> value/value-night(夜间模式) -> themes.xml,这里为整个应用的主题设置了一些默认的样式。

<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Theme.AndroidGuideApp" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
        <!-- Primary brand color. -->
        <item name="colorPrimary">@color/purple_500</item>
        <item name="colorPrimaryVariant">@color/purple_700</item>
        <item name="colorOnPrimary">@color/white</item>
        <!-- Secondary brand color. -->
        <item name="colorSecondary">@color/teal_200</item>
        <item name="colorSecondaryVariant">@color/teal_700</item>
        <item name="colorOnSecondary">@color/black</item>
        <!-- Status bar color. -->
        <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
        <!-- Customize your theme here. -->
    </style>
</resources>

二、应用栏菜单

应用栏菜单由菜单项(又称操作项)组成,在应用栏右上方区域,现在加个新增crime的菜单项。

  • 1、在XML文件中定义菜单

在res里面新建menu文件夹,再新建fragment_crime_list.xml,资源类型为menu。

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/new_crime"
        android:icon="@android:drawable/ic_menu_add"
        android:title="@string/new_crime"
        app:showAsAction="ifRoom|withText" />
</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)
    }
...

}

demo

长按 + 号,可弹出 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

操作过程1

2

3

然后就生成了适配各个屏幕尺寸的图标,不错!👏🏻

然后去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)

会报错: bug

说明啊,版本又有更新啦,这里得到的 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 参考: developer.android.com/reference/k…

六、挑战练习: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<Crime>) :
        RecyclerView.Adapter<RecyclerView.ViewHolder>() {

        // 是否显示空布局,默认不显示
        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 对比数据变化的方式,会报如下错误。

bug

因此这里将代码稍做了还原,成功加上了空布局显示,有关 DiffUtil 还需要细致研究一下才行。

七、其他

CriminalIntent 项目 Demo 地址: github.com/visiongem/A…


🌈关注我吖~❤️

公众号:妮K妮K妮