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属性一起配合使用,可以用于设置FloatingActionButton的位置,此处我是放在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实现扩展和收缩的效果,效果如下:

Android 折叠屏折叠监听 安卓 折叠_sed

功能分析

其实这个效果图在文章的一开始就展示过了,整个布局分为上下两部分:上分部分为可折叠的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;
}else if(Math.abs(dx) > Math.abs(dy)){
intercept = false;
}else if(mState == mStateExpand && dy <= - mScaledTouchSlop){
//上滑
intercept = true;
}else if(mGiveUpTouchEventListener.giveUpTouchEvent() && dy > mScaledTouchSlop){
//下滑
intercept = true;
}else{
intercept = false;
}
break;
case MotionEvent.ACTION_UP:
mLastInterceptX = 0;
mLastInterceptY = 0;
intercept = false;
break;
}
return intercept;
}
…
}

上面就是关于事件拦截的核心代码,首先我们看17行:y <= mCurrHeaderHeight 如果触摸事件是在Header之上也就不拦截了,再看19行Math.abs(dx) > Math.abs(dy),如果是横向滑动也不是我们所需要的事件也不拦截,否则上就是上下滑动的事件了,在这个状态下状态Header处于展开状态且是上滑那就需要拦截处理,也就是21行:mState == mStateExpand && dy <= - mScaledTouchSlop所处理的逻辑,在看24行:mGiveUpTouchEventListener.giveUpTouchEvent() && dy > mScaledTouchSlop,giveUpTouchEvent方法表示如果ExpandedListView的第一个可见元素是0且dy > mScaledTouchSlop(表示是上滑)此时的事件也是需要拦截的

View滑动距离常量TouchSlop

在21行细心的同学可能会看到这么一句dy <= - mScaledTouchSlop,dy指的是滑动的距离,mScaledTouchSlop到底是什么?其实他是Android系统给我们提供的View滑动最小距离常量TouchSlop,也就是说两个Move事件之间的滑动距离如果小于这个常量就系统不认为他是滑动,因为滑动距离太短,反之就认为它是滑动,这个常量值和设备有关,不同的设置上这个值可能是不相同的,我们可以通过如下方式即可获取这个常量:

int mScaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();

折叠展开的事件消费

上面的17到29行就是处理事件拦截的核心处理逻辑,事件拦截完毕,事件就交给TouchEvent方法进行消费了,下面看看Header到底具体是怎么折叠的?其实很简单就是不用重置Header的height就OK了,我们看看代码:

public class StickyLayout extends LinearLayout {
…
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
int dx = x - mLastX;
int dy = y - mLastY;
mCurrHeaderHeight += dy;
setHeaderHeight(mCurrHeaderHeight);
break;
case MotionEvent.ACTION_UP:
int dest = 0;
if(mCurrHeaderHeight <= mOriginHeaderHeight * 0.5){
dest = 0;
mState = mStateCollapsed;
}else{
dest = mOriginHeaderHeight;
mState = mStateExpand;
}
smoothSetHeaderHeight(mCurrHeaderHeight,dest,500);
break;
}
mLastX = x;
mLastY = y;
return super.onTouchEvent(event);
}
…
}

其中12行到13行就是手指拖动状态下的核心逻辑 ,12行计算两次Move事件所移动的距离,13行根据手指滑动的距离来计算Header当前的高度,计算完毕就可以调用setHeaderHeight设置Header的高了

设置Header的高来实现折叠效果

private void setHeaderHeight(int height) {
if(height <= 0){
height = 0;
}else if(height >= mOriginHeaderHeight){
height = mOriginHeaderHeight;
}
if(height == 0 ){
mState = mStateCollapsed;
}else{
mState = mStateExpand;
}
headerView.getLayoutParams().height = height;
headerView.requestLayout();
}

其中第2行到第6行对Header高度的越界处理,第7行到11行是设置Header的状态,第12行到13行给Header的高赋值并刷新Header来变它的位置与大小

手指抬起的自动回弹折叠展开效果

如果当前Header的高小于原始高度的一半手指抬起的时候Header进行收缩,反之就进行展开操作,核心代码在上面的onTouchEvent(MotionEvent event)方法的的17行到25行:

int dest = 0;
if(mCurrHeaderHeight <= mOriginHeaderHeight * 0.5){
dest = 0;
mState = mStateCollapsed;
}else{
dest = mOriginHeaderHeight;
mState = mStateExpand;
}
smoothSetHeaderHeight(mCurrHeaderHeight,dest,500);