前言
大多數(shù)頁(yè)面都有標(biāo)題欄,通常會(huì)在基類里封裝通用標(biāo)題欄的初始化代碼,然后只需在布局代碼里 include 一個(gè)標(biāo)題欄布局,在 Activity 里就能很方便把標(biāo)題欄設(shè)置了。
這可能是目前比較普遍的封裝方式了。這也有一些弊端,每次都要在布局里寫(xiě) include 代碼比較繁瑣。如果是特殊一點(diǎn)的標(biāo)題欄,就只能自己另外實(shí)現(xiàn)了。
今天就介紹一種船新的添加標(biāo)題欄方式,少啰嗦,看最終效果:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 添加帶返回鍵和右邊按鈕的標(biāo)題欄
setToolbar("標(biāo)題", NavIconType.BACK, "完成"){
Toast.makeText(this, "點(diǎn)擊了完成按鈕", Toast.LENGTH_SHORT).show()
}
}
}
就這么簡(jiǎn)單,不用在布局 include 標(biāo)題欄,不用繼承基類,直接調(diào)用一行代碼就實(shí)現(xiàn)了。當(dāng)然不只是這樣,甚至可以添加一個(gè)帶聯(lián)動(dòng)效果的標(biāo)題欄,這是 include 的封裝方式難以做到的。
當(dāng)然這個(gè)方法是要自己寫(xiě)的,因?yàn)闃?biāo)題欄各式各樣,比如右邊有多個(gè)圖標(biāo)、有兩層高度、有搜索框、帶聯(lián)動(dòng)效果等等。需求千變?nèi)f化,只有我們自己知道要什么,不過(guò)只需編寫(xiě)一點(diǎn)代碼,后面復(fù)用就像上面例子那樣隨心所欲非常方便。
那么具體要怎么實(shí)現(xiàn)呢?請(qǐng)聽(tīng)我娓娓道來(lái)。
解決方案
準(zhǔn)備工作
首先請(qǐng)出我們的主角—— LoadingHelper。對(duì),你沒(méi)看錯(cuò),這是個(gè)人封裝的 loading 庫(kù),一個(gè)可以管理標(biāo)題欄的 loading 庫(kù)。不算上注釋僅有一個(gè) 200 多行的 Kotlin 代碼,雖然代碼不多但是非常強(qiáng)大,往下看就知道了。
先解釋一下為什么一個(gè) loading 庫(kù)要管理標(biāo)題欄呢?因?yàn)闃?biāo)題欄在絕大多數(shù)情況會(huì)影響到 loading 的區(qū)域。 多數(shù)情況下我們是需要對(duì)頁(yè)面進(jìn)行 loading,而標(biāo)題欄的存在使得我們要在標(biāo)題欄下方區(qū)域顯示 loading 或 error 等布局。還有一些別的考慮,建議先看下這篇文章《優(yōu)雅地管理 Loading 界面和標(biāo)題欄》,里面較詳細(xì)地介紹了整體的思想和用法。
簡(jiǎn)單介紹下 loading 功能的基礎(chǔ)用法。
loadingHelper = LoadingHelper(this)
loadingHelper.register(ViewType.LOADING, LoadingAdapter())
loadingHelper.showView(ViewType.LOADING)
用法就是這么的簡(jiǎn)單,注冊(cè)了之后進(jìn)行展示。有五個(gè)默認(rèn)視圖類型,也可以傳任意類型的數(shù)據(jù)進(jìn)行注冊(cè)。注冊(cè)的適配器是繼承了 LoadingHelper.Adapter ,寫(xiě)法和 RecyclerView.Adapter 很類似,都是用來(lái)創(chuàng)建和緩存 View。
為方便使用,提供了注冊(cè)全局適配器和展示默認(rèn)類型視圖的方法。
LoadingHelper.setDefaultAdapterPool {
register(ViewType.LOADING, LoadingAdapter())
register(ViewType.ERROR, ErrorAdapter())
register(ViewType.EMPTY, EmptyAdapter())
}
loadingHelper.showLoadingView() // 對(duì)應(yīng)視圖類型 ViewType.LOADING
loadingHelper.showContentView() // 對(duì)應(yīng)視圖類型 ViewType.CONTENT,展示原本的內(nèi)容
loadingHelper.showErrorView() // 對(duì)應(yīng)視圖類型 ViewType.ERROR
loadingHelper.showEmptyView() // 對(duì)應(yīng)視圖類型 ViewType.EMPTY
相對(duì)于其它的 loading 庫(kù),已經(jīng)盡量讓學(xué)習(xí)成本足夠低。除了我們很熟悉的適配器,就是 register 和 show 方法。
添加標(biāo)題欄
馬上進(jìn)入正題,如何添加標(biāo)題欄,這是本庫(kù)的另外一個(gè)非常實(shí)用的功能——給內(nèi)容包裹一層裝飾的容器。添加標(biāo)題欄只是一種最為常見(jiàn)的用法,還有其它使用場(chǎng)景如底部圖文輸入框、頭部搜索框、頭部帶有編輯全選功能的布局等有多個(gè)頁(yè)面需要復(fù)用,或者想在不改變?cè)胁季执a的情況進(jìn)行添加,都可以使用本庫(kù)進(jìn)行添加管理。
下面我們來(lái)添加一個(gè)具有聯(lián)動(dòng)效果的標(biāo)題欄,這個(gè)實(shí)現(xiàn)了想要添加別的都不是什么問(wèn)題。
首先準(zhǔn)備一個(gè)用于裝飾內(nèi)容的布局 DecorView。這個(gè)并不是 Android 源碼里的 DecorView 類,不過(guò)因?yàn)閰⒖剂?DecorView 添加 ActionBar 的實(shí)現(xiàn)原理所以致敬一下,而且這是用于裝飾的 View,叫 DecorView 也合適。
這里所說(shuō)的 DecorView 就只是一個(gè)普通的 View ,但需要有一下的結(jié)構(gòu):

結(jié)構(gòu)很簡(jiǎn)單,其中的 ContentParent 是用于添加內(nèi)容布局,切換 loading、error、empty 等頁(yè)面。
好,我們先實(shí)現(xiàn)一個(gè)帶聯(lián)動(dòng)效果的布局:
<?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>
然后我們需要繼承一個(gè)用于創(chuàng)建 DecorView 的適配器 LoadingHelper.DecorAdapter ,結(jié)合前面的圖再來(lái)看需要實(shí)現(xiàn)的抽象方法應(yīng)該很好理解。
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.loading_container)
}
}
最后設(shè)置一下裝飾適配器即可。
loadingHelper.setDecorAdapter(ScrollDecorAdapter())
不過(guò)多數(shù)情況下我們只是想在簡(jiǎn)單地在頂部添加一個(gè)普通的標(biāo)題欄,而這還需要用個(gè)父容器把 Toolbar 包裹,感覺(jué)寫(xiě)起來(lái)有點(diǎn)麻煩。
當(dāng)然這種情況也是有考慮的,這就要用到另外一個(gè)方法,設(shè)置裝飾的頭部,簡(jiǎn)單來(lái)說(shuō)就是將一個(gè)或多個(gè) View 添加到頂部。
需要實(shí)現(xiàn)前面 loading 功能用到的 LoadingHelper.Adapter,這是用于創(chuàng)建和緩存 View 的適配器。而 LoadingHelper.DecorAdapter 是用于創(chuàng)建裝飾容器 DecorView 的適配器。這兩者不要搞混了。
這時(shí)候我們就能把之前用于 include 的標(biāo)題欄布局利用起來(lái)。下面的適配器代碼是不是很熟悉?
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
}
最后調(diào)用設(shè)置裝飾的頭部的方法,可以添加多個(gè)頭部,當(dāng)然設(shè)置之前也需要注冊(cè)適配器。
loadingHelper.register(ViewType.TITLE, ToolbarAdapter("標(biāo)題", NavIconType.BACK))
loadingHelper.register(VIEW_TYPE_SEARCH, SearchHeaderAdapter())
loadingHelper.setDecorHeader(ViewType.TITLE, VIEW_TYPE_SEARCH)
多次調(diào)用設(shè)置裝飾的方法也沒(méi)問(wèn)題,后面設(shè)置的會(huì)把前面裝飾的給替換掉,如果有這樣的使用場(chǎng)景可以試試。
還可以添加子裝飾容器或子裝飾頭部,比如我想在一個(gè)帶聯(lián)動(dòng)效果的標(biāo)題欄下方添加個(gè)搜索框:
loadingHelper.setDecorAdapter(ScrollDecorAdapter())
loadingHelper.addChildDecorHeader(VIEW_TYPE_SEARCH)
不管是添加裝飾容器還是裝飾頭部,最終都是會(huì)添加到我們的布局里,也就是說(shuō)雖然布局里看起來(lái)沒(méi)有相關(guān)控件的代碼,但是我們?cè)O(shè)置之后仍能通過(guò) findViewById 找到該控件。
val toolbar: Toolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar)
到此為止,如何使用本庫(kù)添加裝飾的容器或者裝飾的頭部就已經(jīng)說(shuō)完了。
那么怎么做到開(kāi)頭那樣用一行代碼添加標(biāo)題欄呢?
推薦用法
因?yàn)槭褂帽編?kù)只需要一個(gè) Activity 或 View 對(duì)象和適配器就能添加標(biāo)題欄,所以可以利用 Kotlin 拓展函數(shù)簡(jiǎn)化使用的代碼。下面對(duì)前面實(shí)現(xiàn)的適配器進(jìn)行封裝。
fun Activity.setToolbar(title: String, type: NavIconType = NavIconType.NONE) =
LoadingHelper(this).apply {
register(ViewType.TITLE, ToolbarAdapter(title, type))
setDecorHeader(ViewType.TITLE)
}
可以理解為讓 Activity 增加了個(gè) setToolbar 的方法,這樣我們就可以在不繼承基類的情況下把標(biāo)題欄添加了:
class MainActivity : AppCompatActivity() {
private lateinit var loadingHelper:LoadingHelper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
loadingHelper = setToolbar("標(biāo)題", NavIconType.BACK)
}
}
前面特地花了點(diǎn)時(shí)間來(lái)講 loading 的用法,因?yàn)樵谶@里我們可以根據(jù)需要使用返回的對(duì)象來(lái)顯示 loading、content、error 等頁(yè)面。這樣使用不僅耦合度低,還保留了 loading 功能。
可能有人會(huì)問(wèn),這是 Kotlin 的用法,那用 Java 的話怎么辦呢?使用 Java 需要寫(xiě)一個(gè)工具類。
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, "標(biāo)題", NavIconType.BACK);
另外還可以選擇封裝在基類里,同樣能很好地解耦,并且在 loading 的時(shí)候就可以不用接觸到 LoadingHelper 對(duì)象,使用起來(lái)更加簡(jiǎn)潔方便。不過(guò)用工具類或者拓展函數(shù)的方式耦合度更低,可以在不改變?cè)瓉?lái)的代碼的情況下進(jìn)行添加。大家可以根據(jù)自己的需要進(jìn)行選擇。
Demo
點(diǎn)擊或者掃描二維碼下載,Demo 里除了簡(jiǎn)單的內(nèi)容,其它基本都是用本庫(kù)動(dòng)態(tài)添加。
總結(jié)
本文主要講了傳統(tǒng)使用 include 的方式來(lái)封裝標(biāo)題欄的弊端,介紹了如何使用 LoadingHelper 對(duì)標(biāo)題欄進(jìn)行深度解耦,推薦了不用在布局寫(xiě)標(biāo)題欄代碼、不用繼承基類就能添加標(biāo)題欄的創(chuàng)新的使用方式,還能同時(shí)兼顧 loading 功能。耦合度非常低,可以很方便應(yīng)用到自己的項(xiàng)目中,推薦大家來(lái)嘗試一下。另外將本庫(kù)封裝在基類也是不錯(cuò)的選擇。
如果你覺(jué)得本庫(kù)還不錯(cuò)的話希望能給個(gè) star 支持一下哦~