Android 开发项目实战:实现折叠式布局,2021 年是做 Android 开发人员的绝佳时机
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="新闻详情" />
</android.support.v7.widget.Toolbar>
</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"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="hello world" />
...
...
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
以上就是实现一个折叠式布局的典型模板布局代码,一个简简单单的布局就实现了这样的效果,但是必须要注意在 AndroidMnifest.xml 必须要给 Activity 指定它的 theme 为 NoActionBar 的样式代码如下:
<activity
android:name=".test.CoordinatorLayoutTestActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
否则会出现 ActionBar 和 ToolBar 共存的情况,的显示效果如下:
另外还需要把自己自定义的 ToolBar 告诉给系统,即第 9 行的 setSupportActionBar(toolbar),否则我们的 ToolBar 会作为一个普通的 View 而存在
public class CoordinatorLayoutTestActivity extends AppCompatActivity {
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_coordinator_layout_test);
Toolbar toolbar = findViewById(R.id.view_toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayShowTitleEnabled(false);
toolbar.setNavigationIcon(R.mipmap.callback_white_icon);
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onBackPressed();
}
});
}
}
如果只指定了 setSupportActionBar(toolbar),没有 AndroidMnifest.xml 在指定 Activity 的 theme 为 NoActionBar,那就运行就直接崩溃了,会报错如下:
Caused by: java.lang.IllegalStateException: This Activity already has an action bar supplied by the window decor. Do not request Window.FEATURE_SUPPORT_ACTION_BAR and set windowActionBar to false in your theme to use a Toolbar instead.
at android.support.v7.app.AppCompatDelegateImpl.setSupportActionBar(AppCompatDelegateImpl.java:345)
at android.support.v7.app.AppCompatActivity.setSupportActionBar(AppCompatActivity.java:130)
意思是说 Activity 已经有一个 ActionBar 了,请在你的样式中使用 ToolBar 替代
在上面的布局文件代码中,根布局 CoordinatorLayout 就是用来协调 AppBarLayout 和 NestedScrollView 之间滚动的,40 行的 NestedScrollView 是我们要滚动的内容,在 11 行的 CollapsingToolbarLayout 标签的内部就是要折叠的内容
其中 43 行的 app:layout_behavior 不配置的效果:
NestedScrollView 的内容在 ToolBar 之上滚动
其中 13 行 app:layout_scrollFlags="scroll|exitUntilCollapsed"如果不配置效果图如下:
如果没有配置则 CollapsingToolbarLayout 包裹内容内容就会固定在顶部,不会滚动
28 行 app:layout_collapseMode="pin"不配置,效果图:
ToolBar 会跟随 NestedScrollView 的滚动而滚动,而不会固定在布局顶部位置
14 行 app:titleEnabled="false"不配置,效果图:
即使 33 行的 TextView 配置了 android:layout_gravity=“center”,title 也不会居中显示
我们感觉折叠式布局就是给我们的 View 设置相关的属性配置,不需要进行任何编码就能完成我们的折叠效果,我们不得的赞叹 android 5.0 给我们提供这一强大的功能
我们来总结一下:
CoordinatorLayout+AppBarLayout+CollapsingToolbarLayout 结合起来才能产生这么神奇的效果,不要幻想使用其中的一个控件就能完成这样的效果
[](
)ToolBar 的设置
系统默认使用的就是系统自带的 ActionBar,如果我们要使用自定义的 ToolBar,就必须明确的告诉 Activity 不需要使用系统自带的 ActionBar 即要给 activity 设置 NoActionBar 的样式,另外必须调用 setSupportActionBar(toolbar)将自己定义的 ToolBar 设置给 Activity。
[](
)CoordinatorLayout 下可滑动控件的设置
CoordinatorLayout 作为整个布局的父布局容器。给你的可以滑动的控件例如 RecyclerView 设置如下属性:app:layout_behavior=@string/appbar_scrolling_view_behavior
CoordinatorLayout 还提供了 layout_anchor 和 layout_anchorGravity 属性一起配合使用,可以用于设置 FloatingActionBu
tton 的位置,此处我是放在 appBar 的右下角。
app:layout_anchor="@id/appbar"
app:layout_anchorGravity=“bottom|right|end”
[](
)CollapsingToolbarLayout 的 layout_scrollFlags 属性
AppBarLayout 里面定义的子 view 只要设置了 app:layout_scrollFlags 属性,就可以在 RecyclerView 滚动事件发生的时候被触发某种行为
例如我给 CollapsingToolbarLayout 控件设置了 app:layout_scrollFlags="scroll|exitUntilCollapsed"此刻如果没有这个属性,CollapsingToolbarLayout 是不会折叠的那么问题来了,layout_scrollFlags 中的属性值除了可以触发折叠的行为,还有其它的属性值吗?并且各个属性的意义是什么?scroll 至少有一个 scroll,即可滚动。
| 属性 | 作用 |
| --- | --- |
| scroll | 必须要给其至少有设置一个 scroll,即可滚动 |
| enterAlways | 向下滚动即可见。例如下拉时,立即显示 Toolbar |
| exitUntilCollapsed | 这个 flag 是定义何时收缩。当你定义了一个 minHeight,这个 view 将在滚动到达这个最小高度的时候消失 |
| enterAlwaysCollapsed | 这个 flag 是定义何时展开。当你定义了一个最小高度 minHeight, 同时 enterAlways 也定义了,那么 view 将在到达这个最小高度的时候开始展示 |
| snap | 当一个滚动事件结束,它将根据显示百分比的大小自动滚动到收缩或展开。 |
如果不设置该属性,则该布局不能滑动
[](
)CollapsingToolbarLayout 的其他属性
另外还可以给 CollapsingToolbarLayout 设置以下属性:
| 属性 | 作用 |
| --- | --- |
| contentScrim | 设置当完全折叠(收缩)后的背景颜色。 |
| expandedTitleMarginEnd | 没有扩张的时候标题显示的位置 |
| expandedTitleMarginStart | 扩张的时候标题向左填充的距离。 |
| statusBarScrim | 设置折叠时状态栏的颜色 |
[](
)CollapsingToolbarLayout 下的 view 的 layout_collapseMode 属性
CollapsingToolbarLayout 里面定义的 view 只要设置了 app:layout_collapseMode 属性,就可以控制子视图的折叠模式。
折叠模式分为两种:
| 属性 | 作用 |
| --- | --- |
| pin | 固定模式。在收缩的时候最后固定在顶端(例如向上滚动的时候就固定 toolBar) |
| parallax | 视差模式,在折叠的时候会有个视差折叠的效果。(例如向下滚动的时候就展开 ImageView) |
[](
)CoordinatorLayout 的 fitsSystemWindows 属性
fitsSystemWindows 属性可以让 view 根据系统窗口来调整自己的布局,简单点说就是我们在设置应用布局时是否考虑系统窗口布局,这里系统窗口包括系统状态栏、导航栏、输入法等,包括一些手机系统带有的底部虚拟按键。android:fitsSystemWindows=”true” (触发 View 的 padding 属性来给系统窗口留出空间) 这个属性可以给任何 view 设置,只要设置了这个属性此 view 的其他所有 padding 属性失效,同时该属性的生效条件是只有在设置了透明状态栏(StatusBar)或者导航栏(NavigationBar)此属性才会生效
[](
)如何监听 CollapsingToolbarLayout 的展开与折叠
使用官方提供的 AppBarLayout.OnOffsetChangedListener 就能实现了,不过要封装一下才好用,自定义一个继承了 AppBarLayout.OnOffsetChangedListener 的类这里命名为 AppBarStateChangeListener
public abstract class AppBarStateChangeListener implements AppBarLayout.OnOffsetChangedListener {
public enum State {
EXPANDED,
COLLAPSED,
IDLE
}
private State mCurrentState = State.IDLE;
@Override
public final void onOffsetChanged(AppBarLayout appBarLayout, int i) {
if (i == 0) {
if (mCurrentState != State.EXPANDED) {
onStateChanged(appBarLayout, State.EXPANDED);
}
mCurrentState = State.EXPANDED;
} else if (Math.abs(i) >= 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);
}
然后我们这样使用它:
mAppBarLayout.addOnOffsetChangedListener(new AppBarStateChangeListener() {
@Override
public void onStateChanged(AppBarLayout appBarLayout, State state) {
Log.d("STATE", state.name());
if( state == State.EXPANDED ) {
//展开状态
}else if(state == State.COLLAPSED){
//折叠状态
}else {
//中间状态
}
}
});
这样就可以在不同的状态下根据自己的业务需求去实现相关的逻辑了
[](
)StickyLayout 自定折叠式布局的实现
好了,上面就是关于通过 CoordinateLayout 实现的折叠式布局所有的知识点,如果说前面只是开胃菜,现在我们就开始上主菜了,我们能不能自己实现这样一个折叠式的布局,利用上一篇我们所讲的头部固定的 ExpandedListView,把它作为具有滑动功能的主 View,在它的顶部添加具有背景图片 Header,随着 ExpandedListView 的滑动 header 实现扩展和收缩的效果,效果如下:
[](
)功能分析
其实这个效果图在文章的一开始就展示过了,整个布局分为上下两部分:上分部分为可折叠的 Header,下半部分就是我们头部固定的 ExpandedListView,他们公共父 view 就是今天我们要实现的折叠式布局 StickyLayout,ExpandedListView 是自身所具备滑动功能的,而我们在整个屏幕上,往上滑动的时候如果 header 处于展开状态则 Header 慢慢的要折叠起来,往下滑动的时候如果 ExpandedListView 顶部数据都显示出来的情况下再往下拉的时候 Header 就慢慢的展开,其他的状态就是我们的 ExpandedListView 在上下滑动,也就是说我们的 Header 在折叠和展开的状态下的这些事件被 StickyLayout 拦截了,其他的事件就交给 ExpandedListView 进行处理从而实现了他的上下滑动,这就属于典型的滑动冲突问题,简言之就是我们在上下滑动的过程中的有些事件需要被 StickyLayout 拦截消掉来实现 Header 的折叠和展开效果,其他的事件就交给 ExpandedListView 来实现它的滑动效果
现在我们要思考的是哪些情况下被拦截:
左右滑动的不需要处理,只处理上下滑动的事件
在展开的状态下,上滑事件需要拦截
ExpandedListView 的第 0 个元素处于可见状态,此时的下滑事件需要拦截
[](
)在事件拦截方法中处理滑动冲突
public class StickyLayout extends LinearLayout {
...
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
mLastInterceptX = x;
mLastInterceptY = y;
intercept = false;
break;
case MotionEvent.ACTION_MOVE:
int dx = x - mLastInterceptX;
int dy = y - mLastInterceptY;
if(y <= mCurrHeaderHeight){
intercept = false;
评论