NestedScrolling是嵌套滑动机制(Nested是嵌套的意思),为了完成父和子之间优雅的滑动效果而引出的机制。
如图所示,我们将要实现的效果如下:
NestedScrolling机制能够让父View和子View在滚动时进行优雅的衔接,为了完成这个效果,Android提供了两个接口:
NestedScrollingChild
NestedScrollingParent
以及两个辅助类
NestedScrollingChildHelper
NestedScrollingParentHelper
父View实现NestedScrollingParent接口,而子View实现NestedScrollingChild接口,并且子View是发起者,父View是接收者。
其布局分配图如下:
上图标注了四块位置:
[第一块]
是父view,父view必须实现NestedScrollingParent接口
[第二块]
是父view的第一个子view,ImageView
[第三块]
是父view的第二个子view,TextView
[第四块]
是父view的第三个子view,比较特殊,它实现了NestedScrollingChild接口,滑动这个View就可以实现嵌套布局的滚动效果
想要实现这种效果,父view需要实现NestedScrollingParent接口,子view需要实现NestedScrollingChild接口,那么开始说明一下这两个接口吧。
NestedScrollingChild接口如下
public interface NestedScrollingChild {
void setNestedScrollingEnabled(boolean var1);
boolean isNestedScrollingEnabled();
boolean startNestedScroll(int var1);
void stopNestedScroll();
boolean hasNestedScrollingParent();
boolean dispatchNestedScroll(int var1, int var2, int var3, int var4, @Nullable int[] var5);
boolean dispatchNestedPreScroll(int var1, int var2, @Nullable int[] var3, @Nullable int[] var4);
boolean dispatchNestedFling(float var1, float var2, boolean var3);
boolean dispatchNestedPreFling(float var1, float var2);
}
- setNestedScrollingEnabled:设置允许滑动还是禁止滑动
- isNestedScrollingEnabled:获取是否可以滑动
- startNestedScroll:开始滑动,滑动实现NestedScrollingChild接口的view时触发这个方法,并且寻找是否含有已实现NestedScrollingParent接口的父view
var1:滑动的方向ViewCompat.SCROLL_AXIS_VERTICAL
或ViewCompat.SCROLL_AXIS_HORIZONTAL
- stopNestedScroll:停止滑动,并清除滑动状态
- hasNestedScrollingParent:获取是否含有已实现NestedScrollingParent接口的父view
- dispatchNestedPreScroll:在子view自己进行滚动之前调用此方法,询问父view是否要在子view之前进行滚动。此方法的前两个参数用于告诉父View此次要滚动的距离;而第三第四个参数用于子view获取父view消费掉的距离和父view位置的偏移量。
- dispatchNestedScroll:在子view自己进行滚动之后调用此方法,询问父view是否还要进行余下(unconsumed)的滚动。前四个参数为输入参数,用于告诉父view已经消费和尚未消费的距离,最后一个参数为输出参数,用于子view获取父view位置的偏移量。如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。
NestedScrollingParent接口如下
public interface NestedScrollingParent {
boolean onStartNestedScroll(@NonNull View var1, @NonNull View var2, int var3);
void onNestedScrollAccepted(@NonNull View var1, @NonNull View var2, int var3);
void onStopNestedScroll(@NonNull View var1);
void onNestedScroll(@NonNull View var1, int var2, int var3, int var4, int var5);
void onNestedPreScroll(@NonNull View var1, int var2, int var3, @NonNull int[] var4);
boolean onNestedFling(@NonNull View var1, float var2, float var3, boolean var4);
boolean onNestedPreFling(@NonNull View var1, float var2, float var3);
int getNestedScrollAxes();
}
- onStartNestedScroll:当执行子view的
startNestedScroll
方法时执行 - onStopNestedScroll:停止滚动
- getNestedScrollAxes:获取滚动的方向
- onNestedPreScroll:当执行子view的
dispatchNestedPreScroll
方法时执行,前两个参数是子view传递给父view的移动距离,最后一个参数需要父view给它复制,然后传递给子view,告诉子view父view当前消费的距离
下面开始结合辅助类NestedScrollingChildHelper
和NestedScrollingParentHelper
自定义父view和子view
MyCustomNestedScrollingChild.java
public class MyCustomNestedScrollingChild extends LinearLayout implements NestedScrollingChild {
private NestedScrollingChildHelper mNestedScrollingChildHelper;
private final int[] offset = new int[2]; //偏移量
private final int[] consumed = new int[2]; //消费
private int lastY;
public MyCustomNestedScrollingChild(Context context) {
super(context);
}
public MyCustomNestedScrollingChild(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MyCustomNestedScrollingChild(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//记录触摸时的Y轴方向
lastY = (int) event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
int y = (int) (event.getRawY());
int dy = y - lastY;//dy为屏幕上滑动的偏移量
lastY = y;
dispatchNestedPreScroll(0, dy, consumed, offset);
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
break;
}
return true;
}
//初始化helper对象
private NestedScrollingChildHelper getScrollingChildHelper() {
if (mNestedScrollingChildHelper == null) {
mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
mNestedScrollingChildHelper.setNestedScrollingEnabled(true);
}
return mNestedScrollingChildHelper;
}
@Override
public void setNestedScrollingEnabled(boolean enabled) { //设置滚动事件可用性
getScrollingChildHelper().setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {//是否可以滚动
return getScrollingChildHelper().isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {//开始滚动
return getScrollingChildHelper().startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {//停止滚动,清空滚动状态
getScrollingChildHelper().stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {//判断是否含有对应的NestedScrollingParent
return getScrollingChildHelper().hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
//在子view进行滚动之后调用此方法,询问父view是否还要进行余下(unconsumed)的滚动。
//前四个参数为输入参数,用于告诉父view已经消费和尚未消费的距离,最后一个参数为输出参数,用于子view获取父view位置的偏移量。
//如果父view接收了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false
return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
//在子view自己进行滚动之前调用此方法,询问父view是否要在子view之前进行滚动。
//此方法的前两个参数用于告诉父View此次要滚动的距离;而第三第四个参数用于子view获取父view消费掉的距离和父view位置的偏移量。
return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return getScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY);
}
}
MyCustomNestedScrollingParent.java
public class MyCustomNestedScrollingParent extends LinearLayout implements NestedScrollingParent {
private ImageView img;
private TextView tv;
private MyCustomNestedScrollingChild myNestedScrollChild;
private NestedScrollingParentHelper mNestedScrollingParentHelper;
private int imgHeight;
private int tvHeight;
public MyCustomNestedScrollingParent(Context context) {
super(context);
}
public MyCustomNestedScrollingParent(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
}
//当view加载完成时获取子view
@Override
protected void onFinishInflate() {
super.onFinishInflate();
//获取第一个子view,ImageView
img = (ImageView) getChildAt(0);
//获取第二个子view,TextView
tv = (TextView) getChildAt(1);
//获取第三个子view,MyCustomNestedScrollingChild
myNestedScrollChild = (MyCustomNestedScrollingChild) getChildAt(2);
img.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
//当布局变化时,获取图片布局的高度
if (imgHeight <= 0) {
imgHeight = img.getMeasuredHeight();
}
}
});
tv.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
//当布局变化时,获取文字布局的高度
if (tvHeight <= 0) {
tvHeight = tv.getMeasuredHeight();
}
}
});
}
//在此可以判断参数target是哪一个子view以及滚动的方向,然后决定是否要配合其进行嵌套滚动
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
if (target instanceof MyCustomNestedScrollingChild) {
return true;
}
return false;
}
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
}
@Override
public void onStopNestedScroll(View target) {
mNestedScrollingParentHelper.onStopNestedScroll(target);
}
//先于child滚动
//前3个为输入参数,最后一个是输出参数
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
if (showImg(dy) || hideImg(dy)) {//根据图片的高度判断上拉和下拉的处理
scrollBy(0, -dy);
consumed[1] = dy;//告诉child消费了多少距离
}
}
//后于child滚动
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
//是否消费了手指滑动事件
return false;
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
//是否消费了手指滑动事件
return false;
}
@Override
public int getNestedScrollAxes() {
return mNestedScrollingParentHelper.getNestedScrollAxes();
}
//下拉的时候是否要向下滚动以显示图片
public boolean showImg(int dy) {
if (dy > 0) {
if (getScrollY() > 0 && myNestedScrollChild.getScrollY() == 0) {
return true;
}
}
return false;
}
//上拉的时候,是否要向上滚动,隐藏图片
public boolean hideImg(int dy) {
if (dy < 0) {
if (getScrollY() < imgHeight) {
return true;
}
}
return false;
}
//限制滚动范围,防止出现偏差
@Override
public void scrollTo(int x, int y) {
if (y < 0) {
y = 0;
}
if (y > imgHeight) {
y = imgHeight;
}
super.scrollTo(x, y);
}
}
子view思路整理:
- 实现
NestedScrollingChild
接口,其回调方法用辅助类填充 - 监听触摸事件,即重写
onTouchEvent
方法 - 求出移动距离dy,调用
dispatchNestedPreScroll
方法将移动距离传递给父view - 调用
startNestedScroll
方法开始滑动
父view的思路整理:
- 实现
NestedScrollingParent
接口,回调方法用辅助类填充,其中onStartNestedScroll
、onNestedPreScroll
、onNestedScroll
、onNestedPreFling
、onNestedFling
几个方法辅助类是没法填充的,需要自己去实现 - 滚动事件和手势不要混用,这里将
onNestedPreFling
和onNestedFling
的返回值设置成false即可 - 填充
onStartNestedScroll
方法,判断是否是MyCustomNestedScrollingChild
,如果是则允许滑动 - 填充
onNestedPreScroll
方法,根据图片的高度判断上拉和下拉的处理,必须要告诉child消费了多少距离 - 重写
scrollTo
方法,防止滑动偏差
最后,贴一下布局:
<com.zyc.hezuo.animationdemo.MyCustomNestedScrollingParent
android:id="@+id/nestedparent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:src="@mipmap/pic_shi"/>
<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:textSize="20sp"
android:gravity="center"
android:background="@color/bt_1"
android:text="我是标题"/>
<com.zyc.hezuo.animationdemo.MyCustomNestedScrollingChild
android:id="@+id/nestedchild"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="我是任意内容我是任意内容我是任意内容我是任意内容我是任意内容我是任意内容我是任意内容我是任意内容我是任意内容我是任意内容"
android:textSize="30sp"/>
</com.zyc.hezuo.animationdemo.MyCustomNestedScrollingChild>
</com.zyc.hezuo.animationdemo.MyCustomNestedScrollingParent>
[本章完...]