要知道在安卓中,处理事件分发的机制和滑动冲突都是通过View的dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent这三个方法互相配合完成的。
但是这种机制是由父View发起,一直向下传递,而且这个机制有个缺点:
父View将事件分发下去之后,一旦有子View决定处理该系列事件(即子View的onTouchEvent返回了true),那么该系列事件将会一直下发给子View处理,即使子View在某个onTouch事件中不想处理(即返回false),那它的父View也不会再继续处理此系列事件,除非父View拦截某个事件,但是父View拦截了touch事件之后,该系列事件就不会下发给子View处理了
于是乎就有了NestedScrolling机制了,NestedScrolling机制中,通过子View在处理滑动事件的手告诉父View我要开始滑动了,此时父View便会决定要不要跟着一起动,或者会寻找有没有另外的子View要跟着一起滑动。NestedScrolling机制主要依赖以下四个类:
一般作为滑动列表的子View实现NestedScrollingChild接口,作为parent的父View则实现NestedScrollingParent,这样滑动的子View就可以将滑动事件分发给父View,并且父View也可以将滑动事件分发给其他的子View了。
贴出NestedScrollingChild和NestedScrollingParent两个接口的API解释:
public interface NestedScrollingChild {
/**
* 设置嵌套滑动是否能用
*/
@Override
public void setNestedScrollingEnabled(boolean enabled);
/**
* 判断嵌套滑动是否可用
*/
@Override
public boolean isNestedScrollingEnabled();
/**
* 开始发起嵌套滑动
*
* @param axes 表示方向轴,有横向和竖向
*/
@Override
public boolean startNestedScroll(int axes);
/**
* 停止嵌套滑动
*/
@Override
public void stopNestedScroll();
/**
* 判断是否有支持嵌套滑动的父View
*/
@Override
public boolean hasNestedScrollingParent() ;
/**
* 惯性滑动时调用
* @param velocityX x 轴上的滑动速率
* @param velocityY y 轴上的滑动速率
* @param consumed 是否被消费
* @return true 如果惯性滑动被父View或其它子View消耗
*/
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) ;
/**
* 进行惯性滑动前调用
* @param velocityX x 轴上的滑动速率
* @param velocityY y 轴上的滑动速率
* @return true 父View消耗了惯性滑动
*/
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) ;
/**
* 子view处理滑动之后调用(即子view滑动完之后通知父view)
* @param dxConsumed x轴上被消费的距离(横向)
* @param dyConsumed y轴上被消费的距离(竖向)
* @param dxUnconsumed x轴上未被消费的距离
* @param dyUnconsumed y轴上未被消费的距离
* @param offsetInWindow 子View的窗体偏移量
* @return true 事件被父View处理, false 事件没有被父View处理
*/
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) ;
/**
* 在子View的onInterceptTouchEvent或者onTouch中,调用该方法通知父View滑动的距离(即在子view处理滑动之前通知父View是否要消耗滑动事件)
* @param dx x轴上滑动的距离
* @param dy y轴上滑动的距离
* @param consumed 父view消费掉的滑动长度
* @param offsetInWindow 子View的窗体偏移量
* @return 支持的嵌套的父View 是否处理了 滑动事件
*/
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow);
}
public interface NestedScrollingParent {
/**
* 回应子view发起的嵌套滑动
* @param child 实现了NestedScrollingParent的view
* @param target 发起嵌套滑动的子view
* @param axes 滑动方向
* @return true 表示接受嵌套滑动
*/
boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes);
/**
* 接受嵌套滑动时回调
* @param child 实现了NestedScrollingParent的view
* @param target 发起嵌套滑动的子view
* @param axes 滑动方向
*/
void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes);
/**
* 嵌套滑动结束回调
* @param target 发起嵌套滑动的子view
*/
void onStopNestedScroll(@NonNull View target);
/**
* 回应正在进行的嵌套滑动(后于子view)
* @param target 发起嵌套滚动的子view
* @param dxConsumed 已经消耗的水平滚动距离
* @param dyConsumed 已经消耗的垂直滚动距离
* @param dxUnconsumed 未消耗的水平滚动距离
* @param dyUnconsumed 未消耗的垂直滚动距离
*/
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
/**
* 嵌套滚动开始之前回调(先于子view)
* @param target 发起嵌套滚动的子view
* @param dx 消耗的水平滚动距离
* @param dy 消耗的垂直滚动距离
* @param consumed 输出此父view消耗的水平和垂直滚动距离
*/
void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);
/**
* 开始惯性滑动时回调(后于子view)
* @param var1
* @param var2
* @param var3
* @param var4
* @return
*/
boolean onNestedFling(@NonNull View var1, float var2, float var3, boolean var4);
/**
* 惯性滑动之前回调(先于子view)
* @param var1
* @param var2
* @param var3
* @return
*/
boolean onNestedPreFling(@NonNull View var1, float var2, float var3);
/**
* 返回NestedScrollingParent的滑动方向
* @return
*/
int getNestedScrollAxes();
}
ok,对两个接口的API有了一定的了解之后,现在我们来看一下NestedScrollingChild接口和NestedScrollingParent接口中的方法对应关系:
NestedScrollingChildImpl | NestedScrollingParentImpl |
---|---|
startNestedScroll | onStartNestedScroll, onNestedScrollAccepted |
dispatchNestedPreScroll | onNestedPreScroll |
dispatchNestedScroll | onNestedScroll |
stopNestedScroll | onStopNestedScroll |
接下来写个实例验证一下,先上效果图:
首先我们自定义一个YNestedScrollingChild继承自LinearLayout,然后实现NestedScrollingChild接口,并实现里面的方法,一般作为发起嵌套滑动的子View,只需自己实现onTouchEvent,在onTouchEvent里面处理相应的逻辑即可,其他NestedScrollingChild接口里面的方法就交给NestedScrollingChildHelper或它的super View去处理。
说明一点:为什么上面说可以交给super View处理呢?因为在API21或以上,安卓View里卖弄已经帮我们实现了NestedScrolling机制,即不需要我们手动实现NestedScrollingChild和NestedScrollingParent接口了,例如:
public class NestedScrollingChildImpl extends RelativeLayout {
public NestedScrollingChildImpl(Context context) {
super(context);
}
public NestedScrollingChildImpl(Context context, AttributeSet attrs) {
super(context, attrs);
}
public NestedScrollingChildImpl(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean startNestedScroll(int axes) {
return super.startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
super.stopNestedScroll();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @androidx.annotation.Nullable int[] offsetInWindow) {
return super.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, @androidx.annotation.Nullable int[] consumed, @androidx.annotation.Nullable int[] offsetInWindow) {
return super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
}
当然在低于API21的情况下呢?那就需要我们自己实现NestedScrollingChild和NestedScrollingParent两个接口了,这里以NestedScrollingChildImpl举例:
public class NestedScrollingChildImpl extends RelativeLayout implements NestedScrollingChild {
private NestedScrollingChildHelper mChildHelper;
public NestedScrollingChildImpl(Context context) {
this(context,null);
}
public NestedScrollingChildImpl(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public NestedScrollingChildImpl(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mChildHelper = new NestedScrollingChildHelper(this);
}
@Override
public boolean startNestedScroll(int axes) {
return mChildHelper.startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
mChildHelper.stopNestedScroll();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @androidx.annotation.Nullable int[] offsetInWindow) {
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, @androidx.annotation.Nullable int[] consumed, @androidx.annotation.Nullable int[] offsetInWindow) {
return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
}
到这里相信应该可以解除很大一部分伙计心中的疑虑了。说明:上述两个NestedScrollingChildImpl类的代码仅仅为了说明API21或以上和API21以下的两种实现方式,所以NestedScrollingChildImpl 中只实现了部分接口,而且跟今天要讲的例子没有任何关系,请各位看官忽略。
接下来继续实现上面gif的例子:
先呈现XML代码:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.dreamer__yy.widget.YNestedScrollingParent
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/iv"
android:layout_width="match_parent"
android:layout_height="150dp"
android:scaleType="fitXY"
android:src="@drawable/icon_header" />
<TextView
android:layout_width="match_parent"
android:layout_height="45dp"
android:background="@color/colorPrimary"
android:gravity="center"
android:text="我是要停靠的,我上面的图片是要滑动的"
android:textColor="@color/white"
android:textSize="18sp" />
<com.dreamer__yy.widget.YNestedScrollingChild
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/large_text"
android:textSize="15sp" />
</com.dreamer__yy.widget.YNestedScrollingChild>
</com.dreamer__yy.widget.YNestedScrollingParent>
</RelativeLayout>
接下来直接上实现了NestedScrollingChild的子View的代码:
public class YNestedScrollingChild extends LinearLayout {
private NestedScrollingChildHelper childHelper;
private int preHeight;
private int maxHeight;
private float lastY;
private int[] consumed = new int[2];
private int[] offset = new int[2];
public YNestedScrollingChild(Context context) {
this(context,null);
}
public YNestedScrollingChild(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public YNestedScrollingChild(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
childHelper = new NestedScrollingChildHelper(this);
childHelper.setNestedScrollingEnabled(true);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//因为XML里面给YNestedScrollingChild的高设置的时wrap_content,这里得出第一次测量的高度即展示出来的高度
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
preHeight = getMeasuredHeight();
//将测量模式设置成不限制,得出YNestedScrollingChild的总高度
heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
maxHeight = getMeasuredHeight();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//记录第一次触摸的Y坐标
lastY = event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
//得到手指触摸的当前的Y坐标
float rawY = event.getRawY();
//得到手指滑动的Y的距离
int dy = (int) (rawY - lastY);
lastY = rawY;
//判断时竖直滑动且找到了支持嵌套滑动的父view并且父view处理了滑动
//consumed是一个空的数组,这里直接传给父View,让父View将自己消耗的Y轴上的距离写进去
if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) && dispatchNestedPreScroll(0, dy, consumed, offset)) {
//计算父View消耗了滑动事件之后还剩下多少待滑动的距离
int scrollY = dy - consumed[1];
if (scrollY != 0) {
//自己处理剩下的滑动距离
scrollBy(0, -scrollY);
}
}else {
//不是竖直滑动,或者没找到支持嵌套滑动的父View,或者父view不处理滑动事件,此时自己处理滑动即可
scrollBy(0, -dy);
}
break;
}
return true;
}
//设置滑动是否可行,此处交给父View处理即可(API 低于21的就交给NestedScrollingChildHelper处理)
@Override
public void setNestedScrollingEnabled(boolean enabled) {
super.setNestedScrollingEnabled(enabled);
}
//嵌套滑动是否可行,此处注意一定要交给NestedScrollingChildHelper处理
@Override
public boolean isNestedScrollingEnabled() {
return childHelper.isNestedScrollingEnabled();
}
//开始嵌套滑动
@Override
public boolean startNestedScroll(int axes) {
return super.startNestedScroll(axes);
}
//结束嵌套滑动
@Override
public void stopNestedScroll() {
super.stopNestedScroll();
}
//是否有支持嵌套滑动的父View
@Override
public boolean hasNestedScrollingParent() {
return super.hasNestedScrollingParent();
}
//子view处理滑动之后调用(即子view滑动完之后通知父view)
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) {
return super.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
}
//在子View的onInterceptTouchEvent或者onTouch中,调用该方法通知父View滑动的距离(即在子view处理滑动之前通知父View是否要消耗滑动事件)
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow) {
return super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
//惯性滑动时调用
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return super.dispatchNestedFling(velocityX, velocityY, consumed);
}
//进行惯性滑动前调用
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return super.dispatchNestedPreFling(velocityX, velocityY);
}
/**
* 控制view滑动范围,不让view进行多余的滑动
* @param x
* @param y
*/
@Override
public void scrollTo(int x, int y) {
y = y < 0 ? 0 : (y > (maxHeight - preHeight) ? (maxHeight - preHeight) : y);
super.scrollTo(x, y);
}
}
然后是实现了NestedScrollingParent接口的父View的代码:
public class YNestedScrollingParent extends LinearLayout {
private NestedScrollingParentHelper parentHelper;
private int imgHeight = 0;
private ImageView iv;
private YNestedScrollingChild scrollingChild;
public YNestedScrollingParent(Context context) {
this(context,null);
}
public YNestedScrollingParent(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public YNestedScrollingParent(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
parentHelper = new NestedScrollingParentHelper(this);
}
/**
* 布局加载完成回调
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
iv = ((ImageView) getChildAt(0));
scrollingChild = ((YNestedScrollingChild) getChildAt(2));
iv.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (imgHeight <= 0) {
imgHeight = iv.getMeasuredHeight();
}
}
});
}
/**
* 控制view滑动范围
* @param x
* @param y
*/
@Override
public void scrollTo(int x, int y) {
y = y < 0 ? 0 : (y > imgHeight ? imgHeight : y);
super.scrollTo(x, y);
}
//如果目标view是YNestedScrollingChild,就接受嵌套滑动事件
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return target instanceof YNestedScrollingChild;
}
@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
super.onNestedScrollAccepted(child, target, axes);
}
@Override
public void onStopNestedScroll(View child) {
super.onStopNestedScroll(child);
}
//子view滑动后回调(后于子view),此处不做逻辑处理
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
}
//子view滑动前回调(先于子view)
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
//判断是否需要滑动
if (showImg(dy) || hideImg(dy)) {
scrollBy(0,-dy);
//记录父View消耗的滑动距离
consumed[1] = dy;
}
//获取嵌套滑动方向
@Override
public int getNestedScrollAxes() {
return super.getNestedScrollAxes();
}
private boolean showImg(int dy){
if (dy > 0) {//表示向下滑动
if (getScrollY() > 0) {
return true;
}
}
return false;
}
private boolean hideImg(int dy){
if (dy < 0) {//表示向上滑动
if (getScrollY() < imgHeight) {//控制最大只能滑动imgHeight
return true;
}
}
return false;
}
}
有人会说,不是说是实现了NestedScrollingParent接口的父View嘛,有这个疑问的朋友肯定是上课走神了,这个在前面解释过了,当然在API21或以上你也可以自己实现NestedScrollingChild/NestedScrollingParent,然后配合NestedScrollingChildHelper /NestedScrollingParentHelper 完成逻辑处理。
OK,这样就可以实现上面gif图的效果啦,本次内容就到此结束的咯~~