Android滑動沖突解決方法


敘述

滑動沖突可以說是日常開發中比較常見的一類問題,也是比較讓人頭疼的一類問題,尤其是在使用第三方框架的時候,兩個原本完美的控件,組合在一起之后,忽然發現整個世界都不好了。

關於滑動沖突

滑動沖突分類

滑動沖突,總的來說就是兩類。

  1. 同方向滑動沖突
    比如ScrollView嵌套ListView,或者是ScrollView嵌套自己

  2. 不同方向滑動沖突
    比如ScrollView嵌套ViewPager,或者是ViewPager嵌套ScrollView,這種情況其實很典型。現在大部分應用最外層都是ViewPager+Fragment 的底部切換(比如微信)結構,這種時候,就很容易出現滑動沖突。不過ViewPager里面無論是嵌套ListView還是ScrollView,滑動沖突是沒有的,畢竟是官方的東西,可能已經考慮到了這些,所以比較完善。

復雜一點的滑動沖突,基本上就是這兩個沖突結合的結果。

滑動沖突解決思路

滑動沖突,就其本質來說,兩個不同方向(或者是同方向)的View,其中有一個是占主導地位的,每次總是搶着去處理外界的滑動行為,這樣就導致一種很別扭的用戶體驗,明明只是橫向的滑動了一下,縱向的列表卻在垂直方向發生了動作。就是說,這個占主導地位的View,每一次都身不由己的攔截了這個滑動的動作,因此,要解決滑動沖突,就是得明確告訴這個占主導地位的View,什么時候你該攔截,什么時候你不應該攔截,應該由下一層的View去處理這個滑動動作。

這里不明白的同學,可以去了解一下Android Touch事件的分發機制,這也是解決滑動沖突的核心知識。

第二種滑動沖突,解決起來是比較簡單的。這里就結合例子說一下。

滑動沖突

這里,說一下背景情況。之前做下拉刷新、上拉加載更多時一直使用的是PullToRefreshView這個控件,因為很方便,不用導入三方工程。在其內部可以放置ListView,GridView及ScrollView,非常方便,用起來可謂是屢試不爽。但是直到有一天,因項目需要,在ListView頂部加了一個輪播圖控件BannerView。結果發現輪播圖滑動的時候,和縱向的下拉刷新組件沖突了。

如之前所說,解決滑動沖突的關鍵,就是明確告知接收到Touch的View,是否需要攔截此次事件。

解決方法

解決方案1,從外部攔截機制考慮

這里,相當於是PullToRefreshView嵌套了ViewPager,那么每次優先接收到Touch事件的必然是PullToRefreshView。因為正常情況下,父控件會優先接收到touch事件。這樣就清楚了,看代碼:

在PullToRefreshView的onInterceptTouchEvent方法中:

    @Override public boolean onInterceptTouchEvent(MotionEvent e) { int y = (int) e.getRawY(); int x = (int) e.getRawX(); boolean resume = false; switch (e.getAction()) { case MotionEvent.ACTION_DOWN: // 發生down事件時,記錄y坐標 mLastMotionY = y; mLastMotionX = x; resume = false; break; case MotionEvent.ACTION_MOVE: // deltaY > 0 是向下運動,< 0是向上運動 int deltaY = y - mLastMotionY; int deleaX = x - mLastMotionX; if (Math.abs(deleaX) > Math.abs(deltaY)) { resume = false; } else { //當前正處於滑動 if (isRefreshViewScroll(deltaY)) { resume = true; } } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: break; } return resume; }

這里最關鍵的代碼就是這行

if (Math.abs(deleaX) > Math.abs(deltaY)) { resume = false; }

橫向滑動距離大於縱向時,無須攔截這次滑動事件,滑動事件會傳遞到下一層的view,也就是這里的輪播圖控件,這樣橫向滑動輪播圖的時候,PullToRefreshView就不會有下拉的動作了。其實,就是這么簡單,但前提是你必須明確了解Android Touch事件的傳遞機制,期間各個方法執行的順序及意義。

ps: 關於上文中提到的isRefreshViewScroll 方法代碼(這個方法其實是PullToRefreshView這個控件自帶的一個方法)

/** * 是否應該到了父View,即PullToRefreshView滑動 * * @param deltaY , deltaY > 0 是向下運動,< 0是向上運動 * @return */ private boolean isRefreshViewScroll(int deltaY) { if (mHeaderState == REFRESHING || mFooterState == REFRESHING) { return false; } // 對於ListView和GridView if (mAdapterView != null) { // 子view(ListView or GridView)滑動到最頂端 if (deltaY > 0) { View child = mAdapterView.getChildAt(0); if (child == null) { // 如果mAdapterView中沒有數據,不攔截 return false; } if (mAdapterView.getFirstVisiblePosition() == 0 && child.getTop() == 0) { mPullState = PULL_DOWN_STATE; return true; } int top = child.getTop(); int padding = mAdapterView.getPaddingTop(); if (mAdapterView.getFirstVisiblePosition() == 0 && Math.abs(top - padding) <= 8) {// 這里之前用3可以判斷,但現在不行,還沒找到原因 mPullState = PULL_DOWN_STATE; return true; } } else if (deltaY < 0) { View lastChild = mAdapterView.getChildAt(mAdapterView .getChildCount() - 1); if (lastChild == null) { // 如果mAdapterView中沒有數據,不攔截 return false; } // 最后一個子view的Bottom小於父View的高度說明mAdapterView的數據沒有填滿父view, // 等於父View的高度說明mAdapterView已經滑動到最后 if (lastChild.getBottom() <= getHeight() && mAdapterView.getLastVisiblePosition() == mAdapterView .getCount() - 1) { mPullState = PULL_UP_STATE; return true; } } } // 對於ScrollView if (mScrollView != null) { // 子scroll view滑動到最頂端 View child = mScrollView.getChildAt(0); if (deltaY > 0 && mScrollView.getScrollY() == 0) { mPullState = PULL_DOWN_STATE; return true; } else if (deltaY < 0 && child.getMeasuredHeight() <= getHeight() + mScrollView.getScrollY()) { mPullState = PULL_UP_STATE; return true; } } return false;

解決方案2,從內容逆向思維分析

有時候,我們不想去修改或者是無法修改最先接收到Touch事件的View 時,比如這里我不想去修改PullToRefreshView的代碼。就必須考慮從當前從Touch傳遞事件中最后的那個View逆向考慮。首先,由Android中View的Touch事件傳遞機制,我們知道Touch事件,首先必然由最外層View接收到,並很有可能被它攔截,如果無法更改這個最外層View,那么是不是就沒轍了呢?其實不然,Android這么高大上的系統必然考慮到了這個問題,好了廢話不說,先看代碼

    private BannerView carouselView; private Context mContext; private PullToRefreshView refreshView; refreshView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { carouselView.getParent().requestDisallowInterceptTouchEvent(false); return false; } }); carouselView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { carouselView.getParent().requestDisallowInterceptTouchEvent(true); int x = (int) event.getRawX(); int y = (int) event.getRawY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: lastX = x; lastY = y; break; case MotionEvent.ACTION_MOVE: int deltaY = y - lastY; int deltaX = x - lastX; if (Math.abs(deltaX) < Math.abs(deltaY)) { carouselView.getParent().requestDisallowInterceptTouchEvent(false); } else { carouselView.getParent().requestDisallowInterceptTouchEvent(true); } default: break; } return false; } });

首先說一下這個方法

public abstract void requestDisallowInterceptTouchEvent (boolean disallowIntercept)

Called when a child does not want this parent and its ancestors to intercept touch events with onInterceptTouchEvent(MotionEvent).
This parent should pass this call onto its parents. This parent must obey this request for the duration of the touch (that is, only clear the flag after this parent has received an up or a cancel.

Parameters
disallowIntercept
True if the child does not want the parent to intercept touch events.

API里的意思很明確,子View如果不希望其父View攔截Touch事件時,可調用此方法。當disallowIntercept這個參數為true時,父View將不攔截。

好了,言歸正傳。這里攔截直接也很明確,在carouselView的onTouch方法中每次進入就設定父View不攔截此次事件,然后在MOTION_MOVE時候,根據滑動的距離判斷再決定是父View是否有權利攔截Touch事件(即滑動行為)。

關鍵的處理邏輯就是這里:

if (Math.abs(deltaX) < Math.abs(deltaY)) { carouselView.getParent().requestDisallowInterceptTouchEvent(false); } else { carouselView.getParent().requestDisallowInterceptTouchEvent(true); }

這個結合上面對這個方法的解釋,應該很好理解了,就不多做闡述了。


可以看到,解決這種滑動沖突的方法很簡單,最根本的還是得充分了解Touch事件的傳遞機制,只有這樣,才能明白該在哪里做什么事情。當然,橫豎滑動的沖突很好理解,但同一方向的滑動沖突情況就有點復雜了,下次再說。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM