Android深入探究自定義View之嵌套滑動的實現(xiàn)
本文主要探討以下幾個問題:
- 嵌套滑動設計目的
- 嵌套滑動的實現(xiàn)
- 嵌套滑動與事件分發(fā)機制
嵌套滑動設計目的
不知道大家有沒有注意過淘寶APP首頁的二級聯(lián)動,滑動的商品的時候上面類別也會滑動,滑動過程中類別模塊停了商品還能繼續(xù)滑動。也就是說滑動的是view,ViewGroup也會跟著滑動。如果用事件分發(fā)機制處理也能處理,但會及其麻煩。那用NestedScroll會咋樣?
嵌套滑動的實現(xiàn)
假設布局如下
RecyclerView 實現(xiàn)了 NestedScrollingChild 接口,NestedScrollView 實現(xiàn)了 NestedScrollingParent,這是實現(xiàn)嵌套布局的基礎
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2, NestedScrollingChild3 public class NestedScrollView extends FrameLayout implements NestedScrollingParent3, NestedScrollingChild3, ScrollingView
滑動屏幕時 RecyclerView 收到滑動事件,在 ACTION_DOWN 時
// RecyclerView.java onTouchEvent函數(shù) 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;
繼續(xù)深入
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,然后回調(diào) onStartNestedScroll 和 onNestedScrollAccepted 。onStartNestedScroll 決定了當前控件是否能接收到其內(nèi)部View(非并非是直接子View)滑動時的參數(shù);按下時確定其嵌套的父布局以及是否能收到后續(xù)事件。再看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 中調(diào)用了 dispatchNestedPreScroll 。dispatchNestedPreScroll 中會回調(diào) onNestedPreScroll 方法,內(nèi)部的 scrollByInternal 中還會回調(diào) onNestedScroll 方法
整個流程如下
onNestedPreScroll中,我們判斷,如果是上滑且頂部控件未完全隱藏,則消耗掉dy,即consumed[1]=dy;如果是下滑且內(nèi)部View已經(jīng)無法繼續(xù)下拉,則消耗掉dy,即consumed[1]=dy,消耗掉的意思,就是自己去執(zhí)行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); // 這個是被消費的距離,如果沒有會被重復消費現(xiàn)象是父布局與子布局同時滑動,滑動的距離被消費兩次 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) { // 調(diào)整contentView的高度為父容器高度,使之填充布局,避免父容器滾動后出現(xiàn)空白 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); // 這個是被消費的距離,如果沒有會被重復消費,現(xiàn)象是父布局與子布局同時滑動 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; } }
嵌套滑動與事件分發(fā)機制
- 事件分發(fā)機制:子View首先得到事件處理權,處理過程中父View可以對其攔截,但是攔截了以后就無法再還給子View(本次手勢內(nèi))。
- NestedScrolling 滑動機制:內(nèi)部View在滾動的時候,首先將dx,dy交給NestedScrollingParent,NestedScrollingParent可對其進行部分消耗,剩余的部分還給內(nèi)部View。
總結:嵌套布局要注意的有幾個方面
- ACTION_DOWN 時子view調(diào)用父布局的onStartNestedScroll,根據(jù)滑動方向判斷父布局是否要收到子view的滑動參數(shù)
- ACTION_MOVE時子view調(diào)用父布局的onNestedPreScroll函數(shù),父布局是否要滑動已經(jīng)消費掉自身需要的距離
- ACTION_UP時,手指抬起可能還有加速度,調(diào)用父布局的onPreFling判斷是否需要消費以及消費剩下的再傳給子布局
到此這篇關于Android深入探究自定義View之嵌套滑動的實現(xiàn)的文章就介紹到這了,更多相關Android 嵌套滑動內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
android實現(xiàn)raw文件夾導入數(shù)據(jù)庫代碼
這篇文章主要介紹了android實現(xiàn)raw文件夾導入數(shù)據(jù)庫代碼,有需要的朋友可以參考一下2013-12-12Android編程實現(xiàn)檢測當前電源狀態(tài)的方法
這篇文章主要介紹了Android編程實現(xiàn)檢測當前電源狀態(tài)的方法,涉及Android針對當前電源的電量、容量、伏數(shù)、溫度等的檢測技巧,非常簡單實用,需要的朋友可以參考下2015-11-11Android 10 啟動之servicemanager源碼解析
這篇文章主要為大家介紹了Android 10 啟動之servicemanager源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-10-10Flutter如何輕松實現(xiàn)動態(tài)更新ListView淺析
在Android中通常都會用到listview.那么flutter里面怎么用呢?下面這篇文章主要給大家介紹了關于Flutter如何輕松實現(xiàn)動態(tài)更新ListView的相關資料,需要的朋友可以參考下2022-02-02