嵌套滑动-CoordinatorLayout
常常有这么一个产品交互需求,顶部是一个背景图,下面是一个列表,要求背景图跟随列表的滑动而消失,比如:
满足这个需求,最常见的空间组合就是CoordinatorLayout+RecyclerView
下面就来深度解析一下这两个控件
原理
- RecyclerView源码实现
public class RecyclerView extends ViewGroup implements NestedScrollingChild2{
}
- CoordinatorLayout源码实现
public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent2{
}
从上面两段代码可以看到,CoordinatorLayout+RecyclerView的嵌套滑动是利用NestedScrollingChild2和NestedScrollingParent2联动的特性,进行位置的移动。大概的流程是NestedScrollingChild2会告诉NestedScrollingParent我要滑动了,你要不要先滑动一些呢,然后NestedScrollingParent2再告诉NestedScrollingChild2,我先滑动了一部分,你滑动剩下的距离吧。这是一个通熟易懂的白话描述逻辑。下面会进行详细解析这两个控件
NestedScrollChild2
- NestedScrollChild
public interface NestedScrollingChild {
//设置嵌套滑动开关
void setNestedScrollingEnabled(boolean enabled);
//嵌套滑动开关
boolean isNestedScrollingEnabled();
//开始滑动,会去通知NestedScrollingParent,让父布局优先消费
boolean startNestedScroll(int axes);
//停止滑动
void stopNestedScroll();
//有嵌套滑动的父布局
boolean hasNestedScrollingParent();
//非手指滑动触发,主动调用scroll触发,会优先调用NestedScrollingParent.onNestedScroll触发嵌套父布局去滑动,
//父布局滑动完之后,剩下的滑动距离存储在offsetInWindow中,子view继续滑动剩下的
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow);
//手指滑动触发,会优先调用NestedScrollingParent.onNestedPreScroll触发嵌套父布局去滑动,父布局滑动完之后,已经消费的滑动距离存储在consumed中
boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow);
//猛的手势滑动
boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
//猛的手势滑动
boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
- NestedScrollChild2
//更丰富的滑动,就不继续详细分析了,主要多了手势滑动
public interface NestedScrollingChild2 extends NestedScrollingChild {
boolean startNestedScroll(int axes, int type);
void stopNestedScroll(int type);
boolean hasNestedScrollingParent(int type);
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type);
boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, int type);
}
NestedScrollingParent2
- NestedScrollingParent
public interface NestedScrollingParent {
//是否嵌套滑动,如果为false,onNestedScrollAccepted将不会被调用
boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int nestedScrollAxes);
//真正开始滑动,适合做一些初始化的动作
void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes);
//停止嵌套滑动
void onStopNestedScroll(@NonNull View target);
//消费子view非手动滑动的场景
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
//消费子view手动滑动场景,并且存储在consumed中
void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);
boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean var4);
boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY);
int getNestedScrollAxes();
}
- NestedScrollingParent2
public interface NestedScrollingParent2 extends NestedScrollingParent {
boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int nestedScrollAxes, int type);
void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int nestedScrollAxes, int type);
void onStopNestedScroll(@NonNull View target, int type);
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type);
void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type);
}
大家多注意看下NestedScrollingParent和NestedScrollChild的注释,大致流程基本都有说了,这里再梳理下流程图:
CoordinatorLayout+AppBarLayout+RecyclerView
经过上面对NestedScrollChild和NestedScrollingParent分析,其实我们对CoordinatorLayout+AppBarLayout+RecyclerView实现嵌套滑动的实现原理有了一个大概印象,现在准备将这个CoordinatorLayout+AppBarLayout+RecyclerView的细节再次梳理下,主要针对普通的滑动,不去追究猛滑等加速度场景,因为这些场景与普通滑动大同小异
- 引入
implementation "com.android.support:design:26.0.0"
- 写xml
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@drawable/bbb"
app:layout_scrollFlags="scroll|exitUntilCollapsed" />
</android.support.design.widget.AppBarLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView11"
android:layout_width="match_parent"
android:layout_height="match_parent">
</android.support.v7.widget.RecyclerView>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
上面是使用这个嵌套布局功能所需要的步骤,其中AppBarLayout是包裹要嵌套滑动的头部
核心的流程上面已经讲解清楚,大家最好拿着源代码看这个流程,注意点就是父布局的移动采用的View.offsetTopAndBottom方法,top为负值代表滚出屏幕。可以添加监听器来监听变化
mAppBarLayout.addOnOffsetChangedListener(this);
总结
这是android很常用的一个控件,为了让自己能应付各种复杂的业务,掌握原理还是很有必要的,希望这篇文章可以帮助你们