漸變式透明標題欄與 CollapsingToolbarLayout 摺疊式標題欄

本篇文章的來源是一開始我須要實現相似 IOS 的彈簧動畫,當時選擇了 ScrollView +頭部 Layout 來實現的,實現效果如圖:java

漸變式透明標題欄

能夠看到,頂部標題區能夠隨着手指滑動而 逐漸 透明或者 逐漸 覆蓋,這個效果是我實現的第一個版本的效果,原理也很是簡單,首頁佈局以下:android

<?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>
複製代碼

接下來咱們分爲兩步:git

一、咱們須要作的是實現狀態欄透明

經過狀態欄透明達到沉浸式效果。這個只要也很是簡單,直接上代碼:github

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

        }

    }
複製代碼

二、對佈局添加滑動監聽 重頭戲

狀態欄透明之後,須要作的就是對佈局的滑動添加事件,而後在滑動事件中計算滑動的距離,接下來根據滑動的距離設置頭部 Layout 的透明度,提及來複雜,代碼卻只有十來行。web

//默認透明度
 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)
        }
    }

複製代碼

三、優化 Toplayout 高度

由於咱們這裏將 Toplayout 替代了 Toolbar ,所以咱們須要對 Toplayout增長狀態欄的內邊距,防止 Toplayout 顯示出現異常。代碼以下:api

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 。bash

CollapsingToolbarLayout 摺疊式標題欄

學習以前先複習一下本身的文章Material Design。文章最後一部分講到了 CollapsingToolbarLayout 的用法及一些屬性名稱的用法。app

一、CollapsingToolbarLayout 佈局

CollapsingToolbarLayout 是不能單獨使用的,它必須做爲 AppBarLayout 的直接子佈局來使用,而 AppBarLayout 又必須做爲CoordinatorLayout 的子佈局。因此咱們的佈局應該是這樣的:ide

<?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>
複製代碼

二、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 屬性效果後面也效果圖

三、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" 效果以下:

四、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 屬性

可摺疊式標題欄 -- 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"
複製代碼
相關文章
相關標籤/搜索