Android 自定义CollapsingToolbarLayout动效

面对开发中遇到的需求,怎么办呢?研究呗!

关联控件

  • CoordinatorLayout
  • AppBarLayout
  • CollapsingToolbarLayout
  • RecyclerView
  • SwipeRefreshLayout

下面放出全部布局,然后逐一讲解内容实现。

首先看一下完成后的效果:

完成效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<android.support.design.widget.CoordinatorLayout
android:id="@+id/coo_root"
android:layout_width="match_parent"
android:layout_height="match_parent">

<android.support.design.widget.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#177FFF">

<android.support.design.widget.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="180dp"
android:minHeight="74dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed|enterAlwaysCollapsed"
app:statusBarScrim="@android:color/transparent">

<ImageView
android:id="@+id/iv_bg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:scaleType="fitXY"
android:src="@drawable/ic_bg_logo"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="0.9" />

<com.dwzq.market.view.trade.reversebond.ExtendToolbar
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="74dp"
app:contentInsetStart="0dp"
app:layout_collapseMode="pin" />

</android.support.design.widget.CollapsingToolbarLayout>

</android.support.design.widget.AppBarLayout>

<!-- 此处放置内容控件,如RecyclerView -->
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_hold_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="10dp"
android:paddingRight="10dp"
app:behavior_overlapTop="50dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />

</android.support.design.widget.CoordinatorLayout>

<TextView
android:id="@+id/tv_total_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="30dp"
android:layout_marginTop="49dp"
android:text="总数值"
android:textColor="@color/p_white"
android:textSize="14sp" />

<com.dwzq.market.widget.textview.DigitsFontTextView2
android:id="@+id/tv_total_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="30dp"
android:layout_marginTop="79dp"
android:text="90,899.63"
android:textColor="@color/p_white"
android:textSize="30sp" />
</FrameLayout>

折叠部分

首先折叠动效,在MaterialDesign中有类似的实现,就是CollapsingToolbarLayout。

CollapsingToolbarLayout需要被CoordinatorLayout,AppBarLayout包裹后才可正常使用。

这里面需要注意几个参数:

  • app:layout_scrollFlags
    这个参数用来确定折叠Toolbar的折叠方式

    • scroll 当前页面可以滚动
    • exitUntilCollapsed 滚动结束后,仍然显示Toolbar(折叠的)
    • enterAlwaysCollapsed 需要配合android:minHeight使用,表示最小折叠高度(当需要折叠保留的部分大于Toolbar的高度情况)
  • android:minHeight

    1. 配合exitUntilCollapsed使用,保持最小高度
    2. 当内容控件(后面会讲)存在高度偏移时app:behavior_overlapTop,此参数需要设置,默认可设置为?actionBarSize
    3. 当CollapsingToolbarLayout配合RecyclerView使用的时候,可能会存在最后一项显示不完整,可设置为?actionBarSize来解决
  • app:statusBarScrim
    当进行全面屏适配时,折叠后状态栏的颜色设置,可设置为@android:color/transparent

背景图片部分

由于需求中,Toolbar上存在背景图片,并且需要在折叠中依旧浮动在Toolbar上,设置如下。

在CollapsingToolbarLayout中,第一个子布局放置ImageView做为背景,同时根据需求调整gravity等属性。

需要特殊提示的属性:

  • android:scaleType
    这里面有很多选项,具体需要查阅文档。

    • fitXY
      可以不用保持图像的宽高比,从控件的左上角分别对图片的宽和高进行缩放
    • centerCrop
      保持图像的宽高比,进行缩放图像
  • app:layout_collapseMode="parallax"
    视差模式,在折叠的时候会有个视差折叠的效果

  • app:layout_collapseParallaxMultiplier="0.9"
    设置视差范围,0-1越大视差越大

Toolbar部分

由于需求中的Toolbar比常规工具栏多出一部分,用于放置缩小折叠后的值。那么我们自定义一个Toolbar。

使用时需要注意的参数

  • app:contentInsetStart="0dp"
    在自定义Toolbar时,有时会出现左侧有一段空白无法使用,这时候需要进行设置重置空白。
  • app:layout_collapseMode="pin"
    固定模式,收缩到最后固定在顶端。

布局文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="74dp">

<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/fl_value_bar"
android:layout_alignParentTop="true"
android:layout_marginLeft="20dp"
android:src="@drawable/hq_white_back" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/fl_value_bar"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:gravity="center_vertical"
android:text="标题栏文字"
android:textColor="@color/p_white"
android:textSize="18sp" />

<FrameLayout
android:id="@+id/fl_value_bar"
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_alignParentBottom="true" />
</RelativeLayout>

自定义Toolbar类文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ExtendToolbar extends Toolbar {
public ExtendToolbar(Context context) {
this(context, null);
}

public ExtendToolbar(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public ExtendToolbar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}

private void init() {
LayoutInflater mInflater = LayoutInflater.from(getContext());
View mView = mInflater.inflate(R.layout.toolbar_reverse_bond, null);
LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.NO_GRAVITY);
addView(mView, lp);
}
}

RecyclerView 数据列表部分

在AppBarLayout外,CoordinatorLayout内,放置需要显示的内容,此处可以为NestedScrollView,RecyclerView,但是不推荐使用ScrollView,存在BUG。

当CollapsingToolbarLayout配合RecyclerView使用的时候,可能会存在最后一项显示不完整,可设置CollapsingToolbarLayout属性android:minHeight="?actionBarSize"来解决。

需要介绍的参数:

  • app:behavior_overlapTop="50dp"
    当数据列表需要向上偏移进入CollapsingToolbar部分时,可以设置此参数,向上偏移。
  • app:layout_behavior="@string/appbar_scrolling_view_behavior"
    一定需要添加此参数,表示该控件与CollapsingToolbar通过behavior绑定。

Toolbar 展示数据部分

最开始考虑是用behavior来操作控件滑动,但是behavior因为需要依附在Toolbar控件上,通过app:layout_anchorapp:layout_anchorGravity来操作位置,但是难以做到我需要的效果。

那么考虑使用FrameLayout + TextView将控件浮在布局上方,监听Toolbar移动位置,从而改变TextView的大小和位置。

首先需要取得控件在屏幕中的位置,由于控件的位置需要Measure,Layout之后才能获得,那么我们创建一个监听树来监听控件加载完毕的回调。

1
2
3
4
5
6
7
8
9
10
11
12
final ViewTreeObserver viewTreeObserver = getActivity().getWindow().getDecorView().getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
oX = mBinding.tvTotalValue.getLeft();
oY = mBinding.tvTotalValue.getTop();
// 移除GlobalLayoutListener监听
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
getActivity().getWindow().getDecorView().getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
}
});

此时我们就获得了控件距离左侧和上方的位置,之后可以根据折叠后的位置、滑动距离、最大滑动距离,来计算出偏移比例,从而展示出动画效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
appBar.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> {
int totalScrollRange = appBarLayout.getTotalScrollRange();
int absOffset = Math.abs(verticalOffset);
float percent = (float) absOffset / (float) totalScrollRange;

//值
int targetX = DisplayUtils.dp2px(96);
int targetY = DisplayUtils.dp2px(51);
//名称
int targetNameX = DisplayUtils.dp2px(20);

if (oX != 0 || oY != 0) {
//总市值 标题
mBinding.tvTotalName.setX(oX - (oX - (float) targetNameX) * percent);
//总市值 值
mBinding.tvTotalValue.setX(oX + ((float) targetX - oX) * percent);
mBinding.tvTotalValue.setY(oY - (oY - (float) targetY) * percent);
mBinding.tvTotalValue.setTextSize(TypedValue.COMPLEX_UNIT_SP, 30f - 14f * percent);
}
});

完成以上操作后,就可以实现该动画效果了。此时还可以进行一些扩展功能。比如下拉刷新,全屏模式适配。

扩展:下拉刷新

使用原生方案实现下拉刷新,在布局的根部,使用SwipeRefreshLayout包裹全部布局

1
2
3
4
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

由于监听的是下拉时的事件,那么当Toolbar折叠之后的下拉也会被监听到,这并非我所需要的效果,此时需要在addOnOffsetChangedListener中进行一点设置即可。

1
2
3
4
5
6
//当Toolbar完全展开之后,才可使用下拉刷新
if (verticalOffset >= 0){
refreshLayout.setEnabled(true);
}else {
refreshLayout.setEnabled(false);
}

扩展:全屏适配的一些提示点

  • fitSystemWindow 针对状态栏适配
  • 根布局调整padding
  • CollapsingToolbarLayout设置状态栏折叠时颜色
    app:statusBarScrim="@android:color/transparent"
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×