3.5View的滑动冲突

没来的日子里在忙着写项目然后看书,习惯了把笔记整理到typora里面,今天突然发现好久没写博客了,打算痛改前非,从今日做起!把每天的笔记在博客上也贴一份,以此来记录每天的学习进度并且督促自己。学的有点慢但在爬行中…
最近在学习Android开发艺术探索,整理笔记。

界面中只要内外两层同时可以滑动,这个时候就会产生滑动冲突

3.5.1常见的滑动冲突场景

常见的滑动一般有三个方面:

  • 外部滑动方向和内部滑动方向不一致;
  • 外部滑动方向和内部滑动方向一致;
  • 上面两种情况的嵌套。

img

场景一:ViewPager+Fragment配合使用组成的页面滑动效果;左右滑动来切换页面,而每个页面内部往往又是一个Listview;ViewPager内部处理了这种滑动冲突。如果我们采用的不是ViewPager而是ScrollView等,那就必须手动处理滑动冲突了,否则造成的后果就是内外两层只能有一层能够滑动。除了这种情况,还有外部上下滑动、内部左右滑动等。
场景三:场景3是场景1和场景2两种情况的嵌套。比如外部有一个SlidingMenu效果,然后内部有一个ViewPager,ViewPager的每一个页面中又是一个Listview。只需要分别处理内层和中层、中层和外层之间的滑动冲突即可。

3.5.2滑动冲突的处理规则

对于场景1,它的处理规则是:当用户左右滑动时,需要让外部的View拦截点击事件,当用户上下滑动时,需要让内部View拦截点击事件。具体来说:根据滑动是水平滑动还是竖直滑动来判断到底由谁来拦截事件,可以通过水平和竖直方向的距离差来判断。也可以依据滑动路径和水平方向做形成的夹角。

img

对于场景2、3来说,根据业务进行判断和拦截。

3.5.3滑动冲突的解决方式

1.外部拦截法

外部拦截法是指点击事件都先经过父容器的拦截处理,如果父容器需要这个事件就给他,比较类似事件的分发机制,需要重写父容器的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:
                intercepted = false;
                break;
            case MotionEvent.ACTION_MOVE:
                if(父容器需要当前的点击事件){
                    intercepted = true;
                }else {
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
        }
        mLastXIntercept = x;
        mLastYIntercept = x;
        return intercepted;
}

这里对上述代码再描述一下,在onInterceptTouchEvent方法中,首先是ACTION_DOWN这个事件,父容器必须返回false,即不拦截ACTION_DOWN事件,这是因为一旦父容器拦截了ACTION_DOWN,那么后续的ACTION_MOVE和ACTION_UP事件都会直接交由父容器处理,这个时候事件没法再传递给子元素了;其次是ACTION_MOVE事件,这个事件可以根据需要来决定是否拦截,如果父容器需要拦截就返回true,否则返回false;ACTION_UP必须返回false,假设事件交给子元素处理,如果父容器这里返回true,那么子元素就无法接收到ACTION_UP,onClick就无法触发,但是父容器比较特殊,一旦它开始拦截任何一个事件,那么后续的事件都会交给它处理,而ACTION_UP也必定可以传递给父容器,即使他返回了false。

2.内部拦截法

内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素要消耗此事件就直接消耗掉,否则就交由父容器进行处理,这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,需要重写子元素的dispatchTouchEvent方法。

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX =  x - mLastX;
                int deltaY =  x - mLastY;
                if(父容器需要拦截事件){
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
 
                break;
        }
        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
}

除了子元素需要处理之外,父元素默认也要拦截除ACTION_DOWN之外的其他事件,这样当子元素调用getParent().requestDisallowInterceptTouchEvent(true)方法时,父元素才能继续拦截所需要的事件。

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

场景一示例

考虑到一种情况,如果此时用户正在水平滑动,但是水平滑动停止之前如果用户再迅速的进行竖直滑动,就会导致界面在水平滑动无法滑动到终点,而处于一种中间状态,为了避免这种不友好的体验,我们水平正在滑动的时候,下一个序列的点击仍然交给父容器。

//场景一的外部拦截法
//重点是父容器的onInterceptTouchEvent()
public class HorizontalScrollViewEx extends ViewGroup {
    private int mChindrensize;
    private int mChindrenWidth;
    private int mChindrenIndex;
    //分别记录上次滑动的坐标
    private int mLastX = 0;
    private int mLastY = 0;
    //分别记录上次滑动的坐标(onInterceptTouchEvent)
    private int mLastXIntercept = 0;
    private int mLastYIntercept = 0;
    private Scroller mScroller;
    private VelocityTracker mVelocityTracker;
    ....
    @Override
    public boolean onInterceptHoverEvent(MotionEvent event) {
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercepted = true;
                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)) {  //判断水平方向和垂直方向移动距离
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
        }
        mLastX = x;
        mLastY = y;
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercepted;
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mVelocityTracker.addMovement(event);
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                scrollBy(-deltaX, 0);
                break;
            //根据用户的滑动速度和位置来决定滚动到哪个子视图,并在用户停止触摸时平滑的滚动到那个位置
            case MotionEvent.ACTION_UP:
                int scrollX = getScrollX();
                int scrollToChildIndex = scrollX / mChindrenWidth;
                mVelocityTracker.computeCurrentVelocity(1000);
                float xVelocity = mVelocityTracker.getXVelocity();
                if (Math.abs(xVelocity) >= 50) {
                    mChindrenIndex = xVelocity > 0 ? mChindrenIndex - 1 : mChindrenIndex + 1;
                } else {
                    mChindrenIndex = (scrollX + mChindrenWidth / 2) / mChindrenWidth;
                }
                mChindrenIndex = Math.max(0, Math.min(mChindrenIndex, mChindrensize - 1));
                int dx = mChindrenIndex * mChindrenWidth - scrollX;
                ssmoothScrollBy(dx, 0);
                mVelocityTracker.clear();
                break;
        }
        mLastX = x;
        mLastY = y;
        return true;
    }
    private void ssmoothScrollBy(int dx, int i) {
        mScroller.startScroll(getScrollX(),0,dx,500);
        invalidate();
    }
    @Override
    public void computeScroll() {
        if(mScroller.computeScrollOffset()){
            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            postInvalidate();
        }
    }
} 

内部拦截:只需要修改ListView的dispatchTouchEvent方法中的父容器的拦截逻辑,同时让父拦截MOVE和Up事件即可。为了重写Listview的dispatchTouchfvent方法,我们必须自定义一个ListView,称为ListViewEx,然后对内部拦截法的模板代码进行修改。

public class ListViewEx extends ListView {
    public static final String TAG = "ListViewEx";
    private HorizontalScrollViewEx mHorizontalScrollViewEx;
    //记录上次滑动的坐标
    private int mLastX = 0;
    private int mLastY = 0;
    ...
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mHorizontalScrollViewEx.requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                int delatX = x - mLastX;
                int delatY = y - mLastY;
                if (Math.abs(delatX) > Math.abs(delatY)) {
                    mHorizontalScrollViewEx.requestDisallowInterceptTouchEvent(false);
                }
                break;
        }
        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(ev);
    }
}
 
 
//除了对子容器的修改,我们还需要修改父容器的onInterceptTouchEvent()
 @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        int action = event.getAction();
        if(action == MotionEvent.ACTION_DOWN){
            mLastX = x;
            mLastY = y;
            if(!mScroller.isFinished()){
                mScroller.abortAnimation();
                return true;
            }
            return false;
        }else {
            return true;
        }
    }

场景二示例

提供一个可以上下滑动的父容器,这里就叫 StickyLayout,它看起来就像是可以上下滑动的竖直的LinearLayout,然后在它的内部中的滑动冲突了。当然这个StickyLayout是有滑动规则的:当Header显示时或者ListView 滑动到顶部时,由StickyLayout拦截事件:当Header隐藏时,这要分情况,如果Listview已经滑动到顶部并且当前手势是向下滑动的话,这个时候还是StickyLayout拦截事件,其他情况则由ListView拦截事件。

//我们主要看它onInterceptTouchEvent中对move的处理
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastXIntercept;
                int deltaY = y - mLastYIntercept;
                if (mDisallowInterceptTouchEventOnHeader && y <= getHeaderHeight()) {
                    intercepted = 0;
                } else if (Math.abs(deltaY) <= Math.abs(deltaX)) {
                    intercepted = 0;
                } else if (mStatus == STATUS_EXPANDED && deltaY <= -mTouchSlop) {
                    intercepted = 1;
                } else if (mGiveUpTouchEventListener != null) {
                    if (mGiveUpTouchEventListener.giveUpTouchEvent(event) && deltaY >= mTouchSlop) {
                        intercepted = 1;
                    }
                }

当事件落在Header上面时,就不会拦截该事件;接着,如果竖直距离差小于水平距离差,那么父容器也不会拦截该事件;当Header是展开状态并且是向上滑动时,父容器拦截事件;当ListView滑动到顶部并且向下滑动的时候,父容器也会拦截事件。

  • 15
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值