NestedScrolling详解
来源:互联网 发布:数据汇集平台 编辑:程序博客网 时间:2024/06/05 12:02
简介
假设我们需要一个这样的效果,拖动子View的时候需要parent先滑动,等parent滑倒顶端的时候再让子View滑动。Android事件分发机制在parent处理事件的时候,没法再次把事件传递给子View(除非再来一个Down,开启一个新的事件序列),所以就需要用到NestedScrolling,也就是嵌套滑动机制。今天我们来实现如下效果
蓝色部分是子View,粉色是Parent,在向上滑动时,保证Parent首先滑动到顶端,向下滑动时保证子View首先滑倒底部。
基本类和方法
这里需要用到两个接口
NestedScrollingChildNestedScrollingParent
和两个辅助类
NestedScrollingChildHelperNestedScrollingParentHelper
NestedScrollingChild
子View实现这个接口
public void setNestedScrollingEnabled(boolean enabled); public boolean isNestedScrollingEnabled(); public boolean startNestedScroll(int axes); public void stopNestedScroll(); public boolean hasNestedScrollingParent(); public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow); public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow); public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed); public boolean dispatchNestedPreFling(float velocityX, float velocityY);
- void setNestedScrollingEnabled(boolean enabled):允许嵌套滑动
- boolean startNestedScroll(int axes):一般在ACTION_DOWN的事件里调用,表示要开始滑动,axes代表方向,有SCROLL_AXIS_VERTICAL、SCROLL_AXIS_HORIZONTAL两种
- boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow):一般在ACTION_MOVE种调用,dx、dy是将要滑动的量,然后分发给Parent让他消耗,consumed是一个二维数组,分别存储Parent消耗的x、y方向上的量,如果无消耗那么返回false。
NestedScrollingParent
Parent实现这个接口
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();
- void onNestedPreScroll(View target, int dx, int dy, int[] consumed):子View调用dispatchNestedPreScroll的时候此方法会被回调,通过判断dx、dy来计算消耗,返回消耗值。
然而真正的逻辑实现都由Helper类帮我们实现了,我们只需要调用helper类的对应方法即可,接下来开始写代码。
ChildView代码
/** * @author wulinpeng * @datetime: 17/6/17 下午10:34 * @description: */public class ChildView extends View implements NestedScrollingChild { private NestedScrollingChildHelper helper; private float lastY = 0; private int[] consume = new int[2]; private int[] offset = new int[2]; public ChildView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } private void init() { helper = new NestedScrollingChildHelper(this); helper.setNestedScrollingEnabled(true); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: lastY = event.getY(); // 开始垂直的滑动 helper.startNestedScroll(SCROLL_AXIS_VERTICAL); break; case MotionEvent.ACTION_MOVE: // 获得滑动量 int dy = (int) (event.getY() - lastY); if (dy < 0) { // 向上滑动的逻辑,保证parent消耗,才到自己 if (!helper.dispatchNestedPreScroll(0, (int) dy, consume, offset)) { // 运行到这说明parent不消耗了,parent已经到达顶部,这时候自身滑动 // 因为向上滑动dy < 0,所以*-1方便比较 int space = (int) getY() * -1; int consumeY = Math.max(space, dy); setY(getY() + consumeY); } } else { // 向下滑动的逻辑,保证自己消耗,才到parent int space = (int) (((ParentView) getParent()).getHeight() - getY() - getHeight()); int consumeY = Math.min(space, dy); dy -= consumeY; setY(getY() + consumeY); // 自己消耗完后,然后传给Parent剩下的dy-consumeY helper.dispatchNestedPreScroll(0, (int) dy, consume, offset); } break; case MotionEvent.ACTION_UP: break; } return true; } @Override public void setNestedScrollingEnabled(boolean enabled) { helper.setNestedScrollingEnabled(enabled); } @Override public boolean isNestedScrollingEnabled() { return helper.isNestedScrollingEnabled(); } @Override public boolean startNestedScroll(int axes) { return helper.startNestedScroll(axes); } @Override public void stopNestedScroll() { helper.stopNestedScroll(); } @Override public boolean hasNestedScrollingParent() { return helper.hasNestedScrollingParent(); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { final boolean b = helper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); return b; } @Override public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { final boolean b = helper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); return b; } @Override public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { return helper.dispatchNestedFling(velocityX, velocityY, consumed); } @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return helper.dispatchNestedPreFling(velocityX, velocityY); }}
注释比较清楚了,主要就是方向不同逻辑不同,向上的时候先分发给Parent,如果Parent不消耗了(返回false,也就是说到达顶部了),那么自己消耗dy(向上滑动,注意越界情况);向下的时候,首先自己向下滑动(自己消耗dy),然后给Parent分发消耗后的dy。
ParentView代码
/** * @author wulinpeng * @datetime: 17/6/17 下午10:37 * @description: */public class ParentView extends FrameLayout implements NestedScrollingParent { private NestedScrollingParentHelper helper; public ParentView(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } private void init() { helper = new NestedScrollingParentHelper(this); } @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { FrameLayout parent = (FrameLayout) getParent(); if (dy > 0) { // 向下滑动 int space = (int) (parent.getHeight() - getY() - getHeight()); int consumeY = Math.min(dy, space); consumed[1] = consumeY; setY(getY() + consumeY); } else { // 向上滑动 int space = (int) (getY() * -1); int consumeY = Math.max(dy, space); consumed[1] = consumeY; setY(getY() + consumeY); } } @Override public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { return true; } @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { return true; } @Override public int getNestedScrollAxes() { return helper.getNestedScrollAxes(); } @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { return true; } @Override public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { helper.onNestedScrollAccepted(child, target, nestedScrollAxes); } @Override public void onStopNestedScroll(View target) { helper.onStopNestedScroll(target); } @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { }}
比较简单,主要是注意越界的情况,接下来只要在布局文件里将ChildView设置为ParentView的child就可以了。
源码解析
但是这两者到底是怎么样联系起来的呢?我们看看Helper类的源码
public boolean startNestedScroll(int axes) { if (hasNestedScrollingParent()) { // Already in progress return true; } if (isNestedScrollingEnabled()) { ViewParent p = mView.getParent(); View child = mView; while (p != null) { if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) { mNestedScrollingParent = p; ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes); return true; } if (p instanceof View) { child = (View) p; } p = p.getParent(); } } return false; }
startNestedScroll是最开始要调用的,作用就是把这个Child和Paren联系起来,内部首先寻找可用的Parent,然后回调Parent的onStartNestedScroll方法,如果返回true,那么就给内部的mNestedScrollingParent赋值同时回调Parent的onNestedScrollAccepted方法,否则mNestedScrollingParent还是null。如果已经有了Parent那么直接返回true,可以知道这个方法调用一次就可以了,只要和Parent联系起来就ok。
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { if (dx != 0 || dy != 0) { int startX = 0; int startY = 0; if (offsetInWindow != null) { mView.getLocationInWindow(offsetInWindow); startX = offsetInWindow[0]; startY = offsetInWindow[1]; } if (consumed == null) { if (mTempNestedScrollConsumed == null) { mTempNestedScrollConsumed = new int[2]; } consumed = mTempNestedScrollConsumed; } consumed[0] = 0; consumed[1] = 0; ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed); if (offsetInWindow != null) { mView.getLocationInWindow(offsetInWindow); offsetInWindow[0] -= startX; offsetInWindow[1] -= startY; } return consumed[0] != 0 || consumed[1] != 0; } else if (offsetInWindow != null) { offsetInWindow[0] = 0; offsetInWindow[1] = 0; } } return false; }
这个方法首先判断isNestedScrollingEnabled和mNestedScrollingParent,如果mNestedScrollingParent==null也就是Parent在onStartNestedScroll返回了false,那么就不会收到这个分发。方法内部回调了Parent的onNestedPreScroll方法,然后判断consume的两个值,如果都是0,那么说明Parent没有消耗,就返回false表示Parent不消耗。
总结
其实就是NestedScrollingChild发出各种事件,比如最开始的startNestedScroll来寻找可用的Parent同时回调Parent的方法,dispatchNestedPreScroll分发偏移量给Parent让它先消耗,而NestedScrollParent只是被动接受各种回调作出处理,比如在onStartNestedScroll返回boolean表示是否接受嵌套滑动,在onNestedPreScroll消耗滑动偏移量。其实高版本的View默认实现了这些方法,但是为了兼容低版本,我们是用Helper来实现,其实实现代码是一样的。
- NestedScrolling详解
- NestedScrolling
- NestedScrolling
- 详解:Android嵌套滑动机制 (NestedScrolling)
- 详解:Android嵌套滑动机制 (NestedScrolling)
- NestedScrolling使用
- Android NestedScrolling 实战
- Android NestedScrolling 实战
- Android NestedScrolling 实战
- Android NestedScrolling 实战
- android嵌套滑动NestedScrolling
- NestedScrolling使用小记
- 嵌套滑动 NestedScrolling
- Android NestedScrolling 解析
- NestedScrolling 机制深入解析
- NestedScrolling嵌套滚动原理
- NestedScrolling机制学习(一)
- Android:NestedScrolling机制
- Python中装饰器的总结
- java中redis的使用
- Oracle那些事(8)-PL/SQL Developer安装
- XML文档基本认识和基于JAVA对简单解析
- Map四种获取key和value值的方法,以及对map中的元素排序
- NestedScrolling详解
- CAS详解
- map containsKey与get方法区别经典总结
- 树莓派控制数字舵机转动
- Android中利用APT生成代码
- Java网络编程重点总结
- mongo索引创建和索引分析
- 搜索中的记录步数
- Java高级部分--线程重点总结