Android嵌套滑动讲解
在Android的事件分发机制里面,当一个View决定消耗事件流时,其它的View就不能再解决这个事件流的了,所以对于有嵌套滑动的地方就要用到NestedScrollingParent和NestedScrollingChild。最新的是NestedScrollingParent2和NestedScrollingChild2是在NestedScrollingParent和NestedScrollingChild的基础上扩展的,相对于旧版本他们对嵌套监听提供了触摸类型的区分,使得fling也可以进行嵌套滚动。要想实现嵌套滑动这两个接口必需成对出现。
组成
已知实现了NestedScrollingParent的ViewGroup有NestedScrollView、CoordinatorLayout、SwipeRefreshLayout
等。已知实现了NestedScrollingChild的接口有BaseGridView、HorizontalGridView、NestedScrollView、RecyclerView、SwipeRefreshLayout、VerticalGridView
。所以NestedScrollView、SwipeRefreshLayout
既实现类Parent接口也实现类Child接口。
先理解一下Parent和Child接口的组成
public interface NestedScrollingParent2 extends NestedScrollingParent { /** * 这个是嵌套滑动控制事件分发的控制方法,只有返回true才能接收到事件分发 * @param child 包含target的ViewParent的直接子View * @param target 发起滑动事件的View * @param axes 滑动的方向,数值和水平方向{@link ViewCompat#SCROLL_AXIS_HORIZONTAL}, * {@link ViewCompat#SCROLL_AXIS_VERTICAL} * @return true 表示父View接受嵌套滑动监听,否则不接受 */ boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes,@NestedScrollType int type); /** * 这个方法在onStartNestedScroll返回true之后在正式滑动之前回调 * @param child 包含target的父View的直接子View * @param target 发起嵌套滑动的View * @param axes 滑动的方向,数值和水平方向{@link ViewCompat#SCROLL_AXIS_HORIZONTAL}, * {@link ViewCompat#SCROLL_AXIS_VERTICAL} or both */ void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes,@NestedScrollType int type); /** * * @param target View that initiated the nested scroll */ void onStopNestedScroll(@NonNull View target); /** * 在子View滑动过程中会分发这个嵌套滑动的方法,要想这里收到嵌套滑动事件必需在onStartNestedScroll返回true * @param dxConsumed 子View在水平方向已经消耗的距离 * @param dyConsumed 子View在垂直方法已经消耗的距离 * @param dxUnconsumed 子View在水平方向剩下的未消耗的距离 * @param dyUnconsumed 子View在垂直方法剩下的未消耗的距离 * @param type 发起嵌套事件的类型 分为触摸(ViewParent.TYPE_TOUCH)和非触摸(ViewParent.TYPE_NON_TOUCH) */ void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type); /** * 在子View开始滑动之前让父View有机会先进行滑动解决 * @param dx 水平方向将要滑动的距离 * @param dy 竖直方向将要滑动的距离 * @param consumed Output. 父View在水平和垂直方向要消费的距离,consumed[0]表示水平方向的消耗,consumed[1]表示垂直方向的消耗, */ void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, @NestedScrollType int type);}
以上就是NestedScrollingParent2主要的方法详情,下面看看NestedScrollingChild2的方法
public interface NestedScrollingChild2 extends NestedScrollingChild { //返回值true表示找到了嵌套交互的ViewParent,type表示引起滑动事件的类型,这个事件和parent中的onStartNestedScroll是对应的 boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type); //中止嵌套滑动的回调 void stopNestedScroll(@NestedScrollType int type); //表示有实现了NestedScrollingParent2接口的父类 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);}
嵌套滑动的原理
下面给出的是NestedScrollView中的onTouchEvent方法的源码,整个嵌套滑动事件和View的事件分发是结合在一起的,相对于在原来view的事件分发里面加了滑动回调给父类,并且把滑动的距离算出来。这样一看就很清晰了。
@Override public boolean onTouchEvent(MotionEvent ev) { ... switch (actionMasked) { case MotionEvent.ACTION_DOWN: { ... //嵌套滑动的原理是滑动事件先从子View开始,在子View接收到ACTION_DOWN事件的时候开始寻觅能否有嵌套滑动的父类并且回调onStartNestedScroll方法 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH); break; } case MotionEvent.ACTION_MOVE: ... //在ACTION_MOVE开始时先分发嵌套滑动之前的事件,最后回调onNestedPreScroll方法 if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset, ViewCompat.TYPE_TOUCH)) { deltaY -= mScrollConsumed[1]; vtev.offsetLocation(0, mScrollOffset[1]); mNestedYOffset += mScrollOffset[1]; } if (mIsBeingDragged) { ... //滑动过程中回调onNestedScroll方法 if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset, ViewCompat.TYPE_TOUCH)) { mLastMotionY -= mScrollOffset[1]; vtev.offsetLocation(0, mScrollOffset[1]); mNestedYOffset += mScrollOffset[1]; } } break; case MotionEvent.ACTION_UP: ... int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); if ((Math.abs(initialVelocity) > mMinimumVelocity)) { //松开手回调fling类型的onNestedScroll方法 flingWithNestedDispatch(-initialVelocity); } //回调stopNestedScroll方法 endDrag(); break; case MotionEvent.ACTION_CANCEL: ... //回调stopNestedScroll方法 endDrag(); break; } ... vtev.recycle(); return true; }
NestedScrollingChildHelper和NestedScrollingParentHelper
这两个是实际解决嵌套逻辑的代理商类,谷歌把嵌套滑动的逻辑已经封装在里面,在需要的地方实现类逻辑的复用,这样设计的好处是避免和onTouchEvent里面事件解决逻辑的耦合,让逻辑更加清晰,调用方便,并且helper里面的方法和接口的方法是逐个对应方法名相同的。这种设计模式值得我们在代码里面学习和使用。我们可以看看子View是怎样样找到有嵌套监听等父类的,我们以onTouchEvent里面startNestedScroll方法,最终来到了NestedScrollingChildHelper里面的startNestedScroll方法
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) { //已经在嵌套滑动过程中 if (hasNestedScrollingParent(type)) { // Already in progress return true; } //首先检查能否可以嵌套滑动,由于像recyclerview中是可以关闭嵌套滑动的 if (isNestedScrollingEnabled()) { ViewParent p = mView.getParent(); View child = mView; //遍历寻觅实现了NestedScrollingParent接口的父类 while (p != null) { //调用父类的onStartNestedScroll方法,假如返回ture则告诉父类的onNestedScrollAccepted方法已经已经收到了父类的请求 if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) { setNestedScrollingParentForType(type, p); ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type); return true; } if (p instanceof View) { child = (View) p; } p = p.getParent(); } } return false; }
对NestedScrollingParent2和NestedScrollingParent进行区分
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 return IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes); } return false; }
实例
这里截取了知乎首页内容滚动和tab联动的例子,当然这个实现的方法有很多,这里用嵌套滑动的方法去实现。逻辑是内容部分是一个实现了NestedScrollingChild2的RecyclerView,当然这部分不用我们去做,RecyclerView原本就实现了,父类是一个实现了NestedScrollingParent2接口的ViewGroup,这里可以用FrameLayout。当RecyclerView滚动时,通知FrameLayout的实现下方Tab栏的滚动逻辑,就这么简单。
image
首先自己设置ViewGroup
public class NestedFrameLayout extends FrameLayout implements NestedScrollingParent2 { public NestedFrameLayout(Context context) { super(context); } public NestedFrameLayout(Context context, AttributeSet attrs) { super(context, attrs); } public NestedFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public boolean onStartNestedScroll(@NonNull View view, @NonNull View view1, int i, int i1) { return true; } @Override public void onNestedScrollAccepted(@NonNull View view, @NonNull View view1, int i, int i1) { } @Override public void onStopNestedScroll(@NonNull View view, int i) { } @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) { if (mListener != null) { if (Math.abs(dyConsumed)>5){ if (dyConsumed>0){ mListener.onScroll(true); }else { mListener.onScroll(false); } } } } @Override public void onNestedPreScroll(@NonNull View view, int i, int i1, @NonNull int[] ints, int i2) { } public Listener mListener; public void setListener(Listener listener) { mListener = listener; } public interface Listener { void onScroll(boolean isScrollUp); }}
而后在MainActivity监听这个滑动事件和解决tab的联动,并且加上动画就看到了下图的效果了
@Override public void onScroll(boolean isScrollUp) { if (isScrollUp) { hideBottomNavigationBar(); } else { showBottomNavigationBar(); } } private void showBottomNavigationBar() { if (!mIsNavigationBarHide) { animateOffset(mBottomTabLayout.getHeight()); mIsNavigationBarHide = true; } } private void hideBottomNavigationBar() { animateOffset(0); mIsNavigationBarHide = false; } private void animateOffset(final int offset) { if (mTranslationAnimator == null) { mTranslationAnimator = ViewCompat.animate(mBottomTabLayout); mTranslationAnimator.setDuration(300); mTranslationAnimator.setInterpolator(new LinearOutSlowInInterpolator()); mTranslationAnimator.setUpdateListener(new ViewPropertyAnimatorUpdateListener() { @Override public void onAnimationUpdate(View view) { } }); } else { mTranslationAnimator.cancel(); } mTranslationAnimator.translationY(offset).start(); }
这里面即可以做到上方无论切到哪个tab滑动时,下方的tab都能联动,由于从上面NestedScrollingChildHelper的分析可知RecyclerView会遍历寻觅想要监听嵌套滑动的ViewGroup。这样就做到了全局联动。
image
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » Android嵌套滑动讲解