Android 解決 View 的滑動衝突

關於 Android 的 TouchEvent 事件分發機制可以看這裏:Java_Android_Learn,本文講解的是如何去解決 View 之間的滑動衝突

當父容器與子 View 都可以滑動時,就會產生滑動衝突。例如 ViewPager 中包含了 ListView 時,ViewPager 可以橫向滑動,而 ListView 可以豎向滑動,此時就會產生滑動衝突。而我們之所以在使用的過程中沒有發現這個問題,是因爲 ViewPager 內部已經處理好滑動衝突了

解決 View 之間的滑動衝突的方法分爲兩種,分別是外部攔截法和內部攔截法

一、外部攔截法

父容器根據需要在 onInterceptTouchEvent 方法中對觸摸事件進行選擇性攔截,思路可以看以下僞代碼

    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                intercepted = false;
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                if (滿足父容器的攔截要求) {
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                intercepted = false;
                break;
            }
            default:
                break;
        }
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercepted;
    }
  • 根據實際的業務需求,判斷是否需要處理 ACTION_MOVE 事件,如果父 View 需要處理則返回 true,否則返回 false 並交由子 View 去處理
  • ACTION_DOWN 事件需要返回 false,父容器不能進行攔截,否則根據 View 的事件分發機制,後續的 ACTION_MOVE 與 ACTION_UP 事件都將默認交由父容器進行處理
  • 原則上 ACTION_UP 事件也需要返回 false,如果返回 true,那麼子 View 將接收不到 ACTION_UP 事件,子 View 的onClick 事件也無法觸發

二、內部攔截法

內部攔截法則是要求父容器不攔截任何事件,所有事件都傳遞給子 View,子 View 根據需求判斷是自己消費事件還是傳回給父容器進行處理,思路可以看以下僞代碼:

子 View 修改其 dispatchTouchEvent 方法

    public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                parent.requestDisallowInterceptTouchEvent(true);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if (父容器需要此類點擊事件) {
                    parent.requestDisallowInterceptTouchEvent(false);
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                break;
            }
            default:
                break;
        }
        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
    }

父容器修改其 onInterceptTouchEvent 方法

    public boolean onInterceptTouchEvent(MotionEvent event) {
        int action = event.getAction();
        if (action == MotionEvent.ACTION_DOWN) {
            return false;
        } else {
            return true;
        }
    }
  • 內部攔截法要求父容器不能攔截 ACTION_DOWN 事件,否則一旦父容器攔截 ACTION_DOWN 事件,那麼後續的觸摸事件都不會傳遞給子View
  • 滑動策略的邏輯放在子 View 的 dispatchTouchEvent 方法的 ACTION_MOVE 事件中,如果父容器需要處理事件則調用 parent.requestDisallowInterceptTouchEvent(false) 方法讓父容器去攔截事件

三、滑動衝突實例

這裏以 ViewPager 作爲父容器,看看 ViewPager 與其內部 View 之間的滑動衝突情況

爲了使 ViewPager 不處理滑動衝突,這裏來重寫其 onInterceptTouchEvent() 方法

/**
 * 作者:葉應是葉
 * 時間:2018/7/15 10:26
 * 描述:
 */
public class MyViewPager extends ViewPager {

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

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

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }

}

這裏用一個布爾變量來控制 ViewPager 中每一個頁面包含的是 ListView 還是 TextView

public class MainActivity extends AppCompatActivity {

    private List<View> viewList;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewPager viewPager = findViewById(R.id.viewPager);
        viewList = new ArrayList<>();
        initData(false);
        viewPager.setAdapter(new MyPagerAdapter(viewList));
    }

    private void initData(boolean flag) {
        for (int j = 0; j < 4; j++) {
            View view;
            if (flag) {
                ListView listView = new ListView(this);
                List<String> dataList = new ArrayList<>();
                for (int i = 0; i < 30; i++) {
                    dataList.add("leavesC " + i);
                }
                ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, dataList);
                listView.setAdapter(adapter);
                view = listView;
            } else {
                TextView textView = new TextView(this);
                textView.setGravity(Gravity.CENTER);
                textView.setText("leavesC " + j);
                view = textView;
            }
            viewList.add(view);
        }
    }

}

當子 View 爲 TextView 時


然而此時還是沒有發生滑動衝突,ViewPager 還是可以正常使用。這是因爲 TextView 默認是不可點擊的,因此 TextView 並不會消費觸摸事件,觸摸事件最後還是傳回給 ViewPager 進行處理,因爲此時還是可以正常使用

如果爲 TextView 設置 textView.setClickable(true);,就會使得 ViewPager 無法滑動

當子 View 爲 ListView 時,則只能上下滑動,而無法左右滑動

四、通過外部攔截法解決滑動衝突

外部攔截法僅需要修改父容器的 onInterceptTouchEvent() 方法即可,通過滑動時橫向滑動距離與豎向滑動距離之間的大小,判斷是否在進行左右滑動,如果判斷出當前是滑動操作,則使 ViewPager 消費該事件

/**
 * 作者:葉應是葉
 * 時間:2018/7/15 10:26
 * 描述:
 */
public class MyViewPager extends ViewPager {

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

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

    private int lastXIntercept;

    private int lastYIntercept;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        final int action = ev.getAction() & MotionEvent.ACTION_MASK;
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                //不攔截此事件
                intercepted = false;
                //調用 ViewPager的 onInterceptTouchEvent 方法用於初始化 mActivePointerId
                super.onInterceptTouchEvent(ev);
                break;
            case MotionEvent.ACTION_MOVE:
                //橫向位移增量
                int deltaX = x - lastXIntercept;
                //豎向位移增量
                int deltaY = y - lastYIntercept;
                //如果橫向滑動距離大於豎向滑動距離,則認爲使用者是想要左右滑動
                //此時就使 ViewPager 攔截此事件
                intercepted = Math.abs(deltaX) > Math.abs(deltaY);
                break;
            case MotionEvent.ACTION_UP:
                //不攔截此事件
                intercepted = false;
                break;
            default:
                break;
        }
        lastXIntercept = x;
        lastYIntercept = y;
        return intercepted;
    }

}

五、通過內部攔截法解決滑動衝突

內部攔截法需要重寫 ListView 的 dispatchTouchEvent 方法

/**
 * 作者:葉應是葉
 * 時間:2018/7/15 12:40
 * 描述:
 */
public class MyListView extends ListView {

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

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

    private int lastX;

    private int lastY;

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        final int action = ev.getAction() & MotionEvent.ACTION_MASK;
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                //橫向位移增量
                int deltaX = x - lastX;
                //豎向位移增量
                int deltaY = y - lastY;
                //如果橫向滑動距離大於豎向滑動距離,則認爲使用者是想要左右滑動
                //此時就通知父容器 ViewPager 處理此事件
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }
        lastX = x;
        lastY = y;
        return super.dispatchTouchEvent(ev);
    }

}

同時也需要修改 MyViewPager 的 onInterceptTouchEvent 方法

/**
 * 作者:葉應是葉
 * 時間:2018/7/15 10:26
 * 描述:
 */
public class MyViewPager extends ViewPager {

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

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

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getAction() & MotionEvent.ACTION_MASK;
        if (action == MotionEvent.ACTION_DOWN) {
            super.onInterceptTouchEvent(ev);
            return false;
        }
        return true;
    }

}

更多的學習筆記看這裏:Java_Android_Learn

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章