破冰之旅——ViewPager作為HeaderView的手勢沖突

這篇文章主要是記錄、總結在實際項目中遇到關于手勢沖突的“破冰之旅”。

旅途目的地

新版本優(yōu)化searching模塊的UI展現(xiàn),頁面列表主要用來展示用戶發(fā)表的moment,上部配備navigation bar和banner位,banner用來展示、引導話題和活動,允許配置多張,是個ViewPager。因為banner可以劃出屏幕,所以決定讓banner作為GridView的headerView。


Fame風靡

航行

目標明確,準備就緒,啟航!

旅途中,我們使用 GridViewWithHeaderAndFooter 來作伴(從下圖.xml代碼中看出支持了下拉刷新),一路上海風吹拂、心情愉悅,眼見著美女們的圖片、視頻都顯示出來了,忍不住欣賞了起來…

  <FPtrFrameLayout
    android:id="@+id/layout_refresh"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LoadMoreGridViewContainer
        android:id="@+id/layout_loadmore"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <GridViewWithHeaderAndFooter
            android:id="@+id/gridView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:numColumns="3" />
    </LoadMoreGridViewContainer>
</FPtrFrameLayout>

前方海域

往往,當我們以為征服了海洋的時候,它便會還以顏色。

當我欣賞佳作正酣之時,突然感覺到前方海域寒氣逼人,趕緊轉換為高能狀態(tài),嚴陣以待。經過仔細的勘測和偵查,查明低溫造成海面結冰,如不想辦法破除,可能會給航行帶來不好的體驗,旅客會覺得你的服務不夠專業(yè)。

原來是,從后臺配置完banner后,發(fā)現(xiàn)banner處 ViewPager 的滑動不順暢,在ViewPager 的滑動過程中,如果手指偏向下滑時,便引起 GridView 的下拉刷新操作。奧,直覺再次告訴我這肯定是手勢沖突了,常在海上行,哪能沒見過點風浪~

OK,回顧下Android的事件分發(fā)機制,下面用偽代碼來展示涉及到的幾個關鍵方法之間的關系:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume = false;
    //首先執(zhí)行當前view的onInterceptTouchEvent(ev)方法
    if (onInterceptTouchEvent(ev)) {
        //如果當前View進行攔截(onInterceptTouchEvent(ev)返回true),
        //接著執(zhí)行當前Veiw的onTouchEvent(ev)方法
        consume = onTouchEvent(ev);
    } else {
        //如果當前View不攔截,則事件就交給子view,調用子view的dispatchTouchEvent(ev)
        consume = getFocusedChild().dispatchTouchEvent(ev);
    }
    return consume;
}

大家也都知道手勢沖突的一般解決方法有兩種(此處默認是老水手):

1、外部攔截法:因為事件都是先經過父View的攔截處理,如果父View需要此事件就直接攔截,如果不需要就不進行攔截讓子View來處理就好了。

2、內部攔截法:前提是子View都能接受到事件,如果需要此事件直接消耗掉便是,否則就交由父View進行處理,需要配合 requestDisallowInterceptTouchEvent(boolean disallowIntercept) 方法來使用。這種方法和Android的事件分發(fā)機制不一致,因此屬于逆向思維,但是在有些場景是很有用的。比如:父View不易修改(直接compile的開源庫)、不易繼承時(別人寫的,且融合有比較多的業(yè)務邏輯)…

破冰

了解了當前的處境,那么我們就針對性地開始破冰吧。

由于是compile的 UltraPullToRefresh 庫,所以我的第一選擇是調整本地自定義 ViewPager 的代碼:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    float x = ev.getX();
    float y = ev.getY();
    switch (ev.getAction()) {
        ……
        case MotionEvent.ACTION_HOVER_MOVE:
            // 當X方向的移動距離大于Y方向時,請求不允許父View攔截,讓ViewPager來處理事件
            if (Math.abs(x - mLastX) > Math.abs(y - mLastY)) {
                getParent().requestDisallowInterceptTouchEvent(true);
            }
            break;
        ……
    }
    mLastX = x;
    mLastY = y;
}

當我滿心歡喜地準備重新起航時,不料,現(xiàn)實給了我當頭一棒,你!休!想!破!

為什么破不了呢?
是因為 requestDisallowInterceptTouchEvent 方法沒起作用?
還是父View直接處理消耗了事件,ViewPager 根本就沒機會接收?

對啊!內部攔截法的前提就是子View必須接收到事件,然后才能根據(jù)需要進行相應的處理。看來問題是出在父View上了,那么我們就直接找來造事者吧,打開GridViewWithHeaderAndFooter 卻并沒有發(fā)現(xiàn)有關事件處理的方法,它繼承自 GridView ,那么就是繼承了 GridView 的事件處理機制,基于之前的經驗,系統(tǒng)的 AbsListView 組件是沒有這個問題的。

好吧,繼續(xù)追蹤水面異常結冰的原因。經過測試發(fā)現(xiàn)了一點就是我們在橫向滑動 ViewPager 時如果手勢偏上的話是沒有問題的,只是偏下時會出現(xiàn)下拉刷新操作,然后導致 ViewPager 的橫向滑動失效,如果速度快的話就有橫向劃不動的感覺…

哈哈,原來是下拉刷新在搞鬼,還記得上面我給出的.xml布局中GridViewWithHeaderAndFooter 外部的兩個父View LoadMoreGridViewContainer 和 FPtrFrameLayout 吧,基于當前的現(xiàn)象,估計是 FPtrFrameLayout 被施了魔咒,異常纏身了。

OK,讓 FPtrFrameLayout 快快現(xiàn)身,快速定位到他的 dispatchTouchEvent 方法:

@Override
public boolean dispatchTouchEvent(MotionEvent e) {
    ……
    int action = e.getAction();
    switch (action) {
        ……
        case MotionEvent.ACTION_MOVE:
            mLastMoveEvent = e;
            mPtrIndicator.onMove(e.getX(), e.getY());
            float offsetX = mPtrIndicator.getOffsetX();
            float offsetY = mPtrIndicator.getOffsetY();
            // 橫向滑動相關的判斷邏輯
            if (mDisableWhenHorizontalMove && !mPreventForHorizontal && (Math.abs(offsetX) > mPagingTouchSlop && Math.abs(offsetX) > Math.abs(offsetY))) {
                if (mPtrIndicator.isInStartPosition()) {
                    mPreventForHorizontal = true;
                }
            }
            if (mPreventForHorizontal) {
                return dispatchTouchEventSupper(e);
            }
            // 以下是處理下拉刷新的邏輯
            boolean moveDown = offsetY > 0;
            boolean moveUp = !moveDown;
            boolean canMoveUp = mPtrIndicator.hasLeftStartPosition();

            // disable move when header not reach top
            if (moveDown && mPtrHandler != null && !mPtrHandler.checkCanDoRefresh(this, mContent, mHeaderView)) {
                return dispatchTouchEventSupper(e);
            }

            if ((moveUp && canMoveUp) || moveDown) {
                movePos(offsetY);
                return true;
            }
     }    
     return dispatchTouchEventSupper(e);
}

因為懷疑是下拉刷新在搞鬼,所以我們先來看他的邏輯:

// 以下是處理下拉刷新的邏輯
boolean moveDown = offsetY > 0;
boolean moveUp = !moveDown;
boolean canMoveUp = mPtrIndicator.hasLeftStartPosition();

// disable move when header not reach top
// 向下滑動,且配置不支持刷新,則返回
if (moveDown && mPtrHandler != null && !mPtrHandler.checkCanDoRefresh(this, mContent, mHeaderView)) {
   return dispatchTouchEventSupper(e);
}
// 向上或向下滑動,執(zhí)行操作
if ((moveUp && canMoveUp) || moveDown) {
    movePos(offsetY);
    return true;
}

我們測試的情況是橫向滑時偏向下move,配置是支持刷新的,所以會進入 movePos 方法,該方法主要是得到Y方向move的距離,然后執(zhí)行 updatePos 方法。

private void movePos(float deltaY) {
    // has reached the top
    if ((deltaY < 0 && mPtrIndicator.isInStartPosition())) {
        if (DEBUG) {
            PtrCLog.e(LOG_TAG, String.format("has reached the top"));
        }
        return;
    }

    int to = mPtrIndicator.getCurrentPosY() + (int) deltaY;

    // over top
    if (mPtrIndicator.willOverTop(to)) {
        if (DEBUG) {
            PtrCLog.e(LOG_TAG, String.format("over top"));
        }
        to = PtrIndicator.POS_START;
    }

    mPtrIndicator.setCurrentPos(to);
    int change = to - mPtrIndicator.getLastPosY();
    updatePos(change);
}

private void updatePos(int change) {
    if (change == 0) {
        return;
    }
    boolean isUnderTouch = mPtrIndicator.isUnderTouch();
    // once moved, cancel event will be sent to child
    if (isUnderTouch && !mHasSendCancelEvent && mPtrIndicator.hasMovedAfterPressedDown()) {
        mHasSendCancelEvent = true;
        sendCancelEvent();
    }
    ……
}

在 updatePos 方法中又會通過判斷執(zhí)行到 sendCancelEvent() 方法,即向子View派發(fā) MotionEvent.ACTION_CANCEL 事件。呵,這便是我剛才橫向滑動然后偏向下move時 ViewPager 滑動失效的原因。

OK,滑動失效的原因找到了,那么我們需要做的就是在 dispatchTouchEvent 方法中繞開下拉刷新的邏輯唄,理所當然,lib庫作者明顯考慮了橫向判斷的邏輯,我們接著分析:

 // (判斷1)
 // X方向滑動距離的絕對值大于 mPagingTouchSlop 且大于Y方向
 if (mDisableWhenHorizontalMove && !mPreventForHorizontal && (Math.abs(offsetX) > mPagingTouchSlop && Math.abs(offsetX) > Math.abs(offsetY))) {
     // 列表是否滑到了頂部
     if (mPtrIndicator.isInStartPosition()) {
         mPreventForHorizontal = true;
     }
 }
 // (判斷2)
 // 為橫向滑動,直接返回。
 if (mPreventForHorizontal) {
    return dispatchTouchEventSupper(e);
 }

可以看出關鍵點是:如果 mPreventForHorizontal 為true就直接交給super.dispatchTouchEvent(e),那么便不會執(zhí)行下拉刷新的邏輯,破冰就指秒可待!mDisableWhenHorizontalMove 默認為false,我們給設成true,mPreventForHorizontal 在 ACTION_DOWN 時被設為false,也不影響。嘿,見鬼了,邏輯沒問題吶(哈哈,太多時候我們都會不禁發(fā)出這樣的疑問:不應該啊,這么沒問題啊~)。“看看 mPagingTouchSlop 吧,萬一是這哥們呢”,心里回響起這個聲音。

mPagingTouchSlop = conf.getScaledTouchSlop() * 2;

即系統(tǒng)的 TOUCH_SLOP * 2,乘以2的話那么 Math.abs(offsetX) > mPagingTouchSlop 條件就不容易成立(除非你快速且準確地橫向滑動)。改為正常試試?修改、運行、啟動一氣呵成,著實有效,你能相信只是兩個字符的改動?第一次觸發(fā) ACTION_MOVE 分支的邏輯經過判斷1為true后,之后的move事件直接進入判斷2并返回。哈哈,我們手上揮舞著跳動的字符,幻化起飛躍屏幕的世界…

揚帆

眼見堅冰悄然融化,何不趁機揚起風帆!

1、這并不是一個通過內截法解決手勢沖突問題的案例。因為父View在處理下拉時并沒有做Intercept,而是向子View派發(fā) ACTION_CANCEL 事件。
2、issues中有人提供了如下解法,但會導致在 ViewPager 區(qū)域下拉不會出現(xiàn)刷新效果。

mViewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
   @Override
   public void onPageScrollStateChanged(int state) {
       mPtrFrame.setEnabled(state == ViewPager.SCROLL_STATE_IDLE);
   }
});

3、也有人說把判斷1中的這個去掉,嗯,一樣的思路,都是使判斷1容易成立。不過 TouchSlop 畢竟是Api定義的認為觸發(fā)移動的最小距離,不過Y方向的滑動卻并沒有對比 TouchSlop,所以這點你可以根據(jù)情況前后統(tǒng)一便是。

Math.abs(offsetX) > mPagingTouchSlop

4、后來在另外一個issues中看到庫的作者說這并不是bug,而是下拉操作比較靈敏而已。對啊,為什么使他如此靈敏呢?可能作者當初設計庫時主要考慮邏輯是下拉的動作,隨著使用場景的復雜,需要支持橫向的業(yè)務,后來增加了 mDisableWhenHorizontalMove 字段來控制,但此時2倍的 TouchSlop 就不太合適了。

航海總結

以上,或許帶你領略、加深了某些技能,但有可能都是錯的,因為這個是我的驗證,或許你有不同的聲音。so,重要的是讓理論指導思考,在實踐中驗證想法和理論,最后達成目標并反過來總結思考整個過程。

最后,感謝同事鵬哥,是他引入了這個組件并在項目中方便使用,我向他描述了問題,他便準確、快速定位到了關鍵點。


「spiritTalk的航海日記」
轉載請標明出處:http://www.lxweimin.com/p/0336b527da3f

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容