NestedScrollingParent与NestedScrollingChild
1、 嵌套滑动的解决方案先看注释了解方法
这时Google官方给的处理方案,在Androidx或者support包中
public interface NestedScrollingChild {
//设置是否允许嵌套滑动,允许的话设为true
void setNestedScrollingEnabled(boolean enabled);
//是否允许嵌套滑动
boolean isNestedScrollingEnabled();
//开始嵌套滑动 axes:滑动方向,一般用于通知父控件我要看是滑动了
boolean startNestedScroll(@ScrollAxis int axes);
//停止嵌套滑动,一般也用于通知父控件
void stopNestedScroll();
// 判断父控件NestedScrollingParent 的onStartNestedScroll方法是否返回true,
//只有true,才能继续一系列的嵌套滑动
boolean hasNestedScrollingParent();
/**
* 子控件消费了一部分滑动之后通知父控件滑动
* @param dxConsumed 子控件X方向已消耗长度
* @param dyConsumed 子控件Y方向已消耗长度
* @param dxUnconsumed 子控件X方向未消耗长度
* @param dyUnconsumed 子控件Y方向未消耗长度
* @param offsetInWindow 控件在window上的偏移
* @return 数据是否可以分发
*/
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow);
/**
* 子控件滑动之前通知父控件滑动
* @param dx x方向上触摸滑动距离
* @param dy y方向上触摸滑动距离
* @param consumed 父控件滑动后未消耗的距离,是父控件返回的值,不是子控件的值
* consumed[0]-X consumed[1]为Y
* @param offsetInWindow 控件在window上的偏移
* @return 数据是否可以分发
*/
boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow);
//子控件消耗Fling滑动后通知父控件,velocityX每秒滑动速度,
boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
//子控件消耗Fling滑动之前通知父控件fling滑动
boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
public interface NestedScrollingParent {
//子控件开始嵌套滑动通知了父控件,这个方法接收调用,父控件决定是否嵌套滑动 @return 是否嵌套滑动
boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes);
//子控件开始嵌套滑动通知了父控件,这个方法也会接收调用,
void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes);
//子控件停止嵌套滑动通知父控件
void onStopNestedScroll(@NonNull View target);
//子控件调用 dispatchNestedScroll 父控件这个方法接收调用
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed);
//子控件调用 dispatchNestedPreScroll 父控件这个方法接收调用 consumed需要父控件赋值
void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);
//子控件调用 dispatchNestedFling 父控件这个方法接收调用,惯性滑动
boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed);
//子控件调用 dispatchNestedPreFling 父控件这个方法接收调用,惯性滑动
boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY);
//获取当前父控件的滑动方向
int getNestedScrollAxes();
}
了解Android事件分发的可以思考一下,事件分发是父控件分发给子控件,父控件也是dispath方法分发,子控件on方法接收,这里其实就是反过来,子控件dispath分发,父控件接收处理,处理结果又返回给子控件。
2、嵌套滑动调用流程
子view | 父View |
---|---|
startNestedScroll | onStartNestedScroll、onNestedScrollAccepted |
dispatchNestedPreScroll | onNestedPreScroll |
dispatchNestedScroll | onNestedScroll |
stopNestedScroll | onStopNestedScroll |
子控件开始滑动调用startNestedScroll通知父控件我要看是滑动了,父控件就会被调用onStartNestedScroll、onNestedScrollAccepted,并返回给子控件我要不要滑动,如果父控件不滑动到此结束,子控件自己滑动就行了,
父控件需要滑动,则子控件开始决定怎样配合滑动,
- 子控件先滑动父控件后滑动,子控件滑动后,调用dispatchNestedScroll,父控件onNestedScroll被调用,如果有父控件没消费的一部分滑动由参数dxUnconsumed返回给子控件继续滑动,
- 父控件先滑动子控件后滑动,子控件先调用dispatchNestedPreScroll,父控件则onNestedPreScroll被调用,父控件滑动后将没有消耗的滑动赋值给consumed,交给子控件,子控件开始滑动,
3:dispatchNestedFling与dispatchNestedPreFling一样逻辑
3、示例解析
3.1 自定义NestedScrollChildLayout和NestedScrollParentLayout子父控件
只要实现NestedScrollingChild 和 NestedScrollingParent接口即可,实现所有方法。
3.2 NestedScrollingChildHelper与NestedScrollingParentHelper
通过名字可以看出这是子控件和父控件嵌套滑动各个控件的帮助类,他们的方法几乎都对应着两个嵌套滑动接口。在androidx或support包中。
- NestedScrollingChildHelper: 子控件调用,寻找父控件并调用父控件中的NestedScrollingParent方法。
- NestedScrollingParentHelper:父控件调用,寻找子控件调用子控件的NestedScrollingChild 方法。
3.3 子控件处理触摸滑动事件实现嵌套滑动
触摸滑动处理方法 handleMoveY(int distanceY), 这里只处理上下滑动
private void handleMoveY(int distanceY){
//父控件是否允许滑动
if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)) {
if (distanceY > 0) {
//向上滑, 父控件先滑动子控件再滑动
if (dispatchNestedPreScroll(0, distanceY, consumed, offset)) {
//consumed为父控件消耗的距离, 未消耗的子控件继续滑动
int unConsumed = distanceY - consumed[1];
if (unConsumed != 0) {
scrollBy(0, unConsumed);
}
} else {
scrollBy(0, distanceY);
}
} else {
//向下滑, 子控件先滑动父控件后滑动
if (getScrollY() >= 0) {
if (getScrollY() == 0) {
//子控件已不再需要滑动,父控件滑动
dispatchNestedScroll(0, 0, 0, distanceY, offset);
return ;
}
//子控件可以消耗所有滑动,先滑动自己
if (getScrollY() + distanceY >= 0) {
scrollBy(0, distanceY);
} else if (getScrollY() != 0) {
//子控件滑动一部分,剩余给父控件滑动
int consume = getScrollY();
int unConsumed = (int) distanceY + consume;
scrollTo(0, -consume); //先自己滑动
dispatchNestedScroll(0, consume, 0, unConsumed, offset);
} else {
dispatchNestedScroll(0, 0, 0, distanceY, offset);
}
} else {
scrollTo(0, 0);
}
}
}
}
由上面注释可以看出,嵌套滑动完全按照 2、嵌套滑动调用流程来完成,
- 向上滑,父控件先消耗一定的滑动距离,子控件滑动剩余距离。父控件则隐藏第一个控件。
- 向下滑,子控件先滑动到顶部,之后将剩余的滑动距离或后续的滑动分发给父控件。父控件显示出第一个控件。
3.4 父控件处理子控件分发的滑动
父控件需要的滑动是隐藏或显示第一个控件,则父控件可滑动的距离就是第一个控件的高度
//向下滑动,子控件已滑动顶部,分发剩余滑动与后续的滑动,父控件开始滑入第一个控件。
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
Log.i(TAG, "onNestedScroll: dyConsumed=" + dxConsumed + " dyUnconsumed=" + dyUnconsumed);
//父控件后滑动为显示上部控件
if (getScrollY() > 0 && dyUnconsumed < 0) {
if (getScrollY() + dyUnconsumed >0) {
scrollBy(0, dyUnconsumed);
} else {
scrollTo(0, 0);
}
}
}
//向上滑动,父控件先滑动,将已消耗的滑动距离传给子控件,子控件继续滑动,实现父控件第一个控件滑出屏幕。
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
super.onNestedPreScroll(target, dx, dy, consumed);
Log.i(TAG, "onNestedPreScroll: dx=" + dx + " dy=" + dy);
View view = getChildAt(0);
//父控件先滑动为隐藏上部控件,
if (getScrollY() < view.getHeight() && dy > 0) {
if (getScrollY() + dy <= view.getHeight()) {
scrollBy(0, dy);
consumed[1] = dy;
} else {
consumed[1] = view.getHeight() - getScrollY();
scrollTo(0, view.getHeight());
}
}
}
4. 接口系统化
在Android api 21之后,嵌套滑动的的接口已经系统化,Android原生系统view与ViewGroup就自带了嵌套滑动的所有接口方法,