漸變式透明標題欄與 CollapsingToolbarLayout 摺疊式標題欄
本篇文章的來源是一開始我需要實現類似 IOS 的彈簧動畫,當時選擇了 ScrollView +頭部 Layout 來實現的,實現效果如圖:

漸變式透明標題欄
可以看到,頂部標題區可以隨著手指滑動而 逐漸 透明或者 逐漸 覆蓋,這個效果是我實現的第一個版本的效果,原理也非常簡單,首頁佈局如下:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="vertical" tools:context=".ui.home.fragment.HomeFragmentD"> <androidx.core.widget.NestedScrollView android:id="@+id/nsv_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:overScrollMode="never"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> ……………… </LinearLayout> </androidx.core.widget.NestedScrollView> <RelativeLayout android:id="@+id/top_layout" android:layout_width="match_parent" android:layout_height="?android:attr/actionBarSize" android:background="@android:color/transparent"> <TextView android:id="@+id/tv_title" style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:lines="1" android:maxLength="12" tools:text="我的" android:ellipsize="end" android:textColor="@color/black80" android:textSize="16sp"/> </RelativeLayout> </RelativeLayout> 複製程式碼
接下來我們分為兩步:
1、我們需要做的是實現狀態列透明
通過狀態列透明達到沉浸式效果。這個只要也非常簡單,直接上程式碼:
private fun initStatusBar() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) window.statusBarColor = Color.TRANSPARENT window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR } } 複製程式碼
2、對佈局新增滑動監聽 重頭戲
狀態列透明以後,需要做的就是對佈局的滑動新增事件,然後在滑動事件中計算滑動的距離,接下來根據滑動的距離設定頭部 Layout 的透明度,說起來複雜,程式碼卻只有十來行。
//預設透明度 private var statusAlpha = 0 // 新增滑動事件監聽 nsv_layout.setOnScrollChangeListener { _: NestedScrollView?, _: Int, scrollY: Int, _: Int, _: Int -> val headerHeight = top_layout.height val scrollDistance = Math.min(scrollY, headerHeight) statusAlpha = (255F * scrollDistance / headerHeight).toInt() setTopBackground() } // 設定頭部透明度 private fun setTopBackground() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { top_layout.setBackgroundColor(Color.argb(statusAlpha, 255, 255, 255)) val window = activity!!.window window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) window.statusBarColor = Color.argb(statusAlpha, 255, 255, 255) } } 複製程式碼
3、優化 Toplayout 高度
因為我們這裡將 Toplayout 替代了 Toolbar ,因此我們需要對 Toplayout增加狀態列的內邊距,防止 Toplayout 顯示出現異常。程式碼如下:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val lp: RelativeLayout.LayoutParams = top_layout.layoutParams as RelativeLayout.LayoutParams lp.topMargin = getSystemBarHeight() top_layout.layoutParams = lp } 複製程式碼
這樣就可以實現目標效果了,但是上面也兩個字叫做“逐漸”,而我現在看到有 APP 實現了當佈局滑動到一定高度就直接顯示,然後回退到一定高度以後就直接透明。這個邏輯通過上述程式碼也可以實現,我們僅僅是設定 statusAlpha 的值為0或者255時才重新整理 Toplayout 的背景透明度,但是我看見人家通過 CollapsingToolbarLayout 實現的,而 CollapsingToolbarLayout 來自 Material Design包的控制元件,屬於谷歌親生兒子,於是我就來學習一波 CollapsingToolbarLayout 。
CollapsingToolbarLayout 摺疊式標題欄
學習之前先複習一下自己的文章Material Design。文章最後一部分講到了 CollapsingToolbarLayout 的用法及一些屬性名稱的用法。
1、CollapsingToolbarLayout 佈局
CollapsingToolbarLayout 是不能單獨使用的,它必須作為 AppBarLayout 的直接子佈局來使用,而 AppBarLayout 又必須作為CoordinatorLayout 的子佈局。所以我們的佈局應該是這樣的:
<?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" tools:context=".MainActivity"> <android.support.design.widget.AppBarLayout android:layout_width="match_parent" android:layout_height="256dp" android:fitsSystemWindows="true"> <android.support.design.widget.CollapsingToolbarLayout android:id="@+id/collapsing_toolbar_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" app:contentScrim="?attr/colorPrimary" app:expandedTitleMarginStart="38dp" app:layout_scrollFlags="scroll|exitUntilCollapsed"> <ImageView android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" android:scaleType="centerCrop" android:src="@mipmap/h" app:layout_collapseMode="parallax" app:layout_collapseParallaxMultiplier="0.7"/> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_collapseMode="pin"/> </android.support.design.widget.CollapsingToolbarLayout> </android.support.design.widget.AppBarLayout> <android.support.v4.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="vertical" app:layout_behavior="@string/appbar_scrolling_view_behavior"> <WebView android:id="@+id/webview" android:layout_width="match_parent" android:layout_height="match_parent"> </WebView> </android.support.v4.widget.NestedScrollView> </android.support.design.widget.CoordinatorLayout> 複製程式碼
2、CollapsingToolbarLayout 屬性
- app:contentScrim=”?attr/colorPrimary” 這個屬性是指CollapsingToolbarLayout趨於摺疊狀態或者是摺疊狀態的時候的背景顏色,因為此時的CollapsingToolbarLayout就是一個簡單的ToolBar形狀,所以背景色我們還是設定系統預設的背景顏色。
- app:expandedTitleMarginStart=”38dp” 這個屬性是指設定擴張時候(還沒有收縮時)title與左邊的距離,不設定的時候有一個預設距離,個人感覺預設距離或許會更好。
- app:layout_scrollFlags=”scroll|exitUntilCollapsed” 這個屬性之前已經解釋過是什麼意思,這裡將它從ToolBar給貼到CollapsingToolbarLayout裡,是因為它現在做為AppBarLayout的唯一子佈局了,所以這個屬性就應該上一層賦值。
然後我們對CollapsingToolbarLayout內的ToolBar和ImageView的同一個屬性layout_collapseMode賦予了不同的值,這個屬性其實有三個值:
- none:有該標誌位的View在頁面滾動的過程中會如同普通的Toolbar一樣,就是簡單的顯示與隱藏效果
- pin:有該標誌位的View在頁面滾動的過程中會一直停留在頂部,比如Toolbar可以被固定在頂部
- parellax:有該標誌位的View在頁面滾動的過程中會產生位移,最後隱藏(這個位移不是垂直方向的直線運動)
ps:上面的 none 和 parellax 屬性效果後面也效果圖
3、CollapsingToolbarLayout 應用
知道了這些屬性以後,那麼該如何實現上面的效果呢? 生死看淡,不服就幹,直接給程式碼:
<?xml version="1.0" encoding="utf-8"?> <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ui.home.fragment.HomeFragmentA"> <com.google.android.material.appbar.AppBarLayout android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/white" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" app:elevation="0dp"> <com.vincent.baseproject.widget.XCollapsingToolbarLayout android:id="@+id/ctl_top_bar" android:layout_width="match_parent" android:layout_height="256dp" app:contentScrim="@color/white" app:layout_scrollFlags="scroll|exitUntilCollapsed" app:scrimVisibleHeightTrigger="120dp"> <ImageView android:id="@+id/top_iv_bg" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="150dp" android:scaleType="centerCrop" android:src="@mipmap/bg_launcher" app:layout_collapseMode="parallax"/> <androidx.appcompat.widget.Toolbar android:id="@+id/top_toolbar" android:layout_width="match_parent" android:layout_height="?android:attr/actionBarSize" app:layout_collapseMode="pin"> <LinearLayout android:layout_width="match_parent"...> </androidx.appcompat.widget.Toolbar> </com.vincent.baseproject.widget.XCollapsingToolbarLayout> </com.google.android.material.appbar.AppBarLayout> <androidx.core.widget.NestedScrollView...> </androidx.coordinatorlayout.widget.CoordinatorLayout> 複製程式碼
對照上面屬性可以知道,ImageView 是位移動畫直至隱藏,然後 Toolbar 是始終不變位置。我們看看效果:

如果這麼看,依然不清楚的話,那麼看看去掉動畫是什麼樣,我們修改 ImageView 佈局看看效果:
<ImageView android:id="@+id/top_iv_bg" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="150dp" android:scaleType="centerCrop" android:src="@mipmap/bg_launcher" app:layout_collapseMode="none"/> 複製程式碼

仔細看的話,應該可以看見上拉時 ImageView 是被擠上去,下滑的時候又直愣愣放下來,而前面的 parallax 屬性產生了一個動畫效果,就是上拉的時候頭部有擠壓效果,但是沒有被直接隱藏(即圖片的頂部一開始沒有被直接隱藏),下滑也是類似,多看兩遍效果圖還是很明顯的。
但是 CollapsingToolbarLayout layout_scrollFlags屬性是什麼意思呢?這個上面的文章裡面也有,還配有效果圖。補充一個上面文章沒有說清楚的一個選項: snap 。即 CollapsingToolbarLayout 如果使用 layout_scrollFlags 屬性的 snap 選項時,需配合其它屬性才行:
app:layout_scrollFlags="scroll|snap"
效果如下:

4、CollapsingToolbarLayout 重寫——獲取自定義屬性
現在實現效果之後還有一個問題,就是需要對 CollapsingToolbarLayout 展開與摺疊的狀態進行回撥,不然摺疊的時候我們的地區兩個字已經被白色覆蓋了,需要在摺疊的時候設定一個其它的顏色。設定顏色的時候需要說明一個問題,對於系統的 title 是支援屬性來設定顏色的,但是我們這裡屬於自定義頭部,因此只能自己想辦法通過事件來判斷,最後我們找到 CollapsingToolbarLayout 的回撥方法:
public void setScrimsShown(boolean shown, boolean animate) { if (this.scrimsAreShown != shown) { if (animate) { this.animateScrim(shown ? 255 : 0); } else { this.setScrimAlpha(shown ? 255 : 0); } this.scrimsAreShown = shown; } } 複製程式碼
由於是回撥方法並不是介面回撥,因此我們需要繼承 CollapsingToolbarLayout 並重寫 setScrimsShown 方法才能實現回撥的介面,程式碼如下:
class XCollapsingToolbarLayout : CollapsingToolbarLayout { var mListener: OnScrimsListener? = null // 漸變監聽 var isCurrentScrimsShown: Boolean = false// 當前漸變狀態 constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) override fun setScrimsShown(shown: Boolean, animate: Boolean) { super.setScrimsShown(shown, animate) if(isCurrentScrimsShown != shown){ isCurrentScrimsShown = shown mListener?.onScrimsStateChange(shown) } } /** * CollapsingToolbarLayout漸變監聽器 */ interface OnScrimsListener { /** * 漸變狀態變化 * * @param shown漸變開關 */ fun onScrimsStateChange(shown: Boolean) } } 複製程式碼
實現了自定義,我們就可以通過介面回撥在摺疊和展開的第一時間來設定我們想要的背景和顏色:
ctl_top_bar.mListener = object : XCollapsingToolbarLayout.OnScrimsListener { override fun onScrimsStateChange(shown: Boolean) { if (shown) { homeA_tv_address.setTextColor( ContextCompat.getColor( context!!, com.vincent.baseproject.R.color.black ) ) } else { homeA_tv_address.setTextColor( ContextCompat.getColor( context!!, com.vincent.baseproject.R.color.white ) ) } homeA_tv_search.isSelected = shown } } 複製程式碼
效果咋樣,瞅一瞅:

OK,目前我們就實現了將第一種頭部的漸變修改為 Material Design 設計為瞬間改變。但是我們能不能使用 CollapsingToolbarLayout 來實現頭部背景的漸變呢?要實現這個效果,我們需要看看摺疊和展開是的標誌位是根據什麼來判斷的?檢視原始碼的 setScrimsShown 方法:
public void setScrimsShown(boolean shown) { this.setScrimsShown(shown, ViewCompat.isLaidOut(this) && !this.isInEditMode()); } public void setScrimsShown(boolean shown, boolean animate) { if (this.scrimsAreShown != shown) { if (animate) { this.animateScrim(shown ? 255 : 0); } else { this.setScrimAlpha(shown ? 255 : 0); } this.scrimsAreShown = shown; } } 複製程式碼
這個時候我們在本類全域性搜尋 setScrimsShown 方法,看看是什麼地方傳入的 shown ,判斷標準是什麼?
final void updateScrimVisibility() { if (this.contentScrim != null || this.statusBarScrim != null) { this.setScrimsShown(this.getHeight() + this.currentOffset < this.getScrimVisibleHeightTrigger()); } } 複製程式碼
走到這裡我們發現,通過正常的辦法是沒有辦法實現漸變的,因為我們需要拿不到高度、偏移值。再查詢 updateScrimVisibility 方法發現這個方法被呼叫的地方也3處:
// 343 行 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); ...... this.updateScrimVisibility(); } // 653行 public void setScrimVisibleHeightTrigger(@IntRange(from = 0L) int height) { if (this.scrimVisibleHeightTrigger != height) { this.scrimVisibleHeightTrigger = height; this.updateScrimVisibility(); } } // 734行:(私有類) private class OffsetUpdateListener implements OnOffsetChangedListener { OffsetUpdateListener() { } public void onOffsetChanged(AppBarLayout layout, int verticalOffset) { ...... CollapsingToolbarLayout.this.updateScrimVisibility(); ...... } } 複製程式碼
檢視原始碼得知就算我們重寫前面兩處呼叫 updateScrimVisibility 的方法,也不能重寫第三處。因此正常手段是不能實現漸變的。那麼其它非正常手段呢?比如在 setScrimsShown 處使用反射拿到總高度、偏移量,算出一個百分比,也是可以的,但是這樣暴力操作也沒有必要。 除非是特地場景,一般情況下還是不要去反射拿取系統非公開的欄位或方法。既然原始碼設定這些許可權修飾符,肯定是有原因的,假設下一個版本修改這個屬性的話,APP 就要出問題了!
我還看到通過計算 AppBarLayout 的偏移量來實現頭部的漸變,這個奇技淫巧也比我們暴力獲取 api 要好得多,參考地址: 使用AppBarLayout+CollapsingToolbarLayout實現自定義工具欄摺疊效果
一個知識點,從盲區到技能點,完成以後覺得不過如此,但是學習的過程中每個人都是費盡九牛二虎之力才走到熟悉。謹以此文來紀念那些天各個QQ群提問的烤魚!
原始碼:
補充 CollapsingToolbarLayout 屬性
可摺疊式標題欄 -- CollapsingToolbarLayout 的屬性
- 設定展開之後 toolbar 字型的大小
app:expandedTitleTextAppearance="@style/toolbarTitle" <style name="toolbarTitle" > <item name="android:textSize">12sp</item> </style> 複製程式碼
- 設定摺疊之後 toolbar 字型的大小
app:collapsedTitleTextAppearance="@style/toolbarTitle" <style name="toolbarTitle" > <item name="android:textSize">12sp</item> </style> 複製程式碼
- 設定展開之後 toolbar 標題各個方向的距離
//展開之後的標題預設在左下方,只有以下這兩個屬性管用 //距離左邊的 margin 值 app:expandedTitleMarginStart="0dp" //距離下方的 margin 值 app:expandedTitleMarginBottom="0dp" 複製程式碼
- 設定展開之後 toolbar 標題下方居中
app:expandedTitleGravity="bottom|center" 複製程式碼
- 設定標題不移動,始終在 toolbar 上
app:titleEnabled="false" 複製程式碼
- 設定 toolbar 背景顏色
//如果設定狀態列透明的話,狀態列會跟toolbar顏色一致 app:contentScrim="@color/colorPrimaryDark" 複製程式碼
- 設定合併之後的狀態列的顏色
//如果設定狀態列透明,則此屬性失效 app:statusBarScrim="@color/colorAccent" 複製程式碼