栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Android 嵌套滑动

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Android 嵌套滑动

在 Android 的事件分发机制当中,对于同一个事件流,如果由父控件拦截/消费了,那么子控件就没办法再获取到该事件流,从而造成在嵌套滑动时的不连贯(一次滑动动作无法同时滑动内外两个可滑动的 View)。

使用嵌套滑动机制可以有效的解决上面的问题。它并没有改变事件分发机制,在发生嵌套滑动时,还是先进行事件分发,由父控件将事件分发给子 View,由子 View 进行消费。只不过,子 View 在自己消费之前,会先去询问父控件,是否需要处理滑动事件。于是才有了“在嵌套滑动中,子 View 是主动的”这样的说法。

下面我们就结合源码看看嵌套滑动是如何实现的。

一、NestedScrollingChild 与 NestedScrollingParent

在嵌套滑动中有两个角色:Child 和 Parent,Child 需要实现 NestedScrollingChild/NestedScrollingChild2/NestedScrollingChild3 接口之一,而 Parent 需要实现 NestedScrollingParent/NestedScrollingParent2/NestedScrollingParent3 接口之一。

两组接口在各自内部都具有继承关系,先看 NestedScrollingChild:

public interface NestedScrollingChild {
    
    void setNestedScrollingEnabled(boolean enabled);
    
    
    boolean isNestedScrollingEnabled();
    
    
    boolean startNestedScroll(@ScrollAxis int axes);
    
    
    void stopNestedScroll();
    
    
    boolean hasNestedScrollingParent();
    
    
    boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow);
    
    
    boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow);
    
    
    boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
    
    
    boolean dispatchNestedPreFling(float velocityX, float velocityY);
}

NestedScrollingChild2 继承了 NestedScrollingChild,对 NestedScrollingChild 内的部分方法参数进行了扩展,添加了 @NestedScrollType int type 参数,这个 type 用来表示滑动类型,是 TYPE_TOUCH(触摸滑动)或 TYPE_NON_TOUCH(惯性滑动):

public interface NestedScrollingChild2 extends NestedScrollingChild {

    boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type);

    void stopNestedScroll(@NestedScrollType int type);

    boolean hasNestedScrollingParent(@NestedScrollType int type);

    boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
            @NestedScrollType int type);

    boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow, @NestedScrollType int type);
}

NestedScrollingChild3 又继承了 NestedScrollingChild2,又对 dispatchNestedScroll() 的方法参数进行了扩充:

public interface NestedScrollingChild3 extends NestedScrollingChild2 {

    void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed,
            @Nullable int[] offsetInWindow, @ViewCompat.NestedScrollType int type,
            @NonNull int[] consumed);
}

再来看 NestedScrollingParent:

public interface NestedScrollingParent {
    
    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);

    
    void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);

    
    void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);

    
    boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed);

    
    boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY);

    
    @ScrollAxis
    int getNestedScrollAxes();
}

NestedScrollingParent2 和 NestedScrollingParent3 对 NestedScrollingParent 的扩展类似于 NestedScrollingChild,代码就不贴了。

二、滑动过程

以上接口中的众多方法是如何使用的,过程如何,先看下图:

将整个嵌套滑动过程分为四个阶段:初始阶段、预滚动阶段、滚动阶段和结束阶段。每个阶段都是子 View 主动回调父控件中的方法,询问父控件的处理状态。而父控件则是被动的接收到滑动事件,根据自己的状态和需求决定是否消费并返回结果。

初始阶段

源码将以既实现了 NestedScrollingChild3 又实现了 NestedScrollingParent3 的 NestedScrollView 为例。

在实现 NestedScrollingChild 和 NestedScrollingParent 接口内的方法时,具体的操作一般都是交给相应的帮助类 NestedScrollingChildHelper 和 NestedScrollingParentHelper 内的同名方法来处理即可。比如说在初始阶段,先调用 NestedScrollingChild 中的 setNestedScrollingEnabled() 来开启嵌套滑动,实际上是调用的 NestedScrollingChildHelper 的同名方法:

#NestedScrollView:
 
	@Override
    public void setNestedScrollingEnabled(boolean enabled) {
        mChildHelper.setNestedScrollingEnabled(enabled);
    }

#NestedScrollingChildHelper:

	public void setNestedScrollingEnabled(boolean enabled) {
        if (mIsNestedScrollingEnabled) {
            ViewCompat.stopNestedScroll(mView);
        }
        mIsNestedScrollingEnabled = enabled;
    }

随后当发生 ACTION_DOWN 时,在 Child 的 onTouchEvent() 中调用 startNestedScroll():

	@Override
    public boolean onTouchEvent(MotionEvent ev) {		
		final int actionMasked = ev.getActionMasked();
		switch (actionMasked) {
			case MotionEvent.ACTION_DOWN: {
				……
				startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
                break;
			}
		}
	}

还是直接把处理交给 Helper:

#NestedScrollView:

	@Override
    public boolean startNestedScroll(int axes, int type) {
        return mChildHelper.startNestedScroll(axes, type);
    }

#NestedScrollingChildHelper:
	
	public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
		// 如果已经找到嵌套滑动的Parent了,直接返回
        if (hasNestedScrollingParent(type)) {
            return true;
        }
        // 在开启嵌套滑动的前提下,为当前Child循环向上寻找一个可以处理嵌套滑动的Parent
        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;
                }
               	// 如果p不能处理嵌套滑动,则让Child指向p,而p指向p的parent开启下一次循环
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

ViewParentCompat 做了兼容处理,onStartNestedScroll() 是去调用 NestedScrollingParent 中的同名方法,返回值表示 Parent 是否接受嵌套滑动事件:

#ViewParentCompat:

	public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
            int nestedScrollAxes, int type) {
        if (parent instanceof NestedScrollingParent2) {
            // First try the NestedScrollingParent2 API
            return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
                    nestedScrollAxes, type);
        } else if (type == ViewCompat.TYPE_TOUCH) {
            // Else if the type is the default (touch), try the NestedScrollingParent API
            if (Build.VERSION.SDK_INT >= 21) {
                try {
                    return parent.onStartNestedScroll(child, target, nestedScrollAxes);
                } catch (AbstractMethodError e) {
                    Log.e(TAG, "ViewParent " + parent + " does not implement interface "
                            + "method onStartNestedScroll", e);
                }
            } else if (parent instanceof NestedScrollingParent) {
                return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
                        nestedScrollAxes);
            }
        }
        return false;
    }

来看 NestedScrollView 内是怎样决定是否处理嵌套滑动的:

	// NestedScrollingParent
    @Override
    public boolean onStartNestedScroll(
            @NonNull View child, @NonNull View target, int nestedScrollAxes) {
        return onStartNestedScroll(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
    }
    
    // NestedScrollingParent2
    @Override
    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes,
            int type) {
        return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

可以看到垂直方向滑动就可以处理嵌套滑动。由于 NestedScrollingParent2 中的 onStartNestedScroll() 参数中提供了滑动方向 axes,所以这里不需要调用我们图中给出的 getNestedScrollAxes() 去获取滑动方向了。

回到 startNestedScroll(),假如我们找到了一个 Parent,接下来就会立即调用 setNestedScrollingParentForType() 和 onNestedScrollAccepted():

#NestedScrollingChildHelper:

	private void setNestedScrollingParentForType(@NestedScrollType int type, ViewParent p) {
        switch (type) {
        	// 触摸滑动
            case TYPE_TOUCH:
                mNestedScrollingParentTouch = p;
                break;
            // 不是用户触摸屏幕造成的滑动,比如惯性滑动Fling
            case TYPE_NON_TOUCH:
                mNestedScrollingParentNonTouch = p;
                break;
        }
    }
   
#ViewParentCompat:

	public static void onNestedScrollAccepted(ViewParent parent, View child, View target,
            int nestedScrollAxes, int type) {
        if (parent instanceof NestedScrollingParent2) {
            // First try the NestedScrollingParent2 API
            ((NestedScrollingParent2) parent).onNestedScrollAccepted(child, target,
                    nestedScrollAxes, type);
        } else if (type == ViewCompat.TYPE_TOUCH) {
            // Else if the type is the default (touch), try the NestedScrollingParent API
            if (Build.VERSION.SDK_INT >= 21) {
                try {
                    parent.onNestedScrollAccepted(child, target, nestedScrollAxes);
                } catch (AbstractMethodError e) {
                    Log.e(TAG, "ViewParent " + parent + " does not implement interface "
                            + "method onNestedScrollAccepted", e);
                }
            } else if (parent instanceof NestedScrollingParent) {
                ((NestedScrollingParent) parent).onNestedScrollAccepted(child, target,
                        nestedScrollAxes);
            }
        }
    }

onNestedScrollAccepted() 主要是给 Parent 做初始化工作用的,比如调用 NestedScrollingParentHelper 中的同名方法可以记录是横向滑动还是纵向滑动:

#NestedScrollingParentHelper:

	public void onNestedScrollAccepted(@NonNull View child, @NonNull View target,
            @ScrollAxis int axes, @NestedScrollType int type) {
        if (type == ViewCompat.TYPE_NON_TOUCH) {
            mNestedScrollAxesNonTouch = axes;
        } else {
            mNestedScrollAxesTouch = axes;
        }
    }

总结一下,初始阶段的工作就是在开启了嵌套滑动功能的前提下,为 Child 找到一个能处理嵌套滑动的 Parent,如果找到了,就为该 Parent 进行一下初始化工作。

预滚动阶段、滚动阶段

预滚动阶段由 ACTION_MOVE 事件触发,先调用 dispatchNestedPreScroll() 询问 Parent 是否需要处理,同样是把具体操作交给 NestedScrollingChildHelper 的同名方法:

	@Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
            int type) {
        return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
    }
	
	public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow, @NestedScrollType int type) {
        if (isNestedScrollingEnabled()) {
        	// 拿到支持嵌套滑动的父容器
            final ViewParent parent = getNestedScrollingParentForType(type);
            if (parent == null) {
                return false;
            }

            if (dx != 0 || dy != 0) {
                int startX = 0;
                int startY = 0;
                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }

				// 如果参数没传 consumed,就临时构造一个。容错处理
                if (consumed == null) {
                    consumed = getTempNestedScrollConsumed();
                }
                consumed[0] = 0;
                consumed[1] = 0;
                // 调用 Parent 的 onNestedPreScroll(),询问其是否需要处理滑动
                ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);

                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
                // 如果 Parent 有在xy的某一个方向进行了消费,就返回 true 表示 Parent 处理了嵌套滑动
                return consumed[0] != 0 || consumed[1] != 0;
            } else if (offsetInWindow != null) {
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }

通过 ViewParentCompat 调用到 Parent 的 onNestedPreScroll(),这时就需要根据具体业务情况处理了,比如说 NestedScrollView 是将其继续向上层分发:

#NestedScrollView:

	@Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
            int type) {
        dispatchNestedPreScroll(dx, dy, consumed, null, type);
    }

而如果我们自定义时需要直接进行滑动处理的话,需要将消费掉的距离准确的写到 consumed 中:

	@Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed)
    {
        // 垂直方向上滑动 dy
        scrollBy(0, dy);
        // 把 Y 轴消费掉的距离写到 consumed[1] 中
        consumed[1] = dy;
    }

这样 Child 会通过 consumed 得知 Parent 消费的距离,然后决定后面如何处理,以 NestedScrollView 为例:

	@Override
    public boolean onTouchEvent(MotionEvent ev) {
		switch (actionMasked) {
			if (mIsBeingDragged) {
				case MotionEvent.ACTION_MOVE:
                // 省略非重点部分……
                if (mIsBeingDragged) {
                    // 1.先询问 Parent 是否消费
                    if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
                            ViewCompat.TYPE_TOUCH)) {
                        // 如果 Parent 消费了,就从 deltaY 中减去已经消费掉的部分
                        deltaY -= mScrollConsumed[1];
                        mNestedYOffset += mScrollOffset[1];
                    }

                    mLastMotionY = y - mScrollOffset[1];

                    final int oldY = getScrollY();
                    final int range = getScrollRange();
                    final int overscrollMode = getOverScrollMode();
                    boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS
                            || (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);

                    // 2.自己消费一部分
                    if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
                            0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) {
                        // Break our velocity if we hit a scroll barrier.
                        mVelocityTracker.clear();
                    }

                    final int scrolledDeltaY = getScrollY() - oldY;
                    // 计算出仍未被消费的距离
                    final int unconsumedY = deltaY - scrolledDeltaY;

                    mScrollConsumed[1] = 0;
					// 3.再次询问 Parent 是否消费
                    dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
                            ViewCompat.TYPE_TOUCH, mScrollConsumed);

                    mLastMotionY -= mScrollOffset[1];
                    mNestedYOffset += mScrollOffset[1];

                    // 省略……
                }
                break;
		}
	}

关注主流程,第 2 步计算出还未消费的 Y 轴距离后,在第 3 步再次询问 Parent 是否消费,通过 NestedScrollingChildHelper 还会再回调到 Parent 的 onNestedScroll():

#NestedScrollingChildHelper:

	public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
            int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type,
            @Nullable int[] consumed) {
        dispatchNestedScrollInternal(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
                offsetInWindow, type, consumed);
    }

	private boolean dispatchNestedScrollInternal(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
            @NestedScrollType int type, @Nullable int[] consumed) {
        if (isNestedScrollingEnabled()) {
            final ViewParent parent = getNestedScrollingParentForType(type);
            if (parent == null) {
                return false;
            }

            if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
                int startX = 0;
                int startY = 0;
                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }

                if (consumed == null) {
                    consumed = getTempNestedScrollConsumed();
                    consumed[0] = 0;
                    consumed[1] = 0;
                }
				// 回调 Parent 的 onNestedScroll()
                ViewParentCompat.onNestedScroll(parent, mView,
                        dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed);

                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
                return true;
            } else if (offsetInWindow != null) {
                // No motion, no dispatch. Keep offsetInWindow up to date.
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }

onNestedScroll() 的具体处理还是要看业务逻辑,这里我们仍以 NestedScrollView 为例:

	// NestedScrollingParent3
    @Override
    public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, int type, @NonNull int[] consumed) {
        onNestedScrollInternal(dyUnconsumed, type, consumed);
    }

    private void onNestedScrollInternal(int dyUnconsumed, int type, @Nullable int[] consumed) {
        final int oldScrollY = getScrollY();
        // 垂直方向上滑动 dyUnconsumed
        scrollBy(0, dyUnconsumed);
        // 计算出本次垂直方向上消费的距离
        final int myConsumed = getScrollY() - oldScrollY;

		// 把消费距离 myConsumed 累加到 consumed[1],并从为消费距离中减掉
        if (consumed != null) {
            consumed[1] += myConsumed;
        }
        final int myUnconsumed = dyUnconsumed - myConsumed;

		// 回调 Parent 的 dispatchNestedScroll() 把更新后的数据传进去
        mChildHelper.dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null, type, consumed);
    }

自己消费掉一部分,计算出未消费部分,继续向上分发,逻辑如下图:


其实总结一下,就是 Child 通过 dispatchNestedPreScroll() 询问 Parent 是否要处理滑动,如果 Parent 消费就把消费掉的部分写入方法参数 consumed,Child 根据 consumed 的值决定后续操作,如果仍有未消费部分,可以自己消费掉一部分,如果还有剩余,就再通过 dispatchNestedScroll() 二次询问 Parent 是否消费。

到这里预滚动阶段和滚动阶段的流程就说完了。

结束阶段

结束阶段一般在 ACTION_UP 事件中处理,先执行 dispatchNestedPreFling(),回调到 Parent 的 onNestedPreFling(),然后执行 dispatchNestedFling() 再回调 Parent 的 onNestedFling(),最后执行自己的 Fling 操作。

Fling 操作结束后,调用 stopNestedScroll(),同样是回调 Parent 的相关方法,过程与之前类似,就不再啰嗦的贴出全部代码了,可以去 NestedScrollView 中看一下(NestedScrollView 的 stopNestedScroll() 是在 ACTION_UP 的 endDrag() 中调用的)。

三、举例

下面举个简单的实例吧,效果如下:

布局很简单,嵌套滑动的 Parent 中从上到下依次是 ImageView、TextView 和嵌套滑动的 Child,Child 也是一个容器,里面放一个 TextView:




    

    

    

        
    


自定义的控件 NestedScrollParentLayout、NestedScrollChildLayout 都继承了 LinearLayout 并分别实现了 NestedScrollingParent2、NestedScrollingChild2,需要先说明的是,这两个控件的实现不具有一般性,很多地方的实现都是基于已知某个子控件是 ImageView 或 TextView 的前提下,仅做加深对原理的理解之用。

先来看 NestedScrollChildLayout,初始化时创建 NestedScrollingChildHelper,所有嵌套滑动过程中用到的 NestedScrollingChild2 中的方法都可以交给 NestedScrollingChildHelper 处理:

public class NestedScrollChildLayout extends LinearLayout implements NestedScrollingChild2 {

    private static final String TAG = NestedScrollChildLayout.class.getSimpleName();

    private NestedScrollingChildHelper mChildHelper;

    public NestedScrollChildLayout(Context context) {
        this(context, null);
    }

    public NestedScrollChildLayout(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public NestedScrollChildLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mChildHelper = new NestedScrollingChildHelper(this);
        mChildHelper.setNestedScrollingEnabled(true);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return mChildHelper.isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes, int type) {
        return mChildHelper.startNestedScroll(axes, type);
    }

    @Override
    public void stopNestedScroll(int type) {
        mChildHelper.stopNestedScroll(type);
    }

    @Override
    public boolean hasNestedScrollingParent(int type) {
        return mChildHelper.hasNestedScrollingParent(type);
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type) {
        return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, int type) {
        return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }
    ......
}

NestedScrollChildLayout 本身的高度设置的是 match_parent,但是其内部可以滑动的 TextView 假如内容很多的话,它的高度可能会比 NestedScrollChildLayout 甚至整个屏幕的高度还要高,所以需要重写 onMeasure() 进行测量:

	int mRealHeight = 0;
	
	@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        mRealHeight = 0;
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            // 子 View 是 WRAP_ConTENT 的话,由于是在一个可滚动的父 View 中,
            // 其实际高度可能高于父 View 的高度,所以设置为 UNSPECIFIED。
            if (view.getLayoutParams().height == LayoutParams.WRAP_CONTENT) {
                heightMeasureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.UNSPECIFIED);
            }

            measureChild(view, widthMeasureSpec, heightMeasureSpec);
            Log.i(TAG, "getMeasuredHeight: " + view.getMeasuredHeight());
            mRealHeight += view.getMeasuredHeight();
        }
        Log.i(TAG, "realHeight: " + mRealHeight);
        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
    }

意思就是如果子 View 的高度是 WRAP_ConTENT 的话,考虑到其高度可能会远超父容器和屏幕,所以先不给它设置限制,即把 MeasureSpec 的 mode 改成 UNSPECIFIED,关于这一点的原理可以参考 每日一问:详细说一下 MeasureSpec.UNSPECIFIED。

再有就是要控制滑动的范围,上不能超出 0 下不能越过整个控件的测量高度:

	
    @Override
    public void scrollTo(int x, int y) {
        Log.i(TAG, "y: " + y + ", getScrollY: " + getScrollY() + ", height: " + getHeight() + ", realHeight: " + mRealHeight + ", -- " + (mRealHeight - getHeight()));
        if (y < 0) {
            y = 0;
        }
        if (y > mRealHeight) {
            y = mRealHeight;
        }
        if (y != getScrollY()) {
            Log.e(TAG, "scrollTo: " + y);
            super.scrollTo(x, y);
        }
    }

最后就是作为嵌套滑动中“主动”的一方,在 ACTION_DOWN、ACTION_MOVE 事件发生时调用相应的流程方法:

	private int mLastTouchX;
    private int mLastTouchY;
    
	@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                mLastTouchY = (int) (event.getRawY() + .5f);
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int x = (int) (event.getRawX() + .5f);
                int y = (int) (event.getRawY() + .5f);
                int dx = mLastTouchX - x;
                int dy = mLastTouchY - y;
                mLastTouchX = x;
                mLastTouchY = y;

                int[] consumed = new int[2];
                // 先询问滑动 Parent 是否消费滑动事件,将其消费掉的部分减去,剩下的自己消费
                if (dispatchNestedPreScroll(dx, dy, consumed, null, ViewCompat.TYPE_NON_TOUCH)) {
                    Log.i(TAG, "dy: " + dy + ", consumed: " + consumed[1]);
                    dy -= consumed[1];
                    if (dy == 0) {
                        Log.i(TAG, "dy: " + dy);
                        return true;
                    } 
                } else {
                    // 滑动 Parent 不处理滑动事件就全部由自己消费掉
                    Log.i(TAG, "scrollBy: " + dy);
                    scrollBy(0, dy);
                }
                break;
            }
        }
        return true;
    }

ACTION_MOVE 中先调用 dispatchNestedPreScroll() 把滑动事件交给 Parent 处理,如果 Parent 处理了就看它是否消费了全部的 dy,再做后续处理,否则就由自己消费全部的 dy。

Child 这边还重写了一个 canScrollVertically(),用来判断是否能在指定的方向上继续滑动:

	
    @Override
    public boolean canScrollVertically(int direction) {
        // 已经滑动的 Y 轴距离,范围为[0,mRealHeight]
        int scrollY = getScrollY();
        // 向下滑动时,只要 scrollY > 0 说明还没见顶,就还能滑
        if (direction > 0) {
            return scrollY > 0;
        } else if (direction < 0) {
            // 向上滑动时,只要 scrollY < mRealHeight 就说明还没到底部,就还能滑
            return scrollY < mRealHeight;
        }
        return super.canScrollVertically(direction);
    }

重写这个方法是因为 View 中默认的方法并不适合我们的滑动逻辑,它会在 Parent 中用到,到时候再说一下。

以上是 Child 部分,再看 Parent,套路基本是一样的,先创建 NestedScrollingParentHelper 把大部分任务交给它:

public class NestedScrollParentLayout extends LinearLayout implements NestedScrollingParent2 {

    private static final String TAG = NestedScrollParentLayout.class.getSimpleName();

    private NestedScrollingParentHelper mParentHelper;

    public NestedScrollParentLayout(Context context) {
        this(context, null);
    }

    public NestedScrollParentLayout(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public NestedScrollParentLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mParentHelper = new NestedScrollingParentHelper(this);
    }

    
    @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) {
        mParentHelper.onNestedScrollAccepted(child, target, axes, type);
    }

    @Override
    public void onStopNestedScroll(@NonNull View target, int type) {
        mParentHelper.onStopNestedScroll(target, type);
    }

    @Override
    public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int realHeight = 0;
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            if (view.getLayoutParams().height == LayoutParams.WRAP_CONTENT) {
                heightMeasureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.UNSPECIFIED);
            }

            measureChild(view, widthMeasureSpec, heightMeasureSpec);
            Log.i(TAG, "getMeasuredHeight: " + view.getClass().getSimpleName() + "," + view.getMeasuredHeight());
            realHeight += view.getMeasuredHeight();
        }
        Log.i(TAG, "realHeight: " + realHeight);
        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
    }

    @Override
    public void scrollTo(int x, int y) {
    	// child view 0 就是 ImageView,滑动下边界不能超过 ImageView 的底部
        View view = getChildAt(0);
        if (y < 0) {
            y = 0;
        }
        if (y > view.getHeight()) {
            y = view.getHeight();
        }
        if (y != getScrollY()) {
            super.scrollTo(x, y);
        }
    }
}

onMeasure() 代码和 Child 是一样的,scrollTo() 的下边界控制在 ImageView 的底边,这样滑动时即便传入了超出边界的 y 也不会执行滑动动作,从而实现 ImageView 下面的 TextView 的顶置效果。

嵌套滑动流程方法中唯一需要重写的就是 onNestedPreScroll(),当 Child 把嵌套滑动事件分发过来的时候,我们要根据当前状态决定是否处理,从效果图中能看出我们的处理原则是:

  1. 向上滑动时,Parent 优先,如果 ImageView 还在显示,那么就由 Parent 先消费,否则就由 Child 消费。
  2. 向下滑动时,Child 优先,如果 Child 还能向下滑动,就由 Child 消费,否则由 Parent 消费。

代码如下:

	
	@Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        // 获取图片的 ImageView
        View view = getChildAt(0);
        // 向上滑但是 ImageView 仍然可见
        boolean hideTop = dy > 0 && getScrollY() < view.getHeight();
        // 向下滑但 Parent 尚未到顶部,并且 Child 不能再向下滑动了
        boolean showTop = dy < 0 && getScrollY() > 0 && !target.canScrollVertically(1);
        if (showTop || hideTop) {
        	// 可以消费的最大距离,就是 ImageView 的高度减去已经滑动了的距离
            int scrollDy = view.getHeight() - getScrollY();
            // 只需消费本 Layout 中可以滑动的部分,不要过度消费
            int consumedDy = Math.min(dy, scrollDy);
            scrollBy(0, consumedDy);
            consumed[1] = consumedDy;
        }

        Log.i(TAG, "onNestedPreScroll--getScrollY():" + getScrollY() + ",dx:" + dx + ",dy:" + dy + ",consumed:" + consumed[1]);
    }

参数 target 就是 NestedScrollChildLayout,计算 showTop 时判断 Child 不能下滑的条件时正好可以用 !target.canScrollVertically(1) 表示。再就是消费的时候不要过度消费,先计算出能够消费的最大距离 scrollDy,如果参数 dy 大于 scrollDy,那么就只能消费 scrollDy 并记录到 consumed[1] 中。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/283886.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号