首先看一下效果图
1. 这是来自 Google 的设计文档的示例图
2. 这是我自己的不才之作
前言
以前看 Google 的 Material Design 指导文档,看到了这么一个效果,印象挺深刻的。最近在家,又想起这么个效果,反正事也不多,所以果断着手实现它。
直接上代码
/activity_main.xml
<?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:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:colorBackground"
android:fitsSystemWindows="true"
tools:context="com.jossing.collapsingtoolbarlayoutdemo.MainActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/ctl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:contentScrim="@color/colorPrimary"
app:scrimAnimationDuration="@android:integer/config_shortAnimTime"
app:layout_scrollFlags="exitUntilCollapsed|scroll|snap">
<ImageView
android:layout_width="match_parent"
android:layout_height="336dp"
android:scaleType="centerCrop"
android:fitsSystemWindows="true"
app:layout_collapseMode="parallax"
android:src="@drawable/material_design_vs_flat_design" />
<android.support.v7.widget.Toolbar
android:id="@+id/toolBar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:fitsSystemWindows="false"
app:layout_collapseMode="pin">
<!-- Toolbar 中的自定义 View -->
<RelativeLayout...>
</android.support.v7.widget.Toolbar>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<!-- 卡片列表 -->
<android.support.v7.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="-112dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</android.support.design.widget.CoordinatorLayout>
以上布局的代码中,可以看到我给 ImageView
设定了固定的 height
,为了让 RecyclerView
中的内容往上顶,以盖住展开的 AppBar
,我还给 RecyclerView
设置了一个 -112dp
的 marginTop
。至于为什么顶上去之后会盖住 AppBar ~,因为在 Android L 以及升级版本中,View 的绘制层次(暂且叫做绘制层次吧)是根据属性 elevation
来测量的,elevation
低的绘制在“下方”,高的绘制到“上”方。而 AppBar
折叠状态的 elevation
为 4dp
,展开状态和中间状态的 elevation
是 0dp
,而 CardView
默认状态下是 2dp
,这就解释了为什么 AppBar
一会儿在卡片列表下方,一会儿又到了卡片列表上方了。这时再回头看看前面那张黑黑的 Z 轴透视图,对于刚开始学习的朋友,应该也很好理解了。
/MainActivity.java#onPostResume()
@Override
protected void onPostResume() {
super.onPostResume();
abl.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
private float density = getResources().getDisplayMetrics().density; // 屏幕缩放比例
private int marginTopOriginal;
private int ratio; // AppBarLayout 的偏移范围与 marginTop 绝对值的比例
private boolean isFirst = true;
@Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
// Log.e("VerticalOffset", ""+verticalOffset);
// Log.e("VerticalOffset_dp", ""+verticalOffset / density);
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) rv.getLayoutParams();
if (isFirst) {
// RecyclerView 原来的 marginTop 的绝对值
marginTopOriginal = lp.topMargin;
// ImageView 的高度
int contentHeight = ctl.getChildAt(0).getLayoutParams().height;
// 减去 statusBar + Toolbar 的高度
int verticalOffsetRange = Math.round(contentHeight - 80 * density);
ratio = verticalOffsetRange / Math.abs(marginTopOriginal);
isFirst = false;
} else {
// AppBarLayout 每折叠一点,marginTop 就按比例增加一点
int newMarginTop = marginTopOriginal + Math.abs(verticalOffset / ratio);
if (newMarginTop <= 0) {
// 避免将 marginTop 设置为大于 0 的值
lp.setMargins(0, newMarginTop, 0, 0);
rv.setLayoutParams(lp);
}
}
}
});
}
当然在 #onCreate() 中,少不了这么一行代码,
setSupportActionBar(toolbar);
其中 abl
是 AppBarLayout
的对象,ctl
是 CollapsingToolbarLayout
的对象,rv
是 RecyclerView
的对象,toolbar
是 Toolbar
的对象。在 #onPostResume()
中写这些代码,是因为其中包含有获取 ImageView
高度的操作,这在 #onCreate()
中是不行的。代码中虽然已经写明注释,但也许并不够完善,所以如果读者还有不明白的地方,欢迎评论提问。
/style.xml
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
</style>
在 /style.xml
中还需要自定义一个 Theme
,内容是去除 ActionBar
、透明 StatusBar
、允许绘制 StatusBar
的背景。然后在 /AndroidManifest.xml
配置,让 activity 使用这个 Theme
。
更新——2017/02/02
把代码中 ratio
变量改为 float 类型,比例更精准
private float ratio;
利用AppBarLayout
的一个 getTotalScrollRange()
方法,直接获取它的 offset 范围,就可以把 ratio 的计算方式更改为
ratio = abl.getTotalScrollRange() / 1f / Math.abs(marginTopOriginal);
然后 newMarginTop
计算时记得加一个 Math.round()
就可以了。
结语
以上的代码已经实现了那个比较特别的折叠效果了,这么看起来还是比较简单的对不对?只要给 AppBarLayout
增加 listener
,然后在回调中写上几句代码就搞定了。其实 /手动笑哭
,在实现它的过程中,我也是踩了不少坑,因为刚开始并不知道可以给 AppBarLayout
设置 listener
,我尝试了各种奇怪的方式,比如给 RecyclerView
、CollapsingToolbarLayout
设置 OnGlobalLayoutListener
来监听 Y、bottom 的变化,还给 RecyclerView
设置过 OnScrollListener
,等等,都没有想要的效果。上面所说的两个 listener
,在 AppBarLayout
收缩折叠时并不会触发,这里应该涉及到了嵌套滚动,不过我还没能很懂里面的原理呢~,说来也是奇怪为什么我可以想到给那些 View
设置监听,唯独想不到 AppBarLayout
呢 /再次手动笑哭
?对了,这个效果我想不到一个好的名字,以至于我在网上一直没有搜索到其他的实现,也许大家有好主意?
最后,欢迎大家评论支持和指出我的不足,也许大家有更好的实现方式,欢迎与我分享,本人在此表示感谢~