1. 程式人生 > >Android 經典筆記之四: 事件衝突解決思路與方案

Android 經典筆記之四: 事件衝突解決思路與方案

事件衝突解決思路與方案
目錄介紹
1.事件機制簡單介紹
1.1 觸控事件
1.2 分發事件
1.3 攔截事件

2.解決滑動衝突的思路及方法
2.1 第一種情況,滑動方向不同
2.2 第二種情況,滑動方法相同
2.3 第三種情況,以上兩種情況巢狀

3.案例解決方法
3.1 針對2問題的解決思路
3.2 滑動方向不同,解決衝突的外部解決法
3.3 滑動方向不同,解決衝突的內部解決法
3.4 ViewPager巢狀ViewPager內部解決法
3.5 滑動方向相同,解決衝突的外部解決法
3.6 解決ScrollView和ViewPager,RecycleView滑動衝突

好訊息

  • 部落格筆記大彙總【16年3月到至今】,包括Java基礎及深入知識點,Android技術部落格,Python學習筆記等等,還包括平時開發中遇到的bug彙總,當然也在工作之餘收集了大量的面試題,長期更新維護並且修正,持續完善……開源的檔案是markdown格式的!同時也開源了生活部落格,從12年起,積累共計47篇[近20萬字],轉載請註明出處,謝謝!
  • 如果覺得好,可以star一下,謝謝!當然也歡迎提出建議,萬事起於忽微,量變引起質變!

1.事件機制簡單介紹
1.1 觸控事件

/**
* 觸控事件
* 如果返回結果為false表示不消費該事件,並且也不會截獲接下來的事件序列,事件會繼續傳遞
* 如果返回為true表示當前View消費該事件,阻止事件繼續傳遞
*
* 在這裡要強調View的OnTouchListener。如果View設定了該監聽,那麼OnTouch()將會回撥。
* 如果返回為true那麼該View的OnTouchEvent將不會在執行 這是因為設定的OnTouchListener執行時的優先順序要比onTouchEvent高。
* 優先順序:OnTouchListener > onTouchEvent > onClickListener
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
    Log.e("onEvent","MyLinearLayout onTouchEvent");
    return super.onTouchEvent(event);
}

1.2 分發事件

/**
* 分發事件
* 根據內部攔截狀態,向其child或者自己分發事件
*
* @param ev
* @return
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    Log.e("onEvent","MyLinearLayout dispatchTouchEvent");
    return super.dispatchTouchEvent(ev);
}

1.3攔截事件

/**
* 攔截事件
* 預設實現是返回false,也就是預設不攔截任何事件
*
* 判斷自己是否需要擷取事件
* 如果該方法返回為true,那麼View將消費該事件,即會呼叫onTouchEvent()方法
* 如果返回false,那麼通過呼叫子View的dispatchTouchEvent()將事件交由子View來處理
* @param ev
* @return
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    Log.e("onEvent","MyLinearLayout onInterceptTouchEvent");
    return super.onInterceptTouchEvent(ev);
}

2.解決滑動衝突的思路及方法
2.1 第一種情況,滑動方向不同
Image.png
2.2 第二種情況,滑動方法相同
Image.png
2.3 第三種情況,以上兩種情況巢狀
Image.png
3.案例解決方法
3.1 針對2問題的解決思路

看了上面三種情況,我們知道他們的共同特點是 父View 和 子View 都想爭著響應我們的觸控事件,但遺憾的是我們的觸控事件 同一時刻 只能被某一個View或者ViewGroup攔截消費,所以就產生了滑動衝突?那既然同一時刻只能由某一個View或者ViewGroup消費攔截,那我們就只需要 決定在某個時刻由這個View或者ViewGroup攔截事件,另外的 某個時刻由 另外一個View或者ViewGroup攔截事件不就OK了嗎?綜上,正如 在 《Android開發藝術》 一書提出的,總共 有兩種解決方案

**3.2 滑動方向不同,解決衝突的外部解決法【**以ScrollView與ViewPager為例

舉例子:以ScrollView與ViewPager為例
從 父View 著手,重寫 onInterceptTouchEvent 方法,在 父View 需要攔截的時候攔截,不要的時候返回false,程式碼大概如下

public class MyScrollView extends ScrollView {

    public MyScrollView(Context context) {
        super(context);
    }

    public MyScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @TargetApi(21)
    public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    private float mDownPosX = 0;
    private float mDownPosY = 0;
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final float x = ev.getX();
        final float y = ev.getY();
        final int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mDownPosX = x;
                mDownPosY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                final float deltaX = Math.abs(x - mDownPosX);
                final float deltaY = Math.abs(y - mDownPosY);
                // 這裡是夠攔截的判斷依據是左右滑動,讀者可根據自己的邏輯進行是否攔截
                if (deltaX > deltaY) {
                    return false;
                }
        }
        return super.onInterceptTouchEvent(ev);
    }
}

**3.3 滑動方向不同,解決衝突的內部解決法【**以ScrollView與ViewPager為例

從子View著手,父View 先不要攔截任何事件,所有的 事件傳遞給 子View,如果 子View 需要此事件就消費掉,不需要此事件的話就交給 父View 處理。
實現思路 如下,重寫 子View 的 dispatchTouchEvent 方法,在 Action_down 動作中通過方法 requestDisallowInterceptTouchEvent(true) 先請求 父View 不要攔截事件,這樣保證 子View 能夠接受到Action_move事件,再在Action_move動作中根據 自己的邏輯是否要攔截事件,不要的話再交給 父View 處理

public class MyViewPager extends ViewPager {

    private static final String TAG = "yc";

    int lastX = -1;
    int lastY = -1;

    public MyViewPager(Context context) {
        super(context);
    }

    public MyViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int x = (int) ev.getRawX();
        int y = (int) ev.getRawY();
        int dealtX = 0;
        int dealtY = 0;
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                dealtX = 0;
                dealtY = 0;
                // 保證子View能夠接收到Action_move事件
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                dealtX += Math.abs(x - lastX);
                dealtY += Math.abs(y - lastY);
                Log.i(TAG, "dealtX:=" + dealtX);
                Log.i(TAG, "dealtY:=" + dealtY);
                // 這裡是夠攔截的判斷依據是左右滑動,讀者可根據自己的邏輯進行是否攔截
                if (dealtX >= dealtY) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                } else {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_CANCEL:
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }
}

3.4 ViewPager巢狀ViewPager內部解決法

從 子View ViewPager著手,重寫 子View 的 dispatchTouchEvent方法,在 子View 需要攔截的時候進行攔截,否則交給 父View 處理,程式碼如下

public class ChildViewPager extends ViewPager {

    private static final String TAG = "yc";
    public ChildViewPager(Context context) {
        super(context);
    }

    public ChildViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int curPosition;
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                curPosition = this.getCurrentItem();
                int count = this.getAdapter().getCount();
                Log.i(TAG, "curPosition:=" +curPosition);
                // 噹噹前頁面在最後一頁和第0頁的時候,由父親攔截觸控事件
                if (curPosition == count - 1|| curPosition==0) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                } else {//其他情況,由孩子攔截觸控事件
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
        }
        return super.dispatchTouchEvent(ev);
    }
}

3.5 滑動方向相同,解決衝突的外部解決法【解決ScrollView和RecycleView滑動衝突】

public class RecyclerScrollview extends ScrollView {

    private int downY;
    private int mTouchSlop;

    public RecyclerScrollview(Context context) {
        super(context);
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    public RecyclerScrollview(Context context, AttributeSet attrs) {
        super(context, attrs);
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    public RecyclerScrollview(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        int action = e.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                downY = (int) e.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                int moveY = (int) e.getRawY();
                if (Math.abs(moveY - downY) > mTouchSlop) {
                    return true;
                }
        }
        return super.onInterceptTouchEvent(e);
    }

}

**注意:RecycleView一定要被巢狀裡面**
<!-- descendantFocusability該屬性是當一個為view獲取焦點時,定義viewGroup和其子控制元件兩者之間的關係。
beforeDescendants:viewgroup會優先其子類控制元件而獲取到焦點
afterDescendants:viewgroup只有當其子類控制元件不需要獲取焦點時才獲取焦點
blocksDescendants:viewgroup會覆蓋子類控制元件而直接獲得焦點-->

<RelativeLayout
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:descendantFocusability="blocksDescendants">
<android.support.v7.widget.RecyclerView
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>
</RelativeLayout>

**3.6 解決ScrollView和ViewPager,RecycleView滑動衝突**

對於ScrollView
public class BottomScrollView extends ScrollView {

    private OnScrollToBottomListener mOnScrollToBottomListener;

    public BottomScrollView(Context context) {
        super(context);
    }

    public BottomScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public BottomScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public BottomScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt){
        super.onScrollChanged(l,t,oldl,oldt);
        // 滑動的距離加上本身的高度與子View的高度對比
        if(t + getHeight() >=  getChildAt(0).getMeasuredHeight()){
            // ScrollView滑動到底部
            if(mOnScrollToBottomListener != null) {
                mOnScrollToBottomListener.onScrollToBottom();
            }
        } else {
            if(mOnScrollToBottomListener != null) {
                mOnScrollToBottomListener.onNotScrollToBottom();
            }
        }
    }

    public void setScrollToBottomListener(OnScrollToBottomListener listener) {
        this.mOnScrollToBottomListener = listener;
    }

    public interface OnScrollToBottomListener {
        void onScrollToBottom();
        void onNotScrollToBottom();
    }
}

**// ViewPager滑動衝突解決**
mViewPager.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        int action = event.getAction();
        if(action == MotionEvent.ACTION_DOWN) {
            // 記錄點選到ViewPager時候,手指的X座標
            mLastX = event.getX();
        }
        if(action == MotionEvent.ACTION_MOVE) {
            // 超過閾值,禁止SwipeRefreshLayout下拉重新整理,禁止ScrollView截斷點選事件
            if(Math.abs(event.getX() - mLastX) > THRESHOLD_X_VIEW_PAGER) {
                mRefreshLayout.setEnabled(false);
                mScrollView.requestDisallowInterceptTouchEvent(true);
            }
        }
        // 使用者擡起手指,恢復父佈局狀態
        if(action == MotionEvent.ACTION_UP) {
            mRefreshLayout.setEnabled(true);
            mScrollView.requestDisallowInterceptTouchEvent(false);
        }
        return false;
    }
});

**// ListView滑動衝突解決**
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;
    }
});