美文网首页Android 经典笔记Android事件分发机制Android自定义View
Android 经典笔记之四: 事件冲突解决思路与方案

Android 经典笔记之四: 事件冲突解决思路与方案

作者: 杨充211 | 来源:发表于2017-09-12 17:47 被阅读167次

    事件冲突解决思路与方案
    目录介绍
    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滑动冲突


    Demo
    https://github.com/yangchong211/YCEventConflict

    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;
        }
    });
    

    后续:
    平时喜欢写写文章,笔记。别人建议我把笔记,以前写的东西整理,然后写成博客,所以我会陆续整理文章,只发自己写的东西,敬请期待:
    知乎:https://www.zhihu.com/people/yang-chong-69-24/pins/posts
    领英:https://www.linkedin.com/in/chong-yang-049216146/
    简书:http://www.jianshu.com/u/b7b2c6ed9284
    csdn:http://my.csdn.net/m0_37700275
    网易博客:http://yangchong211.blog.163.com/
    新浪博客:http://blog.sina.com.cn/786041010yc
    github:https://github.com/yangchong211
    喜马拉雅听书:http://www.ximalaya.com/zhubo/71989305/
    脉脉:yc930211
    开源中国:https://my.oschina.net/zbj1618/blog
    邮箱:yangchong211@163.com

    相关文章

      网友评论

      • maxcion:又来这里骚扰您了。
        特别特别想问一下。在3.2的代码块里面,通过滑动距离判断是否拦截方案中。获取滑动距离的方法是,在down事件中记录位置。在move事件中用当前位置 - down事件位置,获得距离。

        但是在3.3的代码块中获取滑动距离的方案是:在事件结束后记录last位置,然后在move事件中通过相减获取滑动距离。

        这两种方案有啥不同吗?我发现内部拦截代码用的好像都是后者,外部拦截用的都是前者。想知道为什么。麻烦您了。

      本文标题:Android 经典笔记之四: 事件冲突解决思路与方案

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