作者:DylanCai
前言
大多数页面都有标题栏,通常会在基类里封装通用标题栏的初始化代码,然后只需在布局代码里 include 一个标题栏布局,在 Activity 里就能很方便把标题栏设置了。
这可能是目前比较普遍的封装方式了。这也有一些弊端,每次都要在布局里写 include 代码比较繁琐。如果是特殊一点的标题栏,就只能自己另外实现了。
今天就介绍一种船新的添加标题栏方式,少啰嗦,看最终效果:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 添加带返回键和右边按钮的标题栏
setToolbar("标题", NavIconType.BACK, "完成"){
Toast.makeText(this, "点击了完成按钮", Toast.LENGTH_SHORT).show()
}
}
}
「就这么简单,不用在布局写标题栏代码,不用继承基类,直接调用一行代码就实现了。当然不只是这样,甚至可以添加一个带联动效果的标题栏,这是 include 的封装方式难以做到的。」
当然这个方法是要自己写的,因为标题栏各式各样,比如右边有多个图标、有两层高度、有搜索框、带联动效果等等。需求千变万化,只有我们自己知道要什么,不过只需编写一点代码,后面复用就像上面例子那样随心所欲非常方便。
那么具体要怎么实现呢?请听我娓娓道来。
解决方案
准备工作
首先请出我们的主角—— LoadingHelper。对,你没看错,这是个人封装用于请求时展示加载中、加载失败等布局的 loading 库,可以用于管理标题栏。不算上注释仅有一个 200 多行的 Kotlin 代码,虽然代码不多但是非常强大,往下看就知道了
「先解释一下为什么一个用于请求时显示加载中、加载失败等布局的库要管理标题栏呢?因为标题栏在绝大多数情况会影响到 loading 的区域。」
开始使用,先添加依赖:
dependencies {
implementation 'com.dylanc:loadinghelper:2.1.0'
}
简单介绍下 loading 功能的基础用法。
loadingHelper = LoadingHelper(this)
loadingHelper.register(ViewType.LOADING, LoadingAdapter())
loadingHelper.showView(ViewType.LOADING)
用法就是这么的简单,注册了之后进行展示。有五个默认视图类型,也可以传任意类型的数据进行注册。注册的适配器是继承了 LoadingHelper.Adapter ,写法和 RecyclerView.Adapter 很类似,都是用来创建和缓存 View。
为方便使用,提供了注册全局适配器和展示默认类型视图的方法。
LoadingHelper.setDefaultAdapterPool {
register(ViewType.LOADING, LoadingAdapter())
register(ViewType.ERROR, ErrorAdapter())
register(ViewType.EMPTY, EmptyAdapter())
}
loadingHelper.showLoadingView() // 对应视图类型 ViewType.LOADING
loadingHelper.showContentView() // 对应视图类型 ViewType.CONTENT,展示原本的内容
loadingHelper.showErrorView() // 对应视图类型 ViewType.ERROR
loadingHelper.showEmptyView() // 对应视图类型 ViewType.EMPTY
相对于其它的 loading 库,已经尽量让学习成本足够低。除了我们很熟悉的适配器,就是 register 和 show 方法。
添加标题栏
马上进入正题,如何添加标题栏,这是本库的另外一个非常实用的功能——「给内容包裹一层装饰的容器」。添加标题栏只是一种最为常见的用法,还有其它使用场景如底部图文输入框、头部搜索框、头部带有编辑全选功能的布局等有多个页面需要复用,或者想在不改变原有布局代码的情况进行添加,都可以使用本库进行添加管理。
下面我们来添加一个具有联动效果的标题栏,这个实现了想要添加别的都不是什么问题。
首先准备一个用于装饰内容的布局 DecorView。这个并不是 Android 源码里的 DecorView 类,不过因为参考了 DecorView 添加 ActionBar 的实现原理所以致敬一下,而且这是用于装饰的 View,叫 DecorView 也合适。
这里所使用到的 DecorView 就只是一个普通的 View ,需要有以下的结构:
结构很简单,其中的 ContentParent 是用于添加内容布局,切换 loading、error、empty 等页面。
好,我们先实现一个带联动效果的布局:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/app_bar_height"
android:fitsSystemWindows="true"
android:theme="@style/AppTheme.AppBarOverlay">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:toolbarId="@+id/toolbar">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:id="@+id/content_parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
然后我们需要继承一个用于创建 DecorView 的适配器 LoadingHelper.DecorAdapter ,结合前面的图再来看需要实现的抽象方法应该很好理解。DecorView 对应这个布局,ContentParent 对应布局里的 FrameLayout 容器。
class ScrollDecorAdapter : DecorAdapter() {
override fun onCreateDecorView(inflater: LayoutInflater): View {
return inflater.inflate(R.layout.layout_scrolling_toolbar, null)
}
override fun getContentParent(decorView: View): ViewGroup {
return decorView.findViewById(R.id.content_parent)
}
}
最后设置一下装饰适配器即可。
loadingHelper.setDecorAdapter(ScrollDecorAdapter())
不过多数情况下我们只是想在简单地在顶部添加一个普通的标题栏,而这还需要用个父容器把 Toolbar 包裹,感觉写起来有点麻烦。当然这种情况也是有考虑的,这就要用到另外一个方法,「设置装饰的头部,简单来说就是将一个或多个 View 添加到顶部」。
需要实现前面 loading 功能用到的 LoadingHelper.Adapter,这是用于创建和缓存 View 的适配器。而 LoadingHelper.DecorAdapter 是用于创建装饰容器 DecorView 的适配器。这两者不要搞混了。
这时候我们就能把之前用于 include 的标题栏布局利用起来。下面的适配器代码是不是很熟悉?
class ToolbarAdapter(
private val title: String?,
private val type: NavIconType = NavIconType.NONE
) : LoadingHelper.Adapter<LoadingHelper.ViewHolder>() {
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): LoadingHelper.ViewHolder {
return LoadingHelper.ViewHolder(inflater.inflate(R.layout.layout_toolbar, parent, false))
}
override fun onBindViewHolder(holder: LoadingHelper.ViewHolder) {
holder.rootView.apply {
if (!title.isNullOrBlank()) {
toolbar.title = title
}
if (type == NavIconType.BACK) {
toolbar.setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
toolbar.setNavigationOnClickListener {
(holder.rootView.context as Activity).finish()
}
} else {
toolbar.navigationIcon = null
}
}
}
}
enum class NavIconType {
BACK, NONE
}
最后调用设置装饰的头部的方法,可以添加多个头部,当然设置之前也需要注册适配器。
loadingHelper.register(ViewType.TITLE, ToolbarAdapter("标题", NavIconType.BACK))
loadingHelper.register(VIEW_TYPE_SEARCH, SearchHeaderAdapter())
loadingHelper.setDecorHeader(ViewType.TITLE, VIEW_TYPE_SEARCH)
多次调用设置装饰的方法也没问题,后面设置的会把前面装饰的给替换掉,如果有这样的使用场景可以试试。
还可以添加子装饰容器或子装饰头部,比如我想在一个带联动效果的标题栏下方添加个搜索框:
loadingHelper.setDecorAdapter(ScrollDecorAdapter())
loadingHelper.addChildDecorHeader(VIEW_TYPE_SEARCH)
「不管是添加装饰容器还是装饰头部,最终都是会添加到我们的布局里,也就是说虽然布局里看起来没有相关控件的代码,但是我们设置之后仍能通过 findViewById 找到该控件。」
val toolbar: Toolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar)
到此为止,如何使用本库添加装饰的容器或者装饰的头部就已经说完了。
那么怎么做到开头那样用一行代码添加标题栏呢?
推荐用法
因为使用本库只需要一个 Activity 或 View 对象和适配器就能添加标题栏,所以可以利用 Kotlin 拓展函数简化使用的代码。下面对前面实现的适配器进行封装。
fun Activity.setToolbar(title: String, type: NavIconType = NavIconType.NONE) =
LoadingHelper(this).apply {
register(ViewType.TITLE, ToolbarAdapter(title, type))
setDecorHeader(ViewType.TITLE)
}
可以理解为让 Activity 增加了个 setToolbar 的方法,这样我们就可以在不继承基类的情况下把标题栏添加了:
class MainActivity : AppCompatActivity() {
private lateinit var loadingHelper:LoadingHelper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
loadingHelper = setToolbar("标题", NavIconType.BACK)
}
}
前面特地花了点时间来讲 loading 的用法,因为在这里我们可以根据需要使用返回的对象来显示 loading、content、error 等页面。这样使用不仅耦合度低,还保留了 loading 功能。
可能有人会问,这是 Kotlin 的用法,那用 Java 的话怎么办呢?使用 Java 需要写一个工具类。
public class ToolbarUtils {
public static LoadingHelper setToolbar(Activity activity, String title, NavIconType type) {
LoadingHelper loadingHelper = new LoadingHelper(activity);
loadingHelper.register(ViewType.TITLE, new ToolbarAdapter(title, type));
loadingHelper.setDecorHeader(ViewType.TITLE);
return loadingHelper;
}
}
ToolbarUtils.setToolbar(this, "标题", NavIconType.BACK);
另外还可以选择封装在基类里,同样能很好地解耦,并且在 loading 的时候就可以不用接触到 LoadingHelper 对象,使用起来更加简洁方便。不过用工具类或者拓展函数的方式耦合度更低,可以在不改变原来的代码的情况下进行添加。大家可以根据自己的需要进行选择。
Demo
点击或者扫描二维码下载,Demo 里除了简单的内容,其它基本都是用本库动态添加,示例的代码在 GitHub 里。「如果对前面的 Kotlin 代码不熟悉,可以看下 GitHub 文档和 Demo 代码,都是用 Java 写的。」
总结
本文主要讲了传统使用 include 的方式来封装标题栏的弊端,介绍了如何使用 LoadingHelper 对标题栏进行深度解耦,推荐了不用在布局写标题栏代码、不用继承基类就能添加标题栏的创新的使用方式,还能同时兼顾 loading 功能。耦合度非常低,可以很方便应用到自己的项目中,推荐大家来尝试一下。另外将本库封装在基类也是不错的选择。