本文主要探討以下幾個問題:
- 嵌套滑動設計目的
- 嵌套滑動的實作
- 嵌套滑動與事件分發機制
嵌套滑動設計目的
不知道大家有沒有注意過淘寶APP首頁的二級聯動,滑動的商品的時候上面類別也會滑動,滑動程序中類別模塊停了商品還能繼續滑動,也就是說滑動的是view,ViewGroup也會跟著滑動,如果用事件分發機制處理也能處理,但會及其麻煩,那用NestedScroll會咋樣?
嵌套滑動的實作
假設布局如下

RecyclerView 實作了 NestedScrollingChild 介面,NestedScrollView 實作了 NestedScrollingParent,這是實作嵌套布局的基礎
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2, NestedScrollingChild3
public class NestedScrollView extends FrameLayout implements NestedScrollingParent3, NestedScrollingChild3, ScrollingView
滑動螢屏時 RecyclerView 收到滑動事件,在 ACTION_DOWN 時
// RecyclerView.java onTouchEvent函式
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;
繼續深入
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
if (hasNestedScrollingParent(type)) {
// Already in progress
return true;
}
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;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
遞回尋找NestedScrollingParent,然后回呼 onStartNestedScroll 和 onNestedScrollAccepted ,onStartNestedScroll 決定了當前控制元件是否能接收到其內部View(非并非是直接子View)滑動時的引數;按下時確定其嵌套的父布局以及是否能收到后續事件,再看ACTION_MOVE事件
case MotionEvent.ACTION_MOVE: {
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
dx -= mScrollConsumed[0];
dy -= mScrollConsumed[1];
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
}
} break;
ACTION_MOVE 中呼叫了 dispatchNestedPreScroll ,dispatchNestedPreScroll 中會回呼 onNestedPreScroll 方法,內部的 scrollByInternal 中還會回呼 onNestedScroll 方法
整個流程如下

onNestedPreScroll中,我們判斷,如果是上滑且頂部控制元件未完全隱藏,則消耗掉dy,即consumed[1]=dy;如果是下滑且內部View已經無法繼續下拉,則消耗掉dy,即consumed[1]=dy,消耗掉的意思,就是自己去執行scrollBy,實際上就是我們的NestedScrollView 滑動,
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
// 向上滑動,若當前topview可見,需要將topview滑動至不可見
boolean hideTop = dy > 0 && getScrollY() < topView.getMeasuredHeight();
if (hideTop) {
scrollBy(0, dy);
// 這個是被消費的距離,如果沒有會被重復消費現象是父布局與子布局同時滑動,滑動的距離被消費兩次
consumed[1] = dy;
}
}
整體代碼如下
public class NestedScrollLayout extends NestedScrollView {
private View topView;
private ViewGroup contentView;
private static final String TAG = "NestedScrollLayout";
public NestedScrollLayout(Context context) {
this(context, null);
init();
}
public NestedScrollLayout(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
init();
}
public NestedScrollLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
init();
}
public NestedScrollLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr);
init();
}
private FlingHelper mFlingHelper;
int totalDy = 0;
/**
* 用于判斷RecyclerView是否在fling
*/
boolean isStartFling = false;
/**
* 記錄當前滑動的y軸加速度
*/
private int velocityY = 0;
private void init() {
mFlingHelper = new FlingHelper(getContext());
setOnScrollChangeListener(new View.OnScrollChangeListener() {
@Override
public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
if (isStartFling) {
totalDy = 0;
isStartFling = false;
}
if (scrollY == 0) {
Log.e(TAG, "TOP SCROLL");
// refreshLayout.setEnabled(true);
}
if (scrollY == (getChildAt(0).getMeasuredHeight() - v.getMeasuredHeight())) {
Log.e(TAG, "BOTTOM SCROLL");
dispatchChildFling();
}
//在RecyclerView fling情況下,記錄當前RecyclerView在y軸的偏移
totalDy += scrollY - oldScrollY;
}
});
}
private void dispatchChildFling() {
if (velocityY != 0) {
Double splineFlingDistance = mFlingHelper.getSplineFlingDistance(velocityY);
if (splineFlingDistance > totalDy) {
childFling(mFlingHelper.getVelocityByDistance(splineFlingDistance - Double.valueOf(totalDy)));
}
}
totalDy = 0;
velocityY = 0;
}
private void childFling(int velY) {
RecyclerView childRecyclerView = getChildRecyclerView(contentView);
if (childRecyclerView != null) {
childRecyclerView.fling(0, velY);
}
}
@Override
public void fling(int velocityY) {
super.fling(velocityY);
if (velocityY <= 0) {
this.velocityY = 0;
} else {
isStartFling = true;
this.velocityY = velocityY;
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
topView = ((ViewGroup) getChildAt(0)).getChildAt(0);
contentView = (ViewGroup) ((ViewGroup) getChildAt(0)).getChildAt(1);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 調整contentView的高度為父容器高度,使之填充布局,避免父容器滾動后出現空白
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
ViewGroup.LayoutParams lp = contentView.getLayoutParams();
lp.height = getMeasuredHeight();
contentView.setLayoutParams(lp);
}
/**
* 解決滑動沖突:RecyclerView在滑動之前會問下父布局是否需要攔截,父布局使用此方法
*/
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
Log.e("NestedScrollLayout", getScrollY()+"::onNestedPreScroll::"+topView.getMeasuredHeight()+"::dy::"+dy);
// 向上滑動,若當前topview可見,需要將topview滑動至不可見
boolean hideTop = dy > 0 && getScrollY() < topView.getMeasuredHeight();
if (hideTop) {
scrollBy(0, dy);
// 這個是被消費的距離,如果沒有會被重復消費,現象是父布局與子布局同時滑動
consumed[1] = dy;
}
}
private RecyclerView getChildRecyclerView(ViewGroup viewGroup) {
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View view = viewGroup.getChildAt(i);
if (view instanceof RecyclerView && view.getClass() == NestedLogRecyclerView.class) {
return (RecyclerView) viewGroup.getChildAt(i);
} else if (viewGroup.getChildAt(i) instanceof ViewGroup) {
ViewGroup childRecyclerView = getChildRecyclerView((ViewGroup) viewGroup.getChildAt(i));
if (childRecyclerView instanceof RecyclerView) {
return (RecyclerView) childRecyclerView;
}
}
continue;
}
return null;
}
}
嵌套滑動與事件分發機制
- 事件分發機制:子View首先得到事件處理權,處理程序中父View可以對其攔截,但是攔截了以后就無法再還給子View(本次手勢內),
- NestedScrolling 滑動機制:內部View在滾動的時候,首先將dx,dy交給NestedScrollingParent,NestedScrollingParent可對其進行部分消耗,剩余的部分還給內部View,
總結:嵌套布局要注意的有幾個方面
- ACTION_DOWN 時子view呼叫父布局的onStartNestedScroll,根據滑動方向判斷父布局是否要收到子view的滑動引數
- ACTION_MOVE時子view呼叫父布局的onNestedPreScroll函式,父布局是否要滑動已經消費掉自身需要的距離
- ACTION_UP時,手指抬起可能還有加速度,呼叫父布局的onPreFling判斷是否需要消費以及消費剩下的再傳給子布局
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/276214.html
標籤:其他
上一篇:SOAP檔案的讀寫
