View 的滑動沖突處理

參考資料:
1.《Android開發(fā)藝術(shù)探索》

  1. http://www.reibang.com/p/057832528bdd

常見的滑動沖突創(chuàng)景##

  1. 外部滑動方向與內(nèi)部滑動方向不一致竹观;
  2. 外部滑動方向與內(nèi)部滑動方法一致時;
  3. 上面2種情況的嵌套员帮;
內(nèi)外滑動方向不一致
內(nèi)外滑動方向一致
結(jié)合2種情況.png

滑動沖突的處理規(guī)則##

不管多么復(fù)雜的滑動沖突探入,他們之間的區(qū)別僅僅是滑動規(guī)則不同而已踩萎;

處理規(guī)則:根據(jù)滑動的方向怔蚌,進(jìn)行相應(yīng)的攔截叉跛,如果想外部View接受事件靖避,就外部View攔截,想內(nèi)部View接受震放,就內(nèi)部View攔截宾毒;

** 外部攔截法:**
指的是點擊事情是先經(jīng)過父容器的攔截處理,如父容器需要此事件殿遂,則攔截诈铛,如不需要就不攔截;

偽代碼如下:

public boolean onInterceptTouchEvent(MotionEvent ev) {
   boolean result = false;
   switch (ev.getAction()) {
      // 不能消耗down墨礁,如果消耗了down幢竹,后續(xù)分發(fā)事件,onInterceptTouch就不再執(zhí)行恩静,即 子view將收不到任何事件
      case MotionEvent.ACTION_DOWN:   
         lastX = ev.getX();
         lastY = ev.getY();
         result = false;
         // 讓Detector收到DOWN事件焕毫,如果不設(shè)置,則表示ViewGroup將沒有down這個事件 這個時,候驶乾,滑動的時候咬荷,會發(fā)生錯亂;
// 根據(jù)事件分發(fā)原則轻掩,只有在 onInterceptTouchEvent返回true時,onTouchEvent才執(zhí)行懦底;
    // 返回false的時候唇牧,down被子view消耗了,這個時候聚唐,當(dāng)前 容器 onTouchEvent沒有收到down事件丐重;
         // mDetector.onTouchEvent(ev);       
         break;
      case MotionEvent.ACTION_MOVE:
         if(父容器需要當(dāng)前點擊事件) {
            result = true;
         } else {
            result = false;
         }
         break;
      case MotionEvent.ACTION_UP:
         result = false;
   }
   return result;
}

示例代碼:
https://github.com/zhaoyubetter/TestCode/blob/master/improve/src/main/java/test/better/com/leak/ui/CustomHorizontalView.java

內(nèi)部攔截法
是父容器不攔截任何事件,所有的事件都傳遞給子元素杆查,如果子元素要事件就直接消耗掉扮惦,否則交給父容器進(jìn)行處理;
這種方式與Android的事件分發(fā)不一致亲桦,需要配合 requestDisallowInterceptTouchEvent方法才能工作崖蜜;

// 父: onInterceptTouchEvent
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
   boolean result = false;
   switch (ev.getAction()) {
      case MotionEvent.ACTION_DOWN:
         result = false;
         mDetector.onTouchEvent(ev);
         break;
      default:
         result = true;
         break;
   }
   return result;
}

// 子 view : 
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
   int action = ev.getAction();
   switch (action) {
      case MotionEvent.ACTION_DOWN:
         // 要求父不要阻止攔截事件
         getParent().requestDisallowInterceptTouchEvent(true);
         lastX = (int) ev.getX();
         lastY = (int) ev.getY();
         break;
      case MotionEvent.ACTION_MOVE:
         int distanceX = (int) Math.abs(ev.getX() - lastX);
         int distanceY = (int) Math.abs(ev.getY() - lastY);
         int slop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
         // 父要事件了
         if (distanceX > distanceY && distanceX > slop) {
            getParent().requestDisallowInterceptTouchEvent(false);
         }
         break;
   }

   lastX = (int) ev.getX();
   lastY = (int) ev.getY();

   return super.dispatchTouchEvent(ev);
}

示例代碼:
https://github.com/zhaoyubetter/TestCode/blob/master/improve/src/main/java/test/better/com/leak/ui/CustomHorizontalView2.java

滑動方向一致的沖突處理

上面的例子是內(nèi)外滑動的方向相反時的處理,如果滑動方向一致呢客峭?采用 scrollView 包裹ListView就是這種情況豫领,
采用外部攔截法來處理,這里重新 scrollView 的 onInterceptTouchEvent:

public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercept = false;
    float y = ev.getY();

    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
        mDownY = ev.getY();
        intercept = super.onInterceptTouchEvent(ev);
        case MotionEvent.ACTION_MOVE:
        // 第一個條目完全可見時舔琅,并且向下滑動時等恐,才攔截事件
        if (mListView.getFirstVisiblePosition() == 0 &&
            mListView.getChildAt(0).getTop() >= mListView.getPaddingTop() &&
            y > mDownY) {
            intercept = true;
            break;
        }

        // 最后一個條目完全可見時,并且向上滑動,攔截事件
        if (mListView.getLastVisiblePosition() >= mListView.getCount() - 1) {
            final int childIndex = mListView.getLastVisiblePosition() - mListView.getFirstVisiblePosition();
            final int index = Math.min(childIndex, mListView.getCount() - 1);
            final View lastVisibleChild = mListView.getChildAt(index);
            if (lastVisibleChild != null && y < mDownY) {
            Log.e("better", "last bottom: " + lastVisibleChild.getBottom());
            intercept = lastVisibleChild.getBottom() + mListView.getBottom() >= mListView.getHeight();
            Log.e("better", intercept + "");
            }
        }
        break;
    }

    Log.e("better", intercept + "" + " , top: " + mListView.getChildAt(0).getTop() + ", listView Height: " + mListView.getHeight());

    return intercept;
}

代碼路徑:
https://github.com/zhaoyubetter/TestCode/blob/master/improve/src/main/java/test/better/com/leak/ui/ConflictScrollView.java

8.14 修正
上面的代碼课蔬,效果是實現(xiàn)了囱稽,但是他們之間的聯(lián)動有中斷,我們需要解決這個問題二跋,解決的入口战惊,就是 dispatchTouchEvent
修正如下:

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        float y = ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                float dy = y - mLastY;
                if (Math.abs(dy) > ViewConfiguration.getTouchSlop()) {
                    // 內(nèi)層下拉到頭了 并且 外層還能下拉時,重發(fā)事件
                    if (!isReDispatch && !ViewCompat.canScrollVertically(mListView, -1) && dy > 0 && ViewCompat.canScrollVertically(this, -1)) {
                        isReDispatch = true;
                        Log.e("better", "下拉到頭了同欠,外層還可以下拉样傍,重發(fā)事件");
                        ev.setAction(MotionEvent.ACTION_CANCEL);
                        MotionEvent ev2 = MotionEvent.obtain(ev);
                        ev2.setAction(MotionEvent.ACTION_DOWN);
                        dispatchTouchEvent(ev);
                        return dispatchTouchEvent(ev2);
                    }

                    if (!isReDispatch && !ViewCompat.canScrollVertically(mListView, 1) && dy < 0 && ViewCompat.canScrollVertically(this, 1)) {
                        isReDispatch = true;
                        Log.e("better", "上拉  到頭了,外層還可以上拉铺遂,重發(fā)事件");
                        ev.setAction(MotionEvent.ACTION_CANCEL);
                        MotionEvent ev2 = MotionEvent.obtain(ev);
                        ev2.setAction(MotionEvent.ACTION_DOWN);
                        dispatchTouchEvent(ev);
                        return dispatchTouchEvent(ev2);
                    }
                }

                break;

            case MotionEvent.ACTION_UP:
                isReDispatch = false;
        }
        return super.dispatchTouchEvent(ev);
    }

@Override
    public boolean onTouchEvent(MotionEvent ev) {
        float y = ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                float dy = y - mLastY;
                Log.e("better", "onTouchEvent: " + dy);
                if(!isDrag && Math.abs(dy) > ViewConfiguration.getTouchSlop()) {
                    isDrag = true;
                }
                if (isDrag) {
                    if (dy > 0 && !ViewCompat.canScrollVertically(this, -1) && ViewCompat.canScrollVertically(mListView, -1)) {
                        ev.setAction(MotionEvent.ACTION_DOWN);
                        dispatchTouchEvent(ev);
                        isReDispatch = false;
                        Log.e("better", "redispatch --》 onTouchEvent");
                    }
                    if (dy < 0 && !ViewCompat.canScrollVertically(this, 1) && ViewCompat.canScrollVertically(mListView, 1)) {
                        ev.setAction(MotionEvent.ACTION_DOWN);
                        dispatchTouchEvent(ev);
                        isReDispatch = false;
                        Log.e("better", "redispatch --》 onTouchEvent");
                    }
                }

                mLastY = y;
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                isDrag = false;
        }
        return super.onTouchEvent(ev);
    }
@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercept = false;
        float y = ev.getY();

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastY = y;
                intercept = super.onInterceptTouchEvent(ev);
                break;
            case MotionEvent.ACTION_MOVE:
                // 第一個條目完成可見時衫哥,并且向下滑動時,才攔截事件
                float dy = y - mLastY;
                if (Math.abs(dy) > ViewConfiguration.getTouchSlop()) {
                    isDrag = true;
                    if (!ViewCompat.canScrollVertically(mListView, -1) && dy > 0) {
                        intercept = true;
                    }
                    if (!ViewCompat.canScrollVertically(mListView, 1) && dy < 0) {
                        intercept = true;
                    }
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                isDrag = false;
        }

        return intercept;
    }

內(nèi)部攔截法來處理襟锐,只修改ListView 的 dispatchTouchEvent撤逢,不修改 scrollView 代碼:

 @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        float y = ev.getY();

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownY = ev.getY();
                mScrollView.requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                // 向下滑動
                if (getFirstVisiblePosition() == 0 && getChildAt(0).getTop() >= getPaddingTop() &&
                        y > mDownY) {
                    mScrollView.requestDisallowInterceptTouchEvent(false);
                    break;
                }

                if (getLastVisiblePosition() == getCount() - 1) {
                    final View lastVisibleChild = getChildAt(getLastVisiblePosition() - getFirstVisiblePosition());
                    if (lastVisibleChild != null && y < mDownY) {
                        if (lastVisibleChild.getBottom() + getPaddingBottom() <= getHeight()) {
                            mScrollView.requestDisallowInterceptTouchEvent(false);
                        }
                    }
                }
                break;
        }

        return super.dispatchTouchEvent(ev);
    }

通過這種方式,可以發(fā)現(xiàn)當(dāng)內(nèi)部 listview 滾動到 頭 or 尾粮坞,時繼續(xù)滾動時蚊荣,由于事件又給了 scrollView了。所以莫杈,外部scrollView 收到了事件互例,開始了外部滾動;

![內(nèi)部攔截法——滑動方向一致].gif](http://upload-images.jianshu.io/upload_images/2003670-b617ea2c4dc29893.gif?imageMogr2/auto-orient/strip)

如果要使用 外部攔截法筝闹,來實現(xiàn) 上圖動畫中的 效果媳叨,那就復(fù)雜多了。嘗試了一下关顷,沒有實現(xiàn)好糊秆;

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市议双,隨后出現(xiàn)的幾起案子痘番,更是在濱河造成了極大的恐慌,老刑警劉巖平痰,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件汞舱,死亡現(xiàn)場離奇詭異,居然都是意外死亡宗雇,警方通過查閱死者的電腦和手機(jī)兵拢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來逾礁,“玉大人说铃,你說我怎么就攤上這事访惜。” “怎么了腻扇?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵债热,是天一觀的道長。 經(jīng)常有香客問我幼苛,道長窒篱,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任舶沿,我火速辦了婚禮墙杯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘括荡。我一直安慰自己高镐,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布畸冲。 她就那樣靜靜地躺著嫉髓,像睡著了一般。 火紅的嫁衣襯著肌膚如雪邑闲。 梳的紋絲不亂的頭發(fā)上算行,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天,我揣著相機(jī)與錄音苫耸,去河邊找鬼州邢。 笑死,一個胖子當(dāng)著我的面吹牛褪子,可吹牛的內(nèi)容都是我干的量淌。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼褐筛,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了叙身?” 一聲冷哼從身側(cè)響起渔扎,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎信轿,沒想到半個月后晃痴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡财忽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年倘核,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片即彪。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡紧唱,死狀恐怖活尊,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情漏益,我是刑警寧澤蛹锰,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站绰疤,受9級特大地震影響铜犬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜轻庆,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一癣猾、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧余爆,春花似錦纷宇、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至转捕,卻和暖如春作岖,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背五芝。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工痘儡, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人枢步。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓沉删,卻偏偏與公主長得像,于是被迫代替她去往敵國和親醉途。 傳聞我的和親對象是個殘疾皇子矾瑰,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內(nèi)容