當父View及子View均可以滑動,而且滑動方向一致時(例如CoordinatorLayout內嵌RecyclerView或者Webview),滑動衝突的解決就須要依賴於Android爲咱們提供的NestedScrolling接口。java
NestedScrolling 接口分爲兩個部分:NestedScrollingParent
及 NestedScrollingChild
。ide
爲方便描述,如下簡稱NestedScrollingParent
爲NP
, NestedScrollingChild
爲NC
。post
包含的接口:this
public interface NestedScrollingChild {
/** * 設置當前View是否啓用nested scroll特性 * @param enabled 是否啓用 */
void setNestedScrollingEnabled(boolean enabled);
/** * 當前View是否啓用了nested scroll特性 * @return */
boolean isNestedScrollingEnabled();
/** * 在axes軸上發起nested scroll開始操做 * @param axes 滑動方向 * @return 是否有NestedScrollingParent接受這次滑動請求 */
boolean startNestedScroll(@ViewCompat.ScrollAxis int axes);
/** * 終止nested scroll */
void stopNestedScroll();
/** * 當前是否有NestedScrollingParent接受了這次滑動請求 * @return 返回值 */
boolean hasNestedScrollingParent();
/** * nested scroll的一步滑動操做中,在本身開始滑動處理以前,分配預處理操做(通常爲詢問NestedScrollingParent是否消耗部分滑動距離) * @param dx 當前這一步滑動的x軸總距離 * @param dy 當前這一步滑動的y軸總距離 * @param consumed 預處理操做消耗掉的距離(此爲輸出參數,consumed[0]爲預處理操做消耗掉的x軸距離,consumed[1]爲預處理操做消耗掉的y軸距離) * @param offsetInWindow 可選參數,能夠爲null。爲輸出參數,獲取預處理操做使當前view的位置偏移(offsetInWindow[0]和offsetInWindow[1]分別爲x軸和y軸偏移) * @return 預處理操做是否消耗了部分或者所有滑動距離 */
boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow);
/** * 在當前View處理了滑動以後繼續分配滑動操做 (通常在本身處理滑動以後,給NestedScrollingParent機會處理剩餘的滑動距離) * @param dxConsumed 已經消耗了的x軸滑動距離 * @param dyConsumed 已經消耗了的y軸滑動距離 * @param dxUnconsumed 未消耗的x軸滑動距離 * @param dyUnconsumed 未消耗的y軸滑動距離 * @param offsetInWindow 可選參數,能夠爲null。爲輸出參數,獲取預處理操做使當前view的位置偏移(offsetInWindow[0]和offsetInWindow[1]分別爲x軸和y軸偏移) * @return */
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow);
/** * 在當前NestedScrollingChild處理fling事件以前進行預處理(通常詢問NestedScrollingParent是否處理消耗這次fling) * @param velocityX x軸速度 * @param velocityY y軸速度 * @return 預處理是否處理消耗了這次fling */
boolean dispatchNestedPreFling(float velocityX, float velocityY);
/** * 分配fling操做 * @param velocityX x軸方向速度 * @param velocityY y軸方向速度 * @param consumed 當前NestedScrollingChild是否處理了這次fling * @return NestedScrollingParent是否處理了這次fling */
boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
}
複製代碼
NC
是NP
的子孫(並不是必定是直接子View),也是聯合滑動的請求方,滑動產生的一系列MotionEvent
是在此View中跟蹤處理的,通常此View是在 onTouchEvent
中依據對 MotionEvent
的跟蹤分析來發起滑動請求。例如如下 RecyclerView
中 onTouchEvent
的簡化版本:spa
@Override
public boolean onTouchEvent(MotionEvent e) {
final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
final boolean canScrollVertically = mLayout.canScrollVertically();
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
boolean eventAddedToVelocityTracker = false;
final MotionEvent vtev = MotionEvent.obtain(e);
final int action = e.getActionMasked();
final int actionIndex = e.getActionIndex();
if (action == MotionEvent.ACTION_DOWN) {
mNestedOffsets[0] = mNestedOffsets[1] = 0;
}
vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);
switch (action) {
case MotionEvent.ACTION_DOWN: {
mScrollPointerId = e.getPointerId(0);
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
// 發起滾動請求
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
} break;
case MotionEvent.ACTION_MOVE: {
final int x = (int) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
// 先詢問 NP 是否須要提早消耗滑動距離(部分或者所有)
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {
// NP消耗了部分滑動距離
dx -= mScrollConsumed[0]; // NP 消耗的X軸滑動距離
dy -= mScrollConsumed[1]; // NP消耗的Y軸滑動距離
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
//分析是否自己須要滑動及自己滑動所消耗的滑動距離
if (mScrollState != SCROLL_STATE_DRAGGING) {
boolean startScroll = false;
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
if (dx > 0) {
dx -= mTouchSlop;
} else {
dx += mTouchSlop;
}
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
if (dy > 0) {
dy -= mTouchSlop;
} else {
dy += mTouchSlop;
}
startScroll = true;
}
if (startScroll) {
setScrollState(SCROLL_STATE_DRAGGING);
}
}
if (mScrollState == SCROLL_STATE_DRAGGING) {
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
// 本身內部滑動
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
} break;
case MotionEvent.ACTION_POINTER_UP: {
onPointerUp(e);
} break;
case MotionEvent.ACTION_UP: {
mVelocityTracker.addMovement(vtev);
eventAddedToVelocityTracker = true;
mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
final float xvel = canScrollHorizontally
? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
final float yvel = canScrollVertically
? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
// 分析是否產生fling事件(手機快速滑動以後擡起,視圖繼續滑動)
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
resetTouch();
} break;
case MotionEvent.ACTION_CANCEL: {
cancelTouch();
} break;
}
if (!eventAddedToVelocityTracker) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return true;
}
複製代碼
及 scrollByInternal
簡化版:code
boolean scrollByInternal(int x, int y, MotionEvent ev) {
int unconsumedX = 0, unconsumedY = 0;
int consumedX = 0, consumedY = 0;
if (mAdapter != null) {
if (x != 0) {
// 本身滑動消耗的X軸滑動距離
consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
//還沒有消耗的X軸滑動距離
unconsumedX = x - consumedX;
}
if (y != 0) {
// 本身滑動消耗的Y軸滑動距離
consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
//還沒有消耗的Y軸滑動距離
unconsumedY = y - consumedY;
}
}
// 通知 NP 繼續消耗剩餘的滑動距離
if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
TYPE_TOUCH)) {
// Update the last touch co-ords, taking any scroll offset into account
mLastTouchX -= mScrollOffset[0];
mLastTouchY -= mScrollOffset[1];
if (ev != null) {
ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
}
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
// 滑動距離是否已經徹底消耗
return consumedX != 0 || consumedY != 0;
}
複製代碼
因此針對一次滑動操做,NC
的接口調用順序爲:接口
startNestedScroll
-> dispatchNestedPreScroll
-> dispatchNestedScroll
-> stopNestedScroll
事件
通常性的處理邏輯能夠用如下僞代碼總結:ci
private int mLastX;
private int mLastY;
private int[] mConsumed = new int[2];
private int[] mOffsetInWindow = new int[2];
@Override
void onTouchEvent(MotionEvent event) {
int eventX = (int) event.getRawX();
int eventY = (int) event.getRawY();
int action = event.getAction();
int deltaX = eventX - mLastX;
int deltaY = eventY - mLastY;
switch (action) {
case MotionEvent.ACTION_DOWN:
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally()) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically()) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis);
break;
case MotionEvent.ACTION_MOVE:
if (dispatchNestedPreScroll(deltaX, deltaY, mConsumed, mOffsetInWindow)) {
deltaX -= mConsumed[0];
deltaY -= mConsumed[1];
}
int internalScrolledX = internalScrollByX(deltaX);
int internalScrolledY = internalScrollByY(deltaY);
deltaX -= internalScrolledX;
deltaY -= internalScrolledY;
if (deltaX != 0 || deltaY != 0) {
dispatchNestedScroll(mConsumed[0] + internalScrolledX, mConsumed[1] + internalScrolledY, deltaX, deltaY, mOffsetInWindow);
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
stopNestedScroll();
break;
}
mLastX = eventX;
mLastY = eventY;
}
/** * X軸方向滑動 * @param deltaX 滑動距離 * @return 消耗的滑動距離 */
abstract int internalScrollByX(int deltaX);
/** * Y軸方向滑動 * @param deltaY 滑動距離 * @return 消耗的滑動距離 */
abstract int internalScrollByY(int deltaY);
/** * 是否支持橫向滑動 * @return 是否支持 */
abstract boolean canScrollHorizontally();
/** * 是否支持豎向滑動 * @return 是否支持 */
abstract boolean canScrollVertically();
複製代碼
包含接口:get
public interface NestedScrollingParent {
/** * 對NP子孫開始滑動請求的迴應(NestedScrollingChild.startNestedScroll) * @param child 包含發起請求的NP子孫view的直接子view * @param target 發起請求的NP子孫view * @param axes 滑動方向 * @return 是否響應此滑動事件 */
boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ViewCompat.ScrollAxis int axes);
/** * 對開始滑動響應的回調(onStartNestedScroll返回true以後會有此回調產生),使NestedScrollingParent有作滑動初始化工做的時機 * @param child 包含發起請求的NP子孫view的直接子view * @param target 發起請求的NP子孫view * @param axes 滑動方向 */
void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ViewCompat.ScrollAxis int axes);
/** * 終止nested scroll的回調(NestedScrollingChild調用stopNestedScroll) * @param target 發起請求的NP子孫view */
void onStopNestedScroll(@NonNull View target);
/** * 在NestedScrollingChild處理滑動以前,預處理此滑動 * @param target 發起請求的NP子孫view * @param dx x軸滑動距離 * @param dy y軸滑動距離 * @param consumed 回填參數,填入這次預處理消耗掉的滑動距離 */
void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);
/** * 處理NestedScrollingChild未消耗完的滑動距離 * @param target 發起請求的NP子孫view * @param dxConsumed 已消耗的x軸滑動距離 * @param dyConsumed 已消耗的y軸滑動距離 * @param dxUnconsumed 未消耗的x軸滑動距離 * @param dyUnconsumed 未消耗的y軸滑動距離 */
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
/** * 在NestedScrollingChild以前預處理fling事件 * @param target 發起請求的NP子孫view * @param velocityX x軸fling速度 * @param velocityY y軸fling速度 * @return 是否處理此fling */
boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY);
/** * 處理fling事件 * @param target 發起請求的NP子孫view * @param velocityX x軸fling速度 * @param velocityY y軸fling速度 * @param consumed NestedScrollingChild是否已處理此fling * @return 是否處理此fling */
boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed);
/** * 獲取滑動方向 * @return 滑動方向 */
@ViewCompat.ScrollAxis int getNestedScrollAxes();
}
複製代碼