最近在重構一個老項目,遇到ScrollView嵌套WebView的場景,因爲WebView加載的網頁並不是自適應,所以導致在滑動網頁的時候異常卡頓,很明顯是滑動衝突了,解決方式也很常規,針對滑動衝突這裏順便做筆記吧。
衝突場景
滑動衝突場景可簡單分爲兩種:
- 外部和內部的滑動方向不一致
- 外部和內部的滑動方向一致
示圖如下:
其他情況的滑動衝突都是在這兩種衝突的基礎上延伸出來的,或者以上兩種場景的嵌套,面對這種我們處理方式就是剝離成以上基礎的衝突場景,逐個處理。
處理規則
面對場景1,它的處理規則是:當用戶左右滑動時(父),需要讓外部的View攔截點擊事件;當用戶上下滑動時(子),需要讓內部View攔截點擊事件。其實說白了,我們的主要目標是確定目標view的滑動方向。
確定滑動方向,列舉以下三種方式參考:
- 根據垂直滑動和水平滑動的距離對比判斷,哪個大就是哪個方向滑動。
- 根據滑動路徑與水平方向的夾角,大於45度認爲是垂直滑動,否則是水平滑動。
- 根據垂直滑動和水平滑動的速度對比判斷,哪個大就是哪個方向滑動。
面對場景2,因爲滑動方向是一致的,所以我們只有根據業務來區分到底應該滑動哪一個view。核心點就是準確判斷滑動區域的位置。
解決方式
從上邊可以知道衝突的產生主要是不確定具體應該在哪一層滑動(內層、外層),上邊也說了相關的處理規則,接下就是我們按照規則主動把事件分發給相應的view。之前我寫過一篇《Android開發之onTouch事件的分發攔截消費機制探究學習》,這裏用到的知識點就是事件的攔截機制。
針對滑動衝突,這裏給出兩種解決滑動衝突的方式:外部攔截法和內部攔截法。
外部攔截法
外部攔截法,就是處理父View的滑動事件時,父View主動把事件攔截下來自行消化,其他情況繼續不攔截由子View消化處理。
這裏需要注意只在父View的ACTION_MOVE事件中判斷是否進行攔截,其他事件不需要,因爲一旦攔截事件就無法傳遞到子View了。
內部攔截法
內部攔截法,默認子View處理滑動事件,當判斷某個位置或者某種情況下應該父View滑動時,主動告知父View開啓事件攔截,由父View消化處理。這裏用到一個parent.requestDisallowInterceptTouchEvent()
方法。
ViewPager處理滑動衝突分析
有同學可能會問,既然內外層都可以滑動,這樣很容易出現衝突,但是爲什麼Viewpager中不存在滑動衝突呢。其實官方已經爲我們處理了。
ViewPager也是一個ViewGroup,在ViewPager的initViewPager方法中生成Scroller對象,Scroller是Android內置的專門用於漸進式滑動的類,配合插值器可以產生立體的滑動感,既然ViewPager是一個容器並且可以滑動,那麼也就避免不了內嵌view滑動衝突這一遭。
ViewPager只關注水平方向的手指滑動,根據水平方向的手指滑動來切換頁面。在垂直方向上,ViewPager並不關心,因此,ViewPager很有必要解決一下滑動衝突,把豎直方向的滑動傳遞給子View來處理。
我們知道,ViewGroup是在onInterceptTouchEvent函數中決定是否攔截觸摸事件, 所以我們直接去查看ViewPager的onInterceptTouchEvent事件攔截,來分析ViewPager的滑動衝突處理方式:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//1. 觸摸動作
final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
//2. 時刻要注意觸摸是否已經結束
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
//3. Release the drag.
if (DEBUG) Log.v(TAG, "Intercept done!");
//4. 重置一些跟判斷是否攔截觸摸相關變量
resetTouch();
//5. 觸摸結束,無需攔截
return false;
}
//6. 如果當前不是按下事件,我們就判斷一下,是否是在拖拽切換頁面
if (action != MotionEvent.ACTION_DOWN) {
//7. 如果當前是正在拽切換頁面,直接攔截掉事件,後面無需再做攔截判斷
if (mIsBeingDragged) {
if (DEBUG) Log.v(TAG, "Intercept returning true!");
return true;
}
//8. 如果標記爲不允許拖拽切換頁面,我們就"放過"一切觸摸事件
if (mIsUnableToDrag) {
if (DEBUG) Log.v(TAG, "Intercept returning false!");
return false;
}
}
//9. 根據不同的動作進行處理
switch (action) {
//10. 如果是手指移動操作
case MotionEvent.ACTION_MOVE: {
//11. 代碼能執行到這裏,就說明mIsBeingDragged==false,否則的話,在第7個註釋處就已經執行結束了
//12.使用觸摸點Id,主要是爲了處理多點觸摸
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
//13.如果當前的觸摸點id不是一個有效的Id,無需再做處理
break;
}
//14.根據觸摸點的id來區分不同的手指,我們只需關注一個手指就好
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
//15.根據這個手指的序號,來獲取這個手指對應的x座標
final float x = MotionEventCompat.getX(ev, pointerIndex);
//16.在x軸方向上移動的距離
final float dx = x - mLastMotionX;
//17.x軸方向的移動距離絕對值
final float xDiff = Math.abs(dx);
//18.同理,參照16、17條註釋
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float yDiff = Math.abs(y - mInitialMotionY);
if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
//19.判斷當前顯示的頁面是否可以滑動,如果可以滑動,則將該事件丟給當前顯示的頁面處理
//isGutterDrag是判斷是否在兩個頁面之間的縫隙內移動
//canScroll是判斷頁面是否可以滑動
if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&
canScroll(this, false, (int) dx, (int) x, (int) y)) {
mLastMotionX = x;
mLastMotionY = y;
//20.標記ViewPager不去攔截事件
mIsUnableToDrag = true;
return false;
}
//21.如果x移動距離大於最小距離,並且斜率小於0.5,表示在水平方向上的拖動
if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
if (DEBUG) Log.v(TAG, "Starting drag!");
//22.水平方向的移動,需要ViewPager去攔截
mIsBeingDragged = true;
//23.如果ViewPager還有父View,則還要向父View申請將觸摸事件傳遞給ViewPager
requestParentDisallowInterceptTouchEvent(true);
//24.設置滾動狀態
setScrollState(SCROLL_STATE_DRAGGING);
//25.保存當前位置
mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop :
mInitialMotionX - mTouchSlop;
mLastMotionY = y;
//26.啓用緩存
setScrollingCacheEnabled(true);
} else if (yDiff > mTouchSlop) {//27.否則的話,表示是豎直方向上的移動
if (DEBUG) Log.v(TAG, "Starting unable to drag!");
//28.豎直方向上的移動則不去攔截觸摸事件
mIsUnableToDrag = true;
}
if (mIsBeingDragged) {
// 29.跟隨手指一起滑動
if (performDrag(x)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
break;
}
//30.如果手指是按下操作
case MotionEvent.ACTION_DOWN: {
//31.記錄按下的點位置
mLastMotionX = mInitialMotionX = ev.getX();
mLastMotionY = mInitialMotionY = ev.getY();
//32.第一個ACTION_DOWN事件對應的手指序號爲0
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
//33.重置允許拖拽切換頁面
mIsUnableToDrag = false;
//34.標記開始滾動
mIsScrollStarted = true;
//35.手動調用計算滑動的偏移量
mScroller.computeScrollOffset();
//36.如果當前滾動狀態爲正在將頁面放置到最終位置,
//且當前位置距離最終位置足夠遠
if (mScrollState == SCROLL_STATE_SETTLING &&
Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
//37. 如果此時用戶手指按下,則立馬暫停滑動
mScroller.abortAnimation();
mPopulatePending = false;
populate();
mIsBeingDragged = true;
//38.如果ViewPager還有父View,則還要向父View申請將觸摸事件傳遞給ViewPager
requestParentDisallowInterceptTouchEvent(true);
//39.設置當前狀態爲正在拖拽
setScrollState(SCROLL_STATE_DRAGGING);
} else {
//40.結束滾動
completeScroll(false);
mIsBeingDragged = false;
}
if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY
+ " mIsBeingDragged=" + mIsBeingDragged
+ "mIsUnableToDrag=" + mIsUnableToDrag);
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
}
//41.添加速度追蹤
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
//42.只有在當前是拖拽切換頁面時我們纔會去攔截事件
return mIsBeingDragged;
}
從源碼上面看出,斜率小於0.5時,則要攔截,否則不攔截。越靠近y軸的直線,斜率越大,越靠近x軸直線斜率越小。因此,當手指滑動的傾斜度比0.5小時就去攔截事件,由ViewPager來響應切換頁面。
參考
- 《Android開發藝術探索》,微信讀書可以免費閱讀。
- https://blog.csdn.net/huachao1001/article/details/51654692