1.CoordinatorLayout依赖库
旧版本导入CoordinatorLayout依赖
implementation 'com.android.support:design:28.0.0'
升级Android X后的依赖
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.0.0'
2.NestedScrolling机制介绍
NestedScrolling提供了一套父View和子View滑动交互机制;要完成这样的交互,父View需要实现NestedScrollingParent接口,而子View要实现NestedScrollingChild接口,系统提供了NestedScrollingView控件就实现了这两个接口;
public class NestedScrollView extends FrameLayout implements NestedScrollingParent3,
NestedScrollingChild3, ScrollingView {}
NestedScrolling(嵌套滑动),就是子View和父View在滑动过程中,互相通信决定某个滑动是子View处理合适,还是父View处理合适;所以Parent和Child之间存在相互调用,NestedScrollView遵循下面的调用关系:
上图可以这么理解:
- ACTION_DOWN的时候子View就要调用startNestedScrolling()方法来告诉父View自己要开始滑动了(实质上是寻找能够配合child进行嵌套滑动的parent),parent也会继续向上寻找能够配合自己滑动的parent,可以理解为在做一些准备工作;(确认是否处理这次滑动事件)
- 父View会收到onStartNestedScroll回调从而决定是不是要配合子view做出响应;如果需要配合此方法返回true;继而onNestedScrollAccepted()回调会被调用;
- 在滑动事件产生但是子View还没有处理前可以调用;dispatchNestedPreScroll(0,dy,consumed,offsetInWindow)这个方法把事件传给父View,这样父View就能在onNestedPreScroll()方法里面接收到子View的滑动信息,把处理完后的然后做出相应的处理结果通过consumed传递给子View;
- dispatchNestedPreScroll()之后,child可以进行自己的滚动操作;
- 如果父View需要在子View滑动后处理相关事件的话可以在子View的事件处理完成之后调用dispatchNestedScroll()然后父View会在onNestedScroll()收到回调;
- 最后,滑动结束,调用onStopNestedScroll()表示本次处理结束;
- 但是,如果滑动速度比较大,会触发fling,fling也分为preFling和fling连个阶段,处理过程和scroll基本差不多
NestedScrollingParent和NestedScrollingChild都提供了对应非辅助类,NestedScrollingChildHelper和NestedScrollingParentHelper辅助类,辅助类会负责处理大部分逻辑,他们之间的调用关系如下:
2.1NestedScrolling机制关键类说明
NestedScrollingChild/2/3:此接口定义NestedScrolling各状态监听回调方法,需要子View去实现NestedScrollingChild接口,然后借助NestedScrollingChildHelper辅助类实现NestedScrolling各状态派发给父视图处理;
NestedScrollingChildHelper:实现了Child和Parent的交互逻辑,将NestedScrolling操作先交给Parent处理;
NestedScrollingParent/2/3:此接口定义接收子View的NestedScrolling各状态监听回调方法,需要父ViewGroup实现NestedScrollingParent接口,然后借助NestedScrollingParentHelper辅助类实现NestedScrolling各状态派处理;
NestedScrollingParentHelper:帮助Parent实现和Child交互的逻辑;滑动动作是Child的主动发起,Parent就受滑动回调并作出响应;
2.2实现NestedScrollingChild接口
为什么是NestedScrollingChild接口?
NestedScrollingChild定义一套嵌套滑动事件标准方法(从开始startNestedScroll到结束stopNestedScroll),提供给子View实现,子View负责实现NestedScrollingChild的方法,根据事件的动作类型触发嵌套滑动事件标准方法,最终会调用实现了NestedScrollingParent父ViewGroup处理; 简单理解就是我告诉父ViewGroup我要滑动了,你可以先做滑动处理,然后我在处理;
实际上NestedScrollingChildHelper辅助类已经实现好了Child和Parent交互,原来View的处理逻辑滑动事件逻辑上大体不需要改变;需要做的就是,如果准备开始滑动了,需要告诉Parent,Child要准备进入滑动状态了,调用startNestedScroll();Child在滑动之前,先问一个你的Parent是否需要滑动,也就是调用dispatchNestedPreScroll;如果父类消耗了部分滑动事件,Child需要重新计算一下父类消耗后剩余给Child的滑动距离余量;然后,Child自己进行余下的滑动;最后,如果滑动距离还有剩余,Child就再问一下,Parent是否需要在继续滑动你剩下的距离,也就是调用dispatchNestedScroll(), 大概就是这么一回事,当然还会有和Scroll类型的Fling系列方法,但我们这里可以先忽略一下;
NestedScrollView的NestedScrollingChild接口实现都是交给辅助类NestedScrollingChildHelper来处理的,是否需要进行额外的一些操作要根据实际情况来定;
//NestedScrollChild
public NestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs,
int defStyleAttr) {
//...
mParentHelper = new NestedScrollingParentHelper(this);
mChildHelper = new NestedScrollingChildHelper(this);
// ...because why else would you be using this widget?
setNestedScrollingEnabled(true);
}
@Override
public void setNestedScrollingEnabled(boolean enabled) {
mChildHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return mChildHelper.isNestedScrollingEnabled();
}
//在初始化滚动的时候操作,一般在MotionEvent#ACTION_DOWN的时候调用
@Override
public boolean startNestedScroll(int axes) {
return startNestedScroll(axes, ViewCompat.TYPE_TOUCH);
}
@Override
public void stopNestedScroll() {
stopNestedScroll(ViewCompat.TYPE_TOUCH);
}
@Override
public boolean hasNestedScrollingParent() {
return hasNestedScrollingParent(ViewCompat.TYPE_TOUCH);
}
//参数和dispatchNestedPreScroll方法的返回有关联
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
offsetInWindow);
}
//在消费滚动事件之前调用,提供一个让ViewParent实现联合滚动的机会,因此ViewParent可以消费一部分或者全部的滑动事件,参数consumed会记录ViewParent所消费掉的事件
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, ViewCompat.TYPE_TOUCH);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
实现NestedScrollingChild接口挺简单的不是吗?但还需要我们决定什么时候进行调用,和调用哪些方法;
startNestedScroll和stopNestedScroll的调用
startNestedScroll配合stopNestedScroll使用,startNestedScroll会再接收到ACTION_DOWN的时候调用,stopNestedScroll会在接收到ACTION_UP|ACTION_CANCEL的时候调用,NestedScrollView中的伪代码是这样
onInterceptTouchEvent | onTouchEvent(MotionEvent ev){
case MotionEvent.ACTION_DOWN:
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
break;
case MotionEvent.ACTION_UP | ACTION_CANCEL:
stopNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
break;
}
NestedScrollingChildHelper处理startNestedScroll方法,可以看出会调用Parent的onStartNestedScroll和onStartNestedScrollAccepted方法,只要Parent愿意优先处理这次的滑动事件,在结束的时候Parent还会收到onStopNestedScroll回调;
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
if (hasNestedScrollingParent(type)) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
setNestedScrollingParentForType(type, p);
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
public void stopNestedScroll(@NestedScrollType int type) {
ViewParent parent = getNestedScrollingParentForType(type);