史上耦合度最低的添加标题栏方式

6,719 阅读8分钟

前言

大多数页面都有标题栏,通常会在基类里封装通用标题栏的初始化代码,然后只需在布局代码里 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 的区域。 多数情况下我们是需要对页面进行 loading,而标题栏的存在使得我们要在标题栏下方区域显示 loading 或 error 等布局。还有一些别的考虑,有兴趣的可以看下这篇文章《优雅地管理 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 ,需要有以下的结构:

DecorView

结构很简单,其中的 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 功能。耦合度非常低,可以很方便应用到自己的项目中,推荐大家来尝试一下。另外将本库封装在基类也是不错的选择。

如果你觉得本库还不错的话希望能给个 star 支持一下哦~