本篇文章的来源是一开始我须要实现相似 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 替代了 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
学习以前先复习一下本身的文章Material Design。文章最后一部分讲到了 CollapsingToolbarLayout 的用法及一些属性名称的用法。app
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>
复制代码
- 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 属性效果后面也效果图
知道了这些属性之后,那么该如何实现上面的效果呢? 生死看淡,不服就干,直接给代码:
<?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
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 展开与折叠的状态进行回调,否则折叠的时候咱们的地区两个字已经被白色覆盖了,须要在折叠的时候设置一个其它的颜色。设置颜色的时候须要说明一个问题,对于系统的 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"
复制代码