1. 程式人生 > >Android事件衝突場景分析及一般解決思路

Android事件衝突場景分析及一般解決思路

攔截的藝術

先來看介面ViewParent中的一個函式

    /**
     * Called when a child does not want this parent and its ancestors to
     * intercept touch events with
     * {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}.
     *
     * <p>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.</p>
     * 
     * @param
disallowIntercept True if the child does not want the parent to * intercept touch events. */
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept);

引數disallowIntercept的意思就是childView告訴父容器要不要進行攔截

true :告訴所有父控制元件不要攔截,事件交由childrenView處理

    Code 1
    //該程式碼展示的是站在View的角度,在子View的dispatchTouchEvent()中解決衝突的實戰程式碼
@Override public boolean dispatchTouchEvent(MotionEvent ev) { int x = (int) ev.getX(); int y = (int) ev.getY(); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: /** * 子View的所有父ViewGroup都會跳過onInterceptTouchEvent的回撥 * 相當於ViewGroup的流程是dispatchTouchEvent() --> onTouchEvent() */
getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: int deltaX = x - mLastX; int deltaY = y - mLastY; if (Math.abs(deltaX) > Math.abs(deltaY) + 5) {//水平滑動,使得父類可以執行onInterceptTouchEvent /** * 不讓所有的父容器跳過onInterceptTouchEvent() * 相當於ViewGroup的流程是:dispatchTouchEvent() --> onInterceptTouchEvent() --> onTouchEvent() * 也就是ViewGroup攔截以後交給自己的onTouchEvent()來處理 */ getParent().requestDisallowInterceptTouchEvent(false); } break; } mLastX = x; mLastY = y; return super.dispatchTouchEvent(ev); }

false:告訴所有父控制元件攔截。在父控制元件的onInterceptTouchEvent()中可能類似這樣的處理

@Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        if(event.getAction() == MotionEvent.ACTION_DOWN){
            mLastX = x;
            mLastY = y;
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
                return true;
            }
            return false;
        }else{
            //如果是非down事件,說明子View並沒有攔截父類的onInterceptTouchEvent
           //說明該事件交由父類處理,所以不需要再傳遞給子類,返回true
           return true;
        }
    }

然後接下來的事情交給父控制元件的onTouchEvent()來處理
Ok,咱們回到攔截的藝術,主要分兩個角度

解決衝突的兩個角度

ChildView的角度

  • 在View的onTouch()或者子控制元件的dispatchTouchEvent中,根據具體的業務邏輯來告訴父容器要不要攔截。兩種思路下程式碼都以給出,參見Code 1 , 2
    Code 2
    // //該程式碼展示的是站在View的角度,在子View的onTouchEvent()中解決衝突的實戰程式碼
    mListView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        int action = event.getAction();
        if(action == MotionEvent.ACTION_DOWN) {
            mLastY = event.getY();
        }
        if(action == MotionEvent.ACTION_MOVE) {
            int top = mListView.getChildAt(0).getTop();
            float nowY = event.getY();
            if(!isSvToBottom) {
                // 允許scrollview攔截點選事件, scrollView滑動
                mScrollView.requestDisallowInterceptTouchEvent(false);
            } else if(top == 0 && nowY - mLastY > THRESHOLD_Y_LIST_VIEW) {
                // 允許scrollview攔截點選事件, scrollView滑動
                mScrollView.requestDisallowInterceptTouchEvent(false);
            } else {
                // 不允許scrollview攔截點選事件, listView滑動
                mScrollView.requestDisallowInterceptTouchEvent(true);
            }
        }
        return false;
    }
});

ViewGroup角度

  • 在onInterceptTouchEvent()中根據具體業務邏輯判斷要不要攔截事件
@Override  
   public boolean onInterceptTouchEvent(MotionEvent ev) {  
       boolean intercepted = false;  
       int x = (int) ev.getX();  
       int y = (int) ev.getY();  
       switch (ev.getAction()) {  
           case MotionEvent.ACTION_DOWN:  
               //down事件不攔截,否則無法傳給子元素  
               intercepted = false;  
               if (!mScroller.isFinished()) {  
                   mScroller.abortAnimation();  
                   intercepted = true;  
               }  
               break;  
           case MotionEvent.ACTION_MOVE:  
               int deltaX = x - mLastXIntercept;  
               int deltaY = y - mLastYIntercept;  
               //水平滑動則攔截  
               if (Math.abs(deltaX) > Math.abs(deltaY) + 5) {  
                   intercepted = true;  
               } else {  
                   intercepted = false;  
               }  
               break;  
           case MotionEvent.ACTION_UP:  
               //不攔截,否則子元素無法收到  
               intercepted = false;  
               break;  
       }  
      ...
      return intercepted;  
   }  

衝突解決實戰

上下滑動與上下滑動的衝突型

ScrollView巢狀ListView

  • 網上的解決思路分析:

    • 根據展示資料的個數乘以每一個Item的高度,計算出ListView的總體高度,然後動態的設定ListView的高度
    • 複寫ListView的onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法,讓ListView完全展開

    這兩種方式最突出的缺陷在於ListView的子控制元件不能複用,容易造成OOM。

  • 分析衝突點:巢狀之後ListView的高度無法完全顯示,且兩者存在事件衝突

  • 解決思路:無法顯示,複用的問題我們需要在佈局中明確的設定ListView的高度。衝突根據ScrollView的狀態和ListView的狀態共同決定,衝突的解決選擇在子View的onTouch()中解決。

    • ScrollView在未滑動到底部時候,如果點選並滑動ListView時候,ListView是不能滑動的,不禁止。
    • 如果ScrollView滑動到底部,且ListView已經到頂部,繼續下拉ListView,其實會拉動ScrollView,不禁止。
    • 如果ScrollView滑動到底部,使用者向上滑,ListView滑動,禁止ScrollView截斷點選事件能力

對於ScrollView的滑動到底部的監聽我們可以這樣做

boolean isSvToBottom = false;
scroll_view.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                // 判斷 scrollView 當前滾動位置在頂部
                if(scroll_View.getSrollY == 0){
                    isSvToBottom = true;
                }

                // 判斷scrollview 滑動到底部
                // scrollY 的值和子view的高度一樣,這人物滑動到了底部
                if (scroll_view.getChildAt(0).getHeight() - scroll_view.getHeight() 
                            == scroll_view.getScrollY()){
                    isSvToBottom =false;
                }
                return false;
            }
        });

mListView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        int action = event.getAction();
        if(action == MotionEvent.ACTION_DOWN) {
            mLastY = event.getY();
        }
        if(action == MotionEvent.ACTION_MOVE) {
            int top = mListView.getChildAt(0).getTop();
            float nowY = event.getY();
            if(!isSvToBottom) {
                mScrollView.requestDisallowInterceptTouchEvent(false);
            } else if(top == 0 && nowY - mLastY > THRESHOLD_Y_LIST_VIEW) {
                mScrollView.requestDisallowInterceptTouchEvent(false);
            } else {
                mScrollView.requestDisallowInterceptTouchEvent(true);
            }
        }
        return false;
    }
});

子控制元件部分接管型

  • 場景:SwipeRefreshLayout --> ScrollView --> ViewPager,這種佈局場景很常見,多見於應用的首頁,如美團。

  • 分析衝突點:ScrollView的上下滑動與ViewPager的左右滑動原本是不衝突的,但是加入了SwipeRefreshLayout之後,事件場景就變得複雜。當滑動ViewPager的時候,很容易就觸發了SwipeRefreshLayout的下來重新整理。

  • 解決思路:因為ViewPager只涉及X軸方向上的滑動,所以我們在MotionEvent_ACTION_MOVE的時候去動態的判斷X周方向上是否超過了我們設定的閾值,這個值我們可以自定義,也可以去拿系統的這個值
    mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);

  mViewPager.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        int action = event.getAction();
        if(action == MotionEvent.ACTION_DOWN) {
            mLastX = event.getX();
        }
        if(action == MotionEvent.ACTION_MOVE) {
            if(Math.abs(event.getX() - mLastX) > 60f) {
                mRefreshLayout.setEnabled(false);
                mScrollView.requestDisallowInterceptTouchEvent(true);
            }
        }
        if(action == MotionEvent.ACTION_UP) {
            mScrollView.requestDisallowInterceptTouchEvent(false);
            mRefreshLayout.setEnabled(true);
        }
        return false;
    }
});

子控制元件完全接管事件型

ScrollView巢狀Map(第三方地圖控制元件)

  • 分析衝突點:外層套了ScrollView的Map就接受不到事件了,但Map的應用場景是隻要使用者在Map上觸控,也就是MotionEvent_ACTION_DOWN時候,說明就是要操作Map,就跟ScrollView沒有關係。

  • 解決思路:直接在子View的onTouch中告訴ScrollView(父控制元件)要不要攔截

mMapView.getChildAt(0).setOnTouchListener(new View.OnTouchListener() {
    @Override
     public boolean onTouch(View v, MotionEvent event) {
          if(event.getAction() == MotionEvent.ACTION_UP){
               mScrollView.requestDisallowInterceptTouchEvent(false);
           }else{
               mScrollView.requestDisallowInterceptTouchEvent(true);
           }
         return false;
     }
});

總結

本文重要從子View的角度(內部),和ViewGroup(外部)的兩種角度去解決事件衝突的思路,並給出了一些常見的衝突場景以及解決案例,具體的衝突點需要具體分析,主要是分兩點來確定

  • 確定衝突的有關控制元件
  • 找準衝突發生的點
  • 確定是用內部還是外部的方式

注意:內部解決優先於外部解決思路