標籤:配置 全域 epo reset sse nal 寫代碼 http 機制
這兩天偉大的PM下了一個需求,在一個豎滑列表裡實現一個橫向滑動的列表,沒錯,又是這種常見但是又經常被具有著強烈責任心和職業操守程式員所嗤之以鼻的效果,廢話不多說,先:
實現的方式很多,因為項目中已經ViewPager+RV實現基本架構,所以現我也選擇再添加一個RV實現相應的效果。
不過在寫代碼之前,先預估一下這個效果所有的坑。
VP是橫向滑動的,RV是豎向滑動的,那麼現在再添加一個橫向滑動的RV,肯定會有滑動衝突,主要表現在
VP和橫向滑動RV 的衝突,因為兩者都是橫向滑動的,肯定有衝突,無法判斷哪個去滑動
豎向RV和橫向RV的衝突,如果之前VP和豎向RV的衝突已經解決,那麼現在只能我自己解決了
當橫向RV滑動到最後一個item時候,應當讓VP滑動到第二頁,不能卡死在那裡。
以上就是依靠我拙劣的開發經驗所預先預估出來的坑,畢竟,滑動衝突這個字眼對安卓開發人員來說簡直太敏感了,只要滑動方向不一致,就會腦海裡展現,肯定有衝突了!
但是,這裡先預先說明一下,我們其實按照最基本的寫完就ok了,根本不用處理衝突。
WHAT??!!,准本擼起管子大幹一炮的時候,發現不用解決,雖然省心省力,但是心裡不爽啊。為啥呢,是哪裡把滑動衝突處理了,是VP還是RV呢?
一開始的時候我是趨向於RV的,因為從子View層面去處理滑動衝突更好處理一點。但是事件的分發機制是隧道傳播,冒泡處理形式,也就是說,先把事件傳給上層View,上層View如果不處理那麼傳給下層View,下層View處理,則消耗掉事件,不處理,則返回給上層處理,直至有人處理完。
既然我們並沒有複寫任何關於滑動事件的方法,我們想弄清楚原因,只能從源碼裡面找,在源碼裡打斷點,在打斷點之前,你必須對安卓的事件分發有一點點的理解,下面羅列一下事件分發常見的方法:
dispatchTouchEvent(),事件的分發,返回TRUE表示事件被消耗,不向下分發
onTouchEvent(),返回true表示事件被消耗,事實上這個的傳回值決定著dispatchTouchEvent()的傳回值,看源碼就能知道
除了上述幾個方法,ViewGroup還有一個onInterceptTouchEvent方法,表示是否攔截事件,返回true自然也是攔截了
ViewGroup預設是不攔截任何事件的,View預設是要處理所有事件的。
關於更詳細的總結,可以看一下這篇部落格 安卓事件分發機制
接下來開始打斷點,開啟VP的源碼,發現他是繼承自ViewGroup的。這就說明,他預設是不攔截任何事件的。
看到這就應該敏銳的覺察到,如果VP沒有做任何的滑動效果,那麼他就跟一個LinearLayout一樣。所有的事件都會預設下發到他的子view。但是他現在有滑動效果,那麼他是怎麼下發到RV的呢,是他沒管讓RV處理了,還是自己處理好,不讓RV接受事件呢?
打好斷點:
首先賣個關子,這個斷點打的是有問題的。
同樣的方式,開啟RV源碼,發現他也是繼承ViewGroup的,但是我在找他的OnInterceptTouchEvent()方法的時候並沒有找到他複寫該方法,看來他預設也是不攔截事件傳遞的?
既然找不到OnInterceptTouchEvent()方法,那我們就找OntouchEvent方法,找到後,打斷點如下:
這個斷點打的也有問題
順便看了一下OnTouch事件的傳回值,發現
巧了,預設返回TRUE,看來預設情況下,RV是要處理所有事件的。
所有的斷點打點完畢,開始調試,
一步一步走,發現
代碼走進了這個分支裡面,看見裡面好多變數和一些方法我就仔細調試,看值,一行行的去理解,結果調試完了才發現,這!並!沒!有!什!麼!卵!用!
如果你因此而被繁雜的原始碼繞進去,那你真的會被嚇住,你要知道RV可是有10000多行的代碼的,思考一下,為什麼我說的斷點有問題?就是因為這個。我們現在要研究的是橫向滑動為什麼沒有出現滑動衝突,事實上我們只需要在ACTION.MOVE分支打斷點就可以了,其他可以直接掠過。
*
這是我打斷點遇到的第一個坑,畢竟以前沒在源碼裡打過斷點,一看源碼這麼複雜又看不懂只能亂打一通,慢慢看,事實上,這樣只能讓你望而卻步。
*
進入到Action.Move分支,在分析這個分支代碼之前,我們必須時刻謹記我們想要達到的目標以及我們所在的方法是幹什麼的,我們現在在IntercepetTouch方法裡,這個方法是決定是否攔截事件的,返回TRUE表示攔截,返回false表示不攔截,所以,我們先不看這個分支的代碼,畢竟50多行代碼,繞進去你就沒耐心了。
先去瞅一眼傳回值,發現是個變數,mIsBeingDragged,預設是false,這和我們之前說的,ViewGroup預設不攔截所有事件不謀而合。
既然傳回值由mIsBeingDragged決定,那麼我們先全域搜尋一下該參數在哪裡賦過值,crtl+f 全域搜尋,發現除了在ActionDown事件裡和ActionMove事件裡,其他再無賦值的地方,所以,這就為我們排查提供了方便,ActionDown事件已經被我們排除,不需要去分析,只看ActionMove裡面的
case MotionEvent.ACTION_MOVE: if (!mIsBeingDragged) { final int pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex == -1) { // A child has consumed some touch events and put us into an inconsistent // state. needsInvalidate = resetTouch(); break; } final float x = ev.getX(pointerIndex); final float xDiff = Math.abs(x - mLastMotionX); final float y = ev.getY(pointerIndex); final float yDiff = Math.abs(y - mLastMotionY); if (DEBUG) { Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); } if (xDiff > mTouchSlop && xDiff > yDiff) { if (DEBUG) Log.v(TAG, "Starting drag!"); mIsBeingDragged = true; requestParentDisallowInterceptTouchEvent(true); mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop; mLastMotionY = y; setScrollState(SCROLL_STATE_DRAGGING); setScrollingCacheEnabled(true); // Disallow Parent Intercept, just in case ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } } // Not else! Note that mIsBeingDragged can be set above. if (mIsBeingDragged) { // Scroll to follow the motion event final int activePointerIndex = ev.findPointerIndex(mActivePointerId); final float x = ev.getX(activePointerIndex); needsInvalidate |= performDrag(x); } break;
代碼還是蠻多的,但是我們只需要分析給mIsBeingDragged賦值以及有傳回值的地方就可以了,所以其實核心就這兩段代碼,
if (dx != 0 && !isGutterDrag(mLastMotionX, dx) && canScroll(this, false, (int) dx, (int) x, (int) y)) { // Nested view has scrollable area under this point. Let it be handled there. mLastMotionX = x; mLastMotionY = y; mIsUnableToDrag = true; return false; }
和
if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) { if (DEBUG) Log.v(TAG, "Starting drag!"); mIsBeingDragged = true; requestParentDisallowInterceptTouchEvent(true); setScrollState(SCROLL_STATE_DRAGGING); mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop; mLastMotionY = y; setScrollingCacheEnabled(true); }
結合我們現在出現的情況,ViewPager嵌套RV沒有出現滑動衝突,肯定是返回了false,所以,第二段代碼其實也可以廢除掉,不分析的,但是我為了裝逼還是要說一下,if條件就是在判斷滑動的方向,x方向的滑動距離如果大於最小的滑動距離,並且x方向滑動距離的0.53若大於y方向距離,就判定為橫向滑動,那麼就攔截該事件,mIsBeingDragged置為True ,但是這還不夠,該方法讓然調用了 requestParentDisallowInterceptTouchEvent(true);方法,這個方法就是告訴父控制項,我要開始裝逼了,你不要管我,剩下的事情我來處理就好了,你甭管。我個人理解就是雙保險吧,至於其他代碼,不用分析了,因為和我們這次的分析無關。這段代碼也就是攔截了橫向滑動事件,畢竟VP就是幹這個事情的。
所以接下來就很清楚了,問題肯定出現在第一段代碼裡,判斷語句有問題 if (dx != 0 && !isGutterDrag(mLastMotionX, dx) && canScroll(this, false, (int) dx, (int) x, (int) y))
dx!=0不用說了,我們去分析一下isGutterDrag(mLastMotionX, dx) 和canScroll(this, false, (int) dx, (int) x, (int) y)) ,這裡就不做具體分析了,isGutterDrag(mLastMotionX, dx)是判斷是否在兩個頁面之間的縫隙內移動的,所以肯定返回false,那麼一定是這個canScroll(this, false, (int) dx, (int) x, (int) y)返回了true。進源碼看一眼
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { if (v instanceof ViewGroup) { final ViewGroup group = (ViewGroup) v; final int scrollX = v.getScrollX(); final int scrollY = v.getScrollY(); final int count = group.getChildCount(); // Count backwards - let topmost views consume scroll distance first. for (int i = count - 1; i >= 0; i--) { // TODO: Add versioned support here for transformed views. // This will not work for transformed views in Honeycomb+ final View child = group.getChildAt(i); if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && canScroll(child, true, dx, x + scrollX - child.getLeft(), y + scrollY - child.getTop())) { return true; } } } return checkV && ViewCompat.canScrollHorizontally(v, -dx); }
看著挺嚇人,乍一眼不知道是幹嘛用的,還用到了遞迴。但是我們看見裡面有getChildCount這個函數,說明肯定跟子view 是相關的,所以呢,這個肯定是來判斷VP裡的子view的,名字叫canScroll,大致就可以猜到,這個函數是用來判斷子View是不是可以滾動的,看note也就能知道:
** * Tests scrollability within child views of v given a delta of dx. * * @param v View to test for horizontal scrollability * @param checkV Whether the view v passed should itself be checked for scrollability (true), * or just its children (false). * @param dx Delta scrolled in pixels * @param x X coordinate of the active touch point * @param y Y coordinate of the active touch point * @return true if child views of v can be scrolled by delta of dx. */
return true if child views of v can be scrolled by delta of dx,說的很清楚了,如果子view可以滾動就會返回True,VP裡的RV是個子view,並且可以橫向滾動,自然就走進該分支,切IntercpetOntouch返回false,不攔截,交給子View處理,子View該咋處理就咋處理,不做分析了
到這裡也就對為啥VP嵌套RV不會出現事件衝突有了一個大概的瞭解,總結一下就是
VP預設不會攔截事件
VP會攔截橫向滑動事件,這是他的本能,但是這段代碼之前,他又幹了其他事情,就是判斷他的子View是否能滾動,能滾動的話,是不會攔截Move事件的。
VP嵌套VP也不會出現滾動衝突,原因就是上面兩條,不知道為啥網上會有VP套VP的滑動衝突,不理解,我自己寫代碼沒發現。
至於RV的事件衝突,不做分析了,大致雷同,寫部落格好累,代碼沒有上傳github,因為新來公司還沒配置~有需要聯絡。。。。
大佬拍拍磚,寫部落格比寫代碼累多了,好多地方我自己都表述不清楚~~~> 還有RV嵌套RV的衝突怎麼解決的,看完部落格你們自己分析一下吧,權當鍛煉。
從ViewPager嵌套RecyclerView再嵌套RecyclerView看安卓事件分發機制