前言
CollapsingToolbarLayout是android MaterialDeign提供的一个组件,通过搭配AppBarLayout可实现toolbar的折叠效果。下边就通过仿实现稀土掘金个人中心页来讲解它的具体用法。先上效果图:
实现
我们将toolbar分为两层:
- 红色圈中的大模块,暂名:A
- 绿色圈中的模块,暂名:B 通过分层再分析下效果:
- 当视图向上滚动时,A会逐渐折叠
- 当A完全折叠后,B的图标会更新状态,并且用户的信息会向上浮动显示在B中
- 当A没有完全折叠后,用户信息会消失,并且B中的图标会恢复原来的状态
通过效果分析,我们可以这么设计:
- 将A内容包含在CollapsingToolbarLayout中,再配合CoordinatorLayout+AppBarLayout,即可实现A内容的折叠
- 监听A内容是否折叠,改变B的状态
CollapsingToolbarLayout折叠事件的监听
- 通过AppBarLayout.OnOffsetChangedListener的回调
Interface definition for a callback to be invoked when an AppBarLayout's vertical offset changes.
// TODO(b/76413401): update this interface after the widget migration
public interface OnOffsetChangedListener extends BaseOnOffsetChangedListener<AppBarLayout> {
void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset);
}
查看官方api文档,该回调可以获取到AppBarLayout的垂直偏移量。通过判断偏移量的大小,即可获取到当前toolbar的状态
abstract class AppBarStateChangeListener : AppBarLayout.OnOffsetChangedListener {
enum class State {
EXPANDED, COLLAPSED, IDLE
}
private var mCurrentState = State.IDLE
override fun onOffsetChanged(appBarLayout: AppBarLayout?, verticalOffset: Int) {
if (verticalOffset == 0) {
if (mCurrentState != State.EXPANDED) {
onStateChanged(appBarLayout, State.EXPANDED);
}
mCurrentState = State.EXPANDED
} else if (appBarLayout != null) {
if (Math.abs(verticalOffset) >= appBarLayout.totalScrollRange) {
if (mCurrentState != State.COLLAPSED) {
onStateChanged(appBarLayout, State.COLLAPSED);
}
mCurrentState = State.COLLAPSED
} else {
if (mCurrentState != State.IDLE) {
onStateChanged(appBarLayout, State.IDLE);
}
mCurrentState = State.IDLE;
}
}
}
abstract fun onStateChanged(appBarLayout: AppBarLayout?, state: State)
}
- 通过CollapsingToolbarLayout的setScrimsShown函数
Set whether the content scrim and/or status bar scrim should be shown or not. Any change in the vertical scroll may overwrite this value.
Params:
shown – whether the scrims should be shown
animate – whether to animate the visibility change
See Also:getStatusBarScrim(), getContentScrim()
public void setScrimsShown(boolean shown, boolean animate) {
if (scrimsAreShown != shown) {
if (animate) {
animateScrim(shown ? 0xFF : 0x0);
} else {
setScrimAlpha(shown ? 0xFF : 0x0);
}
scrimsAreShown = shown;
}
}
查看官方api文档,可以看到当内容发生变化时,该方法会被自动调用,因此我们可以通过扩展该方法
class NestCollapsingToolbarLayout : CollapsingToolbarLayout {
private var mIsScrimsShown: Boolean = false
private lateinit var scrimsShowListener: OnScrimsShowListener
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 (mIsScrimsShown != shown) {
mIsScrimsShown = shown
if (scrimsShowListener != null) {
scrimsShowListener.onScrimsShowChange(this, mIsScrimsShown)
}
}
}
fun setOnScrimesShowListener(listener: OnScrimsShowListener){
scrimsShowListener = listener
}
interface OnScrimsShowListener {
fun onScrimsShowChange(
nestCollapsingToolbarLayout: NestCollapsingToolbarLayout,
isScrimesShow: Boolean
)
}
}
当然这两个方法会有区别,方法一监听的offset为整个AppBarLayout包含内容的高度,方法二只针对CollapsingToolbarLayout包含内容的高度。这里我使用方法二
布局设计
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:orientation="vertical"
tools:context=".MainActivity">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.example.behaviordemo.NestCollapsingToolbarLayout
android:id="@+id/toolbarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="match_parent"
android:layout_height="150dp"
android:scaleType="centerCrop"
android:src="@drawable/user_profile_header" />
<ImageView
android:id="@+id/ivHead"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginLeft="10dp"
android:layout_marginTop="125dp"
android:background="@drawable/empty_avatar_user" />
<LinearLayout
android:id="@+id/llAuthor"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/ivHead"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/author_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="PG_KING"
android:textColor="@android:color/black"
android:textSize="16sp" />
<ImageView
android:id="@+id/ivLevel"
android:layout_width="27dp"
android:layout_height="15dp"
android:layout_marginLeft="5dp"
android:background="@drawable/ic_user_big_lv1" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/llAuthor"
android:layout_marginLeft="8dp"
android:text="Android"
android:textColor="@android:color/darker_gray"
android:textSize="13sp" />
<TextView
android:layout_width="80dp"
android:layout_height="35dp"
android:layout_alignTop="@+id/llAuthor"
android:layout_alignParentRight="true"
android:layout_marginRight="10dp"
android:background="@drawable/bg_edit"
android:gravity="center"
android:text="编辑"
android:textColor="@color/theme_blue"
android:textSize="15sp" />
</RelativeLayout>
</com.example.behaviordemo.NestCollapsingToolbarLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@android:color/white"
android:orientation="horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="vertical"
android:paddingLeft="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="10000"
android:textColor="@android:color/black"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="关注"
android:textColor="@android:color/darker_gray"
android:textSize="13sp" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="vertical"
android:paddingLeft="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="10000"
android:textColor="@android:color/black"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="关注"
android:textColor="@android:color/darker_gray"
android:textSize="13sp" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="vertical"
android:paddingLeft="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="10000"
android:textColor="@android:color/black"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="关注"
android:textColor="@android:color/darker_gray"
android:textSize="13sp" />
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="10dp"
android:background="@android:color/darker_gray" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="60dp"
app:tabIndicatorColor="@color/theme_blue"
app:tabIndicatorFullWidth="false"
app:tabSelectedTextColor="@color/theme_blue"
app:tabTextColor="@android:color/darker_gray" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<RelativeLayout
android:id="@+id/rlTitle"
android:layout_width="match_parent"
android:layout_height="60dp"
android:paddingLeft="15dp"
android:paddingRight="15dp">
<ImageView
android:id="@+id/ivBack"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_centerVertical="true"
android:src="@drawable/ic_back" />
<LinearLayout
android:id="@+id/llSmallAuthor"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginLeft="30dp"
android:layout_toRightOf="@+id/ivBack"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="invisible">
<ImageView
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:src="@drawable/empty_avatar_user" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:text="PG_KING"
android:textColor="@android:color/black"
android:textSize="16sp" />
<ImageView
android:layout_width="27dp"
android:layout_height="15dp"
android:layout_marginLeft="5dp"
android:background="@drawable/ic_user_big_lv1" />
</LinearLayout>
<ImageView
android:id="@+id/ivShare"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:src="@drawable/ic_share" />
<ImageView
android:id="@+id/ivUserData"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_centerVertical="true"
android:layout_marginRight="30dp"
android:layout_toLeftOf="@+id/ivShare"
android:src="@drawable/ic_userdata" />
</RelativeLayout>
</FrameLayout>
- AppBarLayout+CollapsingToolbarLayout需要实现折叠效果,必须选择CoordinatorLayout作为根布局
- CollapsingToolbarLayout需要设置app:layout_scrollFlags="scroll|exitUntilCollapsed"
- 定义AppBarLayout与滚动视图之间的联系:在RecyclerView或者任意支持嵌套滚动的view比如NestedScrollView上添加app:layout_behavior属性配置
实现滚动效果
appBarLayout.setOnScrimesShowListener(object :
NestCollapsingToolbarLayout.OnScrimsShowListener {
override fun onScrimsShowChange(
nestCollapsingToolbarLayout: NestCollapsingToolbarLayout,
isScrimesShow: Boolean
) {
if (isScrimesShow) {
rlTitle.setBackgroundColor(Color.WHITE)
ivBack.setImageResource(R.drawable.ic_back_blue)
ivShare.setImageResource(R.drawable.ic_share_blue)
ivUserData.setImageDrawable(
tintDrawable(
resources.getDrawable(R.drawable.ic_userdata),
ColorStateList.valueOf(Color.parseColor("#B6B6B6"))
)
)
showSmallAuthor()
} else {
rlTitle.setBackgroundColor(Color.TRANSPARENT)
ivBack.setImageResource(R.drawable.ic_back)
ivShare.setImageResource(R.drawable.ic_share)
ivUserData.setImageDrawable(
tintDrawable(
ivUserData.drawable,
ColorStateList.valueOf(Color.parseColor("#FFFFFF"))
)
)
if (objectAnimator.isRunning) {
objectAnimator.cancel()
}
llSmallAuthor.visibility = View.INVISIBLE
}
}
})
补充
1、这里还使用了TabLayout+ViewPager的组合,用于实现主内容区的逻辑。tablayout和viewpager的关联绑定
pagerAdapter = object : ListPagerAdapter(supportFragmentManager, fragments) {
override fun getPageTitle(position: Int): CharSequence? {
if (tabList.size > position) {
return tabList.get(position)
}
return ""
}
}
viewPager.adapter = pagerAdapter
tabLayout.setupWithViewPager(viewPager)
2、动态改变drawable的颜色
看上图,统计柱状图由白色变成灰色~这里没有使用两张图片进行切换,而是使用了系统提供的DrawableCompat.setTintList函数
fun tintDrawable(drawable: Drawable, colors: ColorStateList): Drawable {
val wrappedDrawable = DrawableCompat.wrap(drawable)
DrawableCompat.setTintList(wrappedDrawable, colors)
return wrappedDrawable
}
// 使用
ivUserData.setImageDrawable(
tintDrawable(
resources.getDrawable(R.drawable.ic_userdata),
ColorStateList.valueOf(Color.parseColor("#B6B6B6"))
)
)
拓展
app:layout_scrollFlags属性介绍
<attr name="layout_scrollFlags">
<!-- Disable scrolling on the view. This flag should not be combined with any of the other
scroll flags. -->
<flag name="noScroll" value="0x0"/>
<!-- The view will be scroll in direct relation to scroll events. This flag needs to be
set for any of the other flags to take effect. If any sibling views
before this one do not have this flag, then this value has no effect. -->
<flag name="scroll" value="0x1"/>
<!-- When exiting (scrolling off screen) the view will be scrolled until it is
'collapsed'. The collapsed height is defined by the view's minimum height. -->
<flag name="exitUntilCollapsed" value="0x2"/>
<!-- When entering (scrolling on screen) the view will scroll on any downwards
scroll event, regardless of whether the scrolling view is also scrolling. This
is commonly referred to as the 'quick return' pattern. -->
<flag name="enterAlways" value="0x4"/>
<!-- An additional flag for 'enterAlways' which modifies the returning view to
only initially scroll back to it's collapsed height. Once the scrolling view has
reached the end of it's scroll range, the remainder of this view will be scrolled
into view. -->
<flag name="enterAlwaysCollapsed" value="0x8"/>
<!-- Upon a scroll ending, if the view is only partially visible then it will be
snapped and scrolled to it's closest edge. -->
<flag name="snap" value="0x10"/>
<!-- An additional flag to be used with 'snap'. If set, the view will be snapped to its
top and bottom margins, as opposed to the edges of the view itself. -->
<flag name="snapMargins" value="0x20"/>
</attr>
查看官方api文档,我们整体梳理下:
- 其他属性的使用需要配合scroll,否则会没有效果,正确用法举例:app:layout_scrollFlags="scroll|exitUntilCollapsed"
- enterAlways:不管向下还是向上滚动,优先滚动的是AppBarLayout中的childView
- exitUntilCollapsed:向上滚动,优先滚动的是AppBarLayout中的childView;向下滚动,优先滚动的是其他视图,直到其他视图已经滚动到顶部再滚动AppBarLayout
- snap:可配合其他属性一起使用,可以保证AppBarLayout完全显示或者完全隐藏而不会显示部分
具体的效果,可以通过写例子进行体验。跑过效果理解起来才没那么枯燥~
最后附上demo地址:gitee-demo