美文网首页
Android View 事件体系笔记 (三):View滑动冲突

Android View 事件体系笔记 (三):View滑动冲突

作者: Marker_Sky | 来源:发表于2017-11-27 15:40 被阅读0次
    目录

    一、View 滑动冲突背景

    1.1 View 滑动冲突常见场景

    • 场景1 —— 外部和内部横竖滑动交错冲突
    • 场景2 —— 外部和内部同向滑动冲突
    • 场景3 —— 以上两种情况的嵌套

    举个栗子:

    • 场景1 —— 横向 ScrollView 嵌套 ListView。类似 ViewPager 和 Fragment,但是 ViewPager 内部处理了这种滑动冲突。
    • 场景2 —— 竖向 ScrollView 嵌套 ListView。
    • 场景3 —— TabLayout 嵌套多个层级的 ViewPager 以及 ListView。

    1.2 View 滑动冲突处理规则

    针对以上滑动冲突,处理规则如下:

    • 场景1 —— 根据用户手势坐标判断左右或者上下滑动,再分别交给响应的 View 处理;
    • 场景2 —— 根据一定的业务逻辑,比如外部 View 没有滑动到底部,则禁止内部 View 滑动;
    • 场景3 —— 根据具体的业务逻辑处理。

    二、View 滑动冲突解决方式

    2.1 外部拦截法

    点击事件先经过父容器处理,如果不需要处理再交给子 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;
        }
    

    case MotionEvent.ACTION_DOWN:必须返回 false,一旦返回 true 拦截事件,后续则不会再传递给子 View;
    case MotionEvent.ACTION_MOVE:根据相应规则处理事件;
    case MotionEvent.ACTION_UP:拦截后子 View 无法处理 onClick 事件。一旦父容器拦截事件,即使 onInterceptTouchEvent 返回 false,ACTION_UP 作为最后一个事件也会传递给父容器。

    2.2 内部拦截法

    父容器不拦截任何事件,子元素需要则处理,不需要则交给父容器处理。重写子元素的 onInterceptTouchEvent 方法来影响父容器拦截事件。
    同时父容器的 onInterceptTouchEvent 中拦截除 ACTION_DOWN 事件外的其他事件,这是为了让子元素通过 requestDisallowInterceptTouchEvent(true) 交给父容器处理的事件生效。

    public boolean onInterceptTouchEvent(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-mLastXIntercept;
                int deltaY=y=mLastYIntercept;
                if(父容器需要拦截的事件){
                // 告知父容器需要拦截此事件
                parent.requestDisallowInterceptTouchEvent(false); 
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                intercepted = false;
                break;
            }
            default:
                break;
            }
            mLastXIntercept=x;
            mLastYIntercept=y;
            return super.dispathTouchEvent(event);
        }
    

    三、View 滑动冲突解决实例

    3.1 外部拦截法处理横竖交错冲突

    (1)创建横向滑动 View 作为父容器

    public class HorizontalScrollViewEx extends ViewGroup{
        ...
        public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
            super(context, attrs);
            mScroller = new Scroller(context);
        }
        ...
    }
    

    (2)布局文件添加该 View

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical" android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <com.sky.viewtest.widget.HorizontalScrollViewEx
            android:id="@+id/container"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    
    </LinearLayout>
    

    (3)Activity 向父容器添加 ListView 模拟滑动冲突

    private HorizontalScrollViewEx mListContainer;
    
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_conllision_view);
        mListContainer = (HorizontalScrollViewEx) findViewById(R.id.container);
        // 添加三个 listView 并添加到 HorizontalScrollViewEx
        for (int i = 0; i < 3; i++) {
            ListView listView = new ListView(this);
            List<String> list =  new ArrayList<>();
            for (int i1 = 0; i1 < 50; i1++) {
                list.add("page" + i + ", name: " + i1);
            }
            ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, list);
            listView.setAdapter(adapter);
            mListContainer.addView(listView);
        }
    }
    

    (4)重写父容器 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:
                mXDown = ev.getRawX();
                mXLastMove = mXDown;
                // 默认返回 false,因为一旦拦截,则无法传递给子 View
                intercepted = false;
                // 如果滑动没有完成,就继续由父控件处理
                if(!mScroller.isFinished()){
                    mScroller.abortAnimation();
                    intercepted = true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                mXMove = ev.getRawX();
                mXLastMove = mXMove;
    
                int deltaX = x - mLastXIntercept;
                int deltaY = y - mLastYIntercept;
                // 如果左右滑动幅度大于上下,是左右滑动则拦截事件
                if(Math.abs(deltaX) > Math.abs(deltaY)){
                    intercepted = true;
                }else{
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
        }
    
        mLastXIntercept = x;
        mLastYIntercept = y;
    
        return intercepted;
    }
    

    case MotionEvent.ACTION_DOWN:默认返回 false,因为一旦返回 true 拦截事件,后面就没子 View 什么事了。
    case MotionEvent.ACTION_MOVE:这里进行判定,如果左右滑动幅度大于上下,是左右滑动则拦截事件由父容器作处理

    (5)重写父容器 onTouchEvent 方法,处理父容器拦截事件后的一系列操作

    // 如果父 View 拦截事件则自己进行处理
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                if(!mScroller.isFinished()){
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                mXMove = event.getRawX();
                int scrolledX = (int) (mXLastMove - mXMove);
                if(getScrollX() + scrolledX < mLeftBorder){
                    scrollTo(mLeftBorder, 0);
                    return true;
                }else if(getScrollX() + getWidth() + scrolledX > mRightBorder){
                    scrollTo(mRightBorder - getWidth(), 0);
                    return true;
                }
                scrollBy(scrolledX, 0);
                mXLastMove = mXMove;
                break;
            case MotionEvent.ACTION_UP:
                int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
                int dx = targetIndex * getWidth() - getScrollX();
                mScroller.startScroll(getScrollX(), 0 , dx, 0);
                invalidate();
                break;
        }
        return super.onTouchEvent(event);
    }
    

    上文仅列出主要代码,全部代码可阅读参考资料

    3.2 内部拦截法处理横竖交错冲突

    父容器拦截除 ACTION_DOWN 以外的所有事件,子元素需要拦截事件时,使用 getParent().requestDisallowInterceptTouchEvent(true) 方法来请求父容器不拦截事件。当某些事件需要父容器处理时,使用 getParent().requestDisallowInterceptTouchEvent(false)

    (1)自定义 ListView 作为子 View,重写 dispatchTouchEvent 方法

    public class MyListView extends ListView {
    
        private int mLastX;
        private int mLastY;
    
        public MyListView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            int x = (int) ev.getX();
            int y = (int) ev.getY();
            switch (ev.getAction()){
                case MotionEvent.ACTION_DOWN:
                    // 要求父控件不拦截事件
                    getParent().requestDisallowInterceptTouchEvent(true);
                    break;
                case MotionEvent.ACTION_MOVE:
                    int deltaX = x - mLastX;
                    int deltaY = y - mLastY;
                    // 如果是左右滑动
                    if(Math.abs(deltaX) > Math.abs(deltaY)){
                        // 要求父控件拦截事件
                        getParent().requestDisallowInterceptTouchEvent(false);
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    break;
            }
            mLastX = x;
            mLastY = y;
            return super.dispatchTouchEvent(ev);
        }
    }
    

    case MotionEvent.ACTION_DOWN:中使用 requestDisallowInterceptTouchEvent 强行时父容器不拦截事件
    case MotionEvent.ACTION_MOVE:横向滑动时,再让父容器处理相应事件

    (2)复制 HorizontalScrollViewEx 重命名 HorizontalScrollViewEx2,并修改 onInterceptTouchEvent 方法拦截除 ACTION_DOWN 以外的所有事件。

    @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:
                mXDown = ev.getRawX();
                mXLastMove = mXDown;
    
                intercepted = false;
                // 如果滑动没有完成,就继续由父控件处理
                if(!mScroller.isFinished()){
                    mScroller.abortAnimation();
                    intercepted = true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                mXMove = ev.getRawX();
                mXLastMove = mXMove;
    
                intercepted = true;
                break;
            case MotionEvent.ACTION_UP:
                intercepted = true;
                break;
        }
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercepted;
    }
    

    (3)修改 Activity 使用新的 父容器和 ListView

    mListContainer = (HorizontalScrollViewEx2) findViewById(R.id.container);
    ...
    MyListView listView = new MyListView(this,null);
    

    实例代码地址

    参考资料:

    《Android开发艺术探索》
    Android 从0开始自定义控件之 View 的滑动冲突详解(四)

    相关文章

      网友评论

          本文标题:Android View 事件体系笔记 (三):View滑动冲突

          本文链接:https://www.haomeiwen.com/subject/emurbxtx.html