Android|CoordinatorLayout+AppBarLayout实现可折叠工具栏

需求背景

22年12月中旬的时候,由于其他迭代缺人,让我支援一下,其中有个页面的需求如下,要求实现一个可折叠效果的信息展示卡片,实现完大概就是下面动图的效果。

折叠效果

查了下资料,其实就是Android Studio里ScrollingActivity那个模版里的效果,大概如下,也可以扩展实现很多更高级的样式,比如B站视频播放页面视频上划折叠的效果。

ScrollingActivity效果

其实信息展示卡片随着划动折叠,还有别的方法,为了防止后面产品有奇怪的需求,就选择了ScrollingActivity的实现方案。

实现方案

方案其实也不难,我也不太擅长写界面,也是我第一次使用CoordinatorLayout布局。这里使用了CoordinatorLayoutAppBarLayoutCollapsingToolbarLayout,然后使用默认提供的appbar_scrolling_view_behavior进行实现的。

界面布局

主要通过xml布局文件进行适配即可,真正的代码量倒是不多,大概拆一下布局,如下。

布局示意图

整体是个CoordinatorLayout,coordinator翻译过来就是“协调员”的意思,个人理解,它通过behavior协调其中各个View的展示。

上面的部分整体是AppBarLayout,里面包裹着一个CollapsingToolbarLayout和一个XTabLayout

最下面是个ViewPager,承载着一个Fragment,里面有一个RecyclerView,需要设置ViewPager的属性:app:layout_behavior="@string/appbar_scrolling_view_behavior"

主体的xml文件大致如下:

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
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout 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="@color/white"
android:orientation="vertical"
tools:context=".activity.MineActivity">

<android.support.design.widget.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/collasping_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="44dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed">

<!-- 此处是作者信息相关的布局 -->

<!-- 此处是标题栏的布局 -->

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

<com.androidkun.xtablayout.XTabLayout
android:id="@+id/mine_tab_layout"
android:layout_width="match_parent"
android:layout_height="39dp"
android:layout_marginBottom="1dp"
app:layout_collapseMode="pin"
app:xTabIndicatorColor="#0079ff"
app:xTabIndicatorWidth="20dp"
app:xTabIndicatorHeight="2dp"
app:xTabPaddingEnd="20dp"
app:xTabPaddingStart="20dp"
app:xTabIndicatorRoundX="1dp"
app:xTabIndicatorRoundY="1dp"
app:xTabMode="scrollable"
app:xTabSelectedTextColor="@color/c2_2"
app:xTabSelectedTextSize="16dp"
app:xTabTextBold="false"
app:xTabTextColor="@color/c07"
app:xTabTextSelectedBold="true"
app:xTabTextSize="16dp" />

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

<android.support.v4.view.ViewPager
android:id="@+id/mine_view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</android.support.design.widget.CoordinatorLayout>

属性设置

一开始我也有点晕,后来发现很简单,根据用户操作滚动的控件需要设置layout_behavior,需要根据用户滚动事件进行变化的控件放到AppBarLayout里,需要进行折叠的部分放到CollapsingToolbarLayout中,相应的触发场景需要配置app:layout_scrollFlags,定制折叠样式需要配置app:layout_collapseMode

这里CollapsingToolbarLayout设置了最小高度minHeightlayout_scrollFlags设置为scroll|exitUntilCollapsed,意味着“此布局和滚动时间关联,且在滚动到达这个最小高度的时候开始折叠“。

被折叠的控件设置了layout_collapseMode属性为parallax,意味着“当触发折叠时,此控件会有视差折叠效果”,layout_collapseParallaxMultiplier系数设置为0.5,可以自行调节。

对于在折叠过程中不希望被隐藏的控件,需要设置layout_collapseMode="pin",这样就会出现XTabLayout吸顶的效果了。

具体更详细的属性设置,可以参考下面的链接:https://www.jianshu.com/p/06c0ae8d9a96

设置监听事件

由于这里需要在工具栏完全折叠的时候在标题展示作者名字和关注按钮,还需要在代码里设置监听事件。

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
// 设置监听
mAppBarLayout.addOnOffsetChangedListener(new AppBarStateChangeListener() {
@Override
public void onStateChanged(AppBarLayout appBarLayout, State state) {
if (state == State.COLLAPSED) {
showTitleBarUserInfo(true);
} else {
showTitleBarUserInfo(false);
}
}
});

// 监听实现
public abstract class AppBarStateChangeListener implements AppBarLayout.OnOffsetChangedListener {

public enum State {
EXPANDED,
COLLAPSED,
IDLE
}

private State mCurrentState = State.IDLE;

/**
* Called when the {@link AppBarLayout}'s layout offset has been changed. This allows
* child views to implement custom behavior based on the offset (for instance pinning a
* view at a certain y value).
*
* @param appBarLayout the {@link AppBarLayout} which offset has changed
* @param verticalOffset the vertical offset for the parent {@link AppBarLayout}, in px
*/
@Override
public final void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
if (verticalOffset == 0) {
if (mCurrentState != State.EXPANDED) {
onStateChanged(appBarLayout, State.EXPANDED);
}
mCurrentState = State.EXPANDED;
} else if (Math.abs(verticalOffset) >= appBarLayout.getTotalScrollRange()) {
if (mCurrentState != State.COLLAPSED) {
onStateChanged(appBarLayout, State.COLLAPSED);
}
mCurrentState = State.COLLAPSED;
} else {
if (mCurrentState != State.IDLE) {
onStateChanged(appBarLayout, State.IDLE);
}
mCurrentState = State.IDLE;
}
}

public abstract void onStateChanged(AppBarLayout appBarLayout, State state);
}

这里监听的其实是AppBarLayout的onOffsetChanged事件,垂直偏移量verticalOffset改变时就会回调这个方法,当AppBarLayout展开时verticalOffset的值为0,折叠过程中verticalOffset为一个负值,其绝对值小于appBarLayout.getTotalScrollRange()

其他注意

  1. AppBarLayout的父类是LinearLayout,写布局时需要注意方向。

  2. CollapsingToolbarLayout的父类是FrameLayout,写布局时需要注意控件的添加顺序,避免折叠时控件显示层级的问题。

问题:Theme.AppCompat 错误

写完布局的时候,run的时候遇到了一个错误:“**The style on this component requires your app theme to be Theme.AppCompat (or a descendant)**”,有点奇怪。

大致错误如下:

1
2
3
4
5
6
Caused by: android.view.InflateException: Binary XML file line #100 in xxx_layout: Binary XML file line #100 in xxx_layout: Error inflating class com.google.android.material.XXX
Caused by: android.view.InflateException: Binary XML file line #100 in xxx_layout: Error inflating class com.google.android.material.XXX
Caused by: java.lang.reflect.InvocationTargetException
at java.lang.reflect.Constructor.newInstance0(Native Method)
...
Caused by: java.lang.IllegalArgumentException: The style on this component requires your app theme to be Theme.AppCompat (or a descendant).

是在初始化界面解析xml布局文件时出错了,错误大概的原因是,当前Activity用了Material Design控件,但是Activity的theme并不是Material Design。

解决方法

很简单,实现一个Theme.AppCompat的style就行了,具体颜色看情况即可。

1
2
3
4
5
6
7
<style name="WithAppBarTheme" parent="@android:style/Theme.Material.Light.NoActionBar">
...
<!--添加下面三个名称的颜色(颜色值随便),样式满足Theme.AppCompat-->
<item name="colorPrimary">#ffffff</item>
<item name="colorPrimaryDark">#000000</item>
<item name="colorAccent">#AAAAAA</item>
</style>

兼容问题:Vivo安卓7崩溃

在实际测试的过程中发现,在Vivo X9 7.1.2系统进入作者/个人页退出会出现系统的onResume崩溃,无法处理,错误堆栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
E/VivoSystemReflect: Failure register UserProfilingManager
java.lang.ClassNotFoundException: com.vivo.services.userprofiling.UserProfilingManager
at java.lang.Class.classForName(Native Method)
at java.lang.Class.forName(Class.java:400)
at java.lang.Class.forName(Class.java:326)
at android.app.VivoSystemReflect.getServiceConstructor(VivoSystemReflect.java:461)
at android.app.VivoSystemReflect.-wrap0(VivoSystemReflect.java)
at android.app.VivoSystemReflect$6.createService(VivoSystemReflect.java:446)
at android.app.SystemServiceRegistry$CachedServiceFetcher.getService(SystemServiceRegistry.java:858)
at android.app.SystemServiceRegistry.getSystemService(SystemServiceRegistry.java:799)
at android.app.ContextImpl.getSystemService(ContextImpl.java:1518)
at android.content.ContextWrapper.getSystemService(ContextWrapper.java:659)
at android.app.Application.getVivoUserProfilingManager(Application.java:327)
at android.app.Activity.onResume(Activity.java:1287)
at android.support.v4.app.FragmentActivity.onResume(FragmentActivity.java:514)
at com.qihoo.browser.activity.ActivityBase.onResume(ActivityBase.kt:117)
at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1276)
at android.app.Activity.performResume(Activity.java:6963)
at android.app.ActivityThread.performResumeActivity(ActivityThread.java:3469)

页面是可以进入使用的,功能也都正常,但是退出回到上一页面时就触发崩溃,很恼火。经过测试发现是WithAppBarTheme的原因,应该是Vivo 7系统做了什么魔改,导致兼容问题,测试了后面的几个版本都没有问题。

这个问题我暂时没有什么好办法处理,只能针对这一类别设备做了一个没有AppBarLayout的布局文件,改为Acitivity.onCreate()时动态设置theme,在AppBarLayout使用的地方使用非空判断!

1
2
3
4
5
6
7
8
9
10
11
12
public static boolean isEvilVivoDevice() {
return "vivo".equals(Build.MANUFACTURER) && Build.VERSION.RELEASE.startsWith("7.");
}

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
if (!AndroidUtil.isEvilVivoDevice())
setTheme(R.style.WithAppBarTheme); // 需要在super.onCreate之前设置Theme

super.onCreate(savedInstanceState);
// ...
}

注意,这里需要在super.onCreate之前设置Theme。

最后

这里简单记录了下如何使用CoordinatorLayoutAppBarLayout去实现折叠工具栏,第一次使用,还挺有意思。

其实,本来还打算写一个开发中遇到的“坑”,写了一半去分析源码时,越分析越不对劲,分析了两天,结果发现是自己的问题,感觉自己是个智障🤣🤣🤣🤣🤣🤣,不过分析的过程对CoordinatorLayout测量和绘制的流程理解更深刻了一点,下次单独再写吧。

最后的最后,兔年第一篇文章,祝大家都“💰🐰无量”~


以上,欢迎关注公众号“小黑杂说”。

小黑杂说


Android|CoordinatorLayout+AppBarLayout实现可折叠工具栏
https://wuruofan.com/2023/02/01/using-coordinatorlayout-and-appbarlayout-to-realize-a-collapsing-toolbar/
作者
rf.w
发布于
2023年2月1日
许可协议