史上耦合度最低的添加標(biāo)題欄方式

前言

大多數(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):

DecorView.png

結(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 支持一下哦~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容