你也许没见过 NestedScrollingParent 和NestedScrollingChild这两个接口,但你或多或少听过嵌套滑动。就像下图一样,顶部随着下滑出现,上滑隐藏。如果使用传统的事件分发来写的话,不仅复杂还容易出错。
而使用NestedScrollingParent 和NestedScrollingChild来实现的话就简单多了,虽然本质也是基于事件分发,但是谷歌爸爸已经帮我们都封装好了。
文章图片
那么 NestedScrollingParent 和NestedScrollingChild是怎么实现滑动的联动的呢。说到底,这两个只是个接口,还等待我们去实现。别担心,我们只需要定义两个view实现这两个接口,而两个接口的关联类已经有了。现在先来了解一下这两个接口。
既然有Parent和Child,我就可以理解为分别和父视图和子视图相关。具体来说,就是通过捕捉实现了NestedScrollingChild的view的事件,来通知实现了NestedScrollingParent 的view去做点什么事
我们先来看NestedScrollingChild
public interface NestedScrollingChild {
/**
* 设置是否允许嵌套滑动,允许的话设为true
*/
public void setNestedScrollingEnabled(boolean enabled);
/*
* 是否允许嵌套滑动
*/
public boolean isNestedScrollingEnabled();
/**
* 开始嵌套滑动
*/
public boolean startNestedScroll(int axes);
/**
* 结束嵌套滑动
*/
public void stopNestedScroll();
/**
* 判断NestedScrollingParent 的onStartNestedScroll方法是否返回true,只有true,才能继续一系列的嵌套滑动
*/
public boolean hasNestedScrollingParent();
/**
* 子view消费了拖动事件之后通知父view,
*/
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
/**
*子view消费了拖动事件之前通知父view,dx dy是将要消费的距离,如果父view要消费可通过
*设置consumed[0]=x .consumed[1]=y来分别消费x,y。然后子view继续处理剩下的位移(即dx-x,dy-y)
*/
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
/**
*子view消费了滑动事件之后通知父view,
*/
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
/**
*子view消费了滑动事件之前通知父view
*/
public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
再看看NestedScrollingParent
public interface NestedScrollingParent {
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
public void onStopNestedScroll(View target);
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed);
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
public int getNestedScrollAxes();
}
【NestedScrollingParent 和NestedScrollingChild 实现嵌套滑动】有没有发现两个接口很相似,只是把dispatch开头的换成了on开头。
一般来说on开头的方法代表的都是回调方法,我以startNestedScroll方法为例。当子view开始滑动时,会调用startNestedScroll方法,在该方法里面,主要实现就是获取当前的view的父view,如果父view实现了NestedScrollingParent 接口,就会回调onStartNestedScroll。然后在这些回调实现一些界面的变动,就有联动的效果了。
前面说了,两个视图的关联,谷歌已经帮我们实现好了。就是NestedScrollingChildHelper和NestedScrollingParentHelper。
NestedScrollingParentHelper的实现比较简单,就只是保存滑动的方向,事实上不使用它也没什么关系
public class NestedScrollingParentHelper {
private final ViewGroup mViewGroup;
private int mNestedScrollAxes;
/**
* Construct a new helper for a given ViewGroup
*/
public NestedScrollingParentHelper(ViewGroup viewGroup) {
mViewGroup = viewGroup;
}public void onNestedScrollAccepted(View child, View target, int axes) {
mNestedScrollAxes = axes;
}public int getNestedScrollAxes() {
return mNestedScrollAxes;
}public void onStopNestedScroll(View target) {
mNestedScrollAxes = 0;
}
}
而NestedScrollingChildHelper的作用就是负责关联两个view了
当子view开始滑动的使用,会调用NestedScrollingChildHelper的startNestedScroll方法,来启动嵌套滑动。
从下面的代码可以知道,NestedScrollingChild 的startNestedScroll会回调NestedScrollingParent的onStartNestedScroll
后续的几个方法基本也是这种调用模式
public boolean startNestedScroll(int axes) {
//验证一个滑动流程只可以startNestedScroll一次
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
//启动滑动,子view要调用setNestedScrollingEnabled方法启动。
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
//判断父view 是否实现NestedScrollingParent 且接口的onStartNestedScroll方法返回true
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
mNestedScrollingParent = p;
//调用NestedScrollingParent 的onNestedScrollAccepted回调方法
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
从两个接口和NestedScrollingChildHelper和NestedScrollingParentHelper,我发现,虽然NestedScrollingChildHelper和NestedScrollingParentHelper没有实现两个接口,但是定义的方法名字是一样的。这种代理模式不但便于理解,也更好的解耦
通过这四个东西,就可以实现一些联动效果了,NestedScrollingChild 的实现类有很多,比如recyclerview和nestscrollview。一般我们都是基于这些视图滚动进行关联。
下面我定义一个粉色条,根据scrollview的上下滚动而进行上下滑动。效果如下
文章图片
实现起来其实很简单,因为NestedScrollView实现了NestedScrollingChild接口,NestedScrollView里面又根据滑动情况已经处理好了NestedScrollingChildHelper的调用,在这里,我只要专注NestedScrollingParent 的实现就可以了
.support.v4.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="com.example.administrator.transdemo.ScrollingActivity"
tools:showIn="@layout/activity_scrolling"> .support.v4.widget.NestedScrollView>
ParentView 的实现如下
public class ParentView extends FrameLayout implements NestedScrollingParent2 {
public ParentView(@NonNull Context context) {
this(context, null);
}public ParentView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}public ParentView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}View imageRight;
@Override
protected void onFinishInflate() {
super.onFinishInflate();
imageRight= getChildAt(1);
}@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
//如果是竖直方向滑动,就启动嵌套滑动
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}@Override
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {}@Override
public void onStopNestedScroll(@NonNull View target, int type) {
}@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
//这里的Consumed代表NestScrollView消耗的距离, Unconsumed代表NestScrollView未消耗的距离
//imageRight根据NestScrollView滑动的距离而进行相应的滑动、。
imageRight.setTranslationY(imageRight.getTranslationY() + dyConsumed);
}@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed, int type) {}
}
只需要几行代码就实现了两个view的关联滑动。
下面如果我加多一个关联的view,代码如下
public class ParentView extends FrameLayout implements NestedScrollingParent2 {
public ParentView(@NonNull Context context) {
this(context, null);
}public ParentView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}public ParentView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}View imageLeft;
View imageRight;
@Override
protected void onFinishInflate() {
super.onFinishInflate();
imageLeft = getChildAt(1);
imageRight = getChildAt(2);
}@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}@Override
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {}@Override
public void onStopNestedScroll(@NonNull View target, int type) {}@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
imageRight.setTranslationY(imageRight.getTranslationY() + dyConsumed);
}@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed, int type) {
imageLeft.setTranslationY(imageLeft.getTranslationY() + dy);
}
}
文章图片
imageLeft表示左边的条块,imageRight表示右边的,滑动scrollview的时候,会怎么变化?
答案如下图
文章图片
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed, int type) {
//dx dy表示ontouEvent move的时候产生的原始偏移值,NestedScrollView处理它自己的滑动之前先调用这个方法。
//因为没有调用consumed[1] = xx,消耗掉相应的偏移值,所以他和右边滑块的速度是一样的。
//而NestedScrollView滑动到顶部的,继续上滑,ontouEvent move照样调用,该方法继续触发,下面的语句继续执行
//而imageRight 是跟随NestedScrollView偏移值进行相应的偏移,所以imageRight 不会动。
imageLeft.setTranslationY(imageLeft.getTranslationY() + dy);
}
上面我使用了NestedScrollingParent2 而不是NestedScrollingParent,其实NestedScrollingParent2 继承了NestedScrollingParent接口,只是在下面方法后面加多了一个参数 @NestedScrollType int type ,该参数有两张情况TYPE_TOUCH, TYPE_NON_TOUCH,表示当前状态是否在手指触摸下
boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes,
@NestedScrollType int type);
void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes,
@NestedScrollType int type);
void onStopNestedScroll(@NonNull View target, @NestedScrollType int type);
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type);
void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed,
@NestedScrollType int type);
NestedScrollingParent 和NestedScrollingChild应用无处不在。使用最多的就是CoordinatorLayout。
CoordinatorLayout + AppBarLayout实现的toolbar滑动就是使用这个原理实现的。当前CoordinatorLayout不止是有嵌套滑动这个效果。但是目前很多酷炫的滑动特效都是基于CoordinatorLayout 实现的
推荐阅读
- 从零开发一个完整的Android项目(九)——图片浏览
- Android开发|ViewPager自适应高度问题
- Android|Android 指定销毁一个Activity
- 【Android】简单图片浏览器
- 理解ButterKnife(自动生成绑定资源的代码)
- MAC下搭建Android Studio
- android用shape画一条横线
- 华为推送 的坑
- Duplicate class com.alipay.a.a.a found in modules classes.jar (:alipaySdk-15.6.2-20190416165036:) an