Android——View的事件体系(三)View的滑动冲突

主要介绍内容:

  • View的滑动冲突
    • 常见的滑动冲突场景
    • 滑动冲突的处理规则
    • 滑动冲突的解决方式

上一篇博客中我们已经介绍了 View 的事件分发机制,想要了解的请戳这里:Android——View事件体系(二)View的事件分发机制 本篇我们将详细介绍 View的滑动冲突以及常见的解决方式。

View的滑动冲突

本篇开始介绍 View 体系中一个深入的话题:滑动冲突。那么滑动冲突是如何产生的呢?其实在界面中只要内外两层同时可以滑动,这个时候就会产生滑动冲突。如何解决滑动冲突呢?这既是一件困难又是一件简单的事,说困难是因为许多开发者面对滑动冲突都会显得束手无策,说简单是因为滑动冲突的解决有固定的套路,只要知道了这个固定套路问题就好解决了。

常见的滑动冲突场景

常见的滑动冲突场景可以简单分为如下三种:

  • 场景1——外部滑动方向和内部滑动方向不一致;
  • 场景2——外部滑动方向和内部滑动方向一致;
  • 场景3——上面两种情况的嵌套。

如图所示:

这里写图片描述

先说场景1,主要是将 ViewPager 和 Fragment 配合使用所组成的页面滑动效果,主流应用几个都会使用这个效果。在这种效果中,可以通过左右滑动来切换页面,而每个页面内部往往又是一个 LIstView。本来这种情况下是有滑动冲突的,但是 ViewPager 内部处理了这种滑动冲突,因此采用 ViewPager 是我们无须关注这个问题,如果我们采用的不是 ViewPager 而是 ScrollView 等,那就必须手动处理滑动冲突了,否则造成的后果就是内外两层只能有一层能够滑动,这是因为两者之间的滑动事件有冲突。除了这种典型情况外,还存在其他情况,比如外部上下滑动,内部左右滑动等,但是它们属于同一类滑动冲突。

再说场景2,这种情况稍微复杂一些,当内外两层都在同一个方向可以滑动的时候,显然存在逻辑问题,因为当手指开始滑动的时候,系统无法知道用户到底是想让哪一层滑动,所以当手指滑动的时候就会出现问题,要么只有一层能滑动,要么就是内外两层都滑动得很卡顿。在实际的开发中,这种场景主要是指内外两层同时能上下滑动或者内外两层同时能左右滑动。

最后说下场景3,场景3是场景1和场景2两种情况的嵌套,因此场景3的滑动冲突看起来就更复杂了。比如在许多应用中会有这么一个效果:内层有一个场景1中的滑动效果,然后外层又有一个场景2中的滑动效果。具体说就是,外部有一个 SlideMenu 效果,然后内部有一个 ViewPager, Viewpager中的每一个页面中又是一个ListView。虽然说场景3的滑动冲突看起来更复杂,但是它是几个单一的滑动冲突的叠加,因此只需要分别处理内层和中层、中层和外层之间的滑动冲突即可,而具体的处理方法其实就是和场景1、场景2相同的。

从本质上来说,这三种滑动冲突场景的复杂读其实是相同的,因为它们的区别仅仅是滑动策略的不同,至于解决滑动冲突的方法,它们几个是通用的,我们在下面会详细介绍这个问题。

滑动冲突的处理规则

一般来说,不管滑动冲突多么的复杂,它都有既定的规则,根据这些规则我们就可以选择合适的方法来处理。

对于场景1,它的处理规则就是:当用户左右滑动是,需要让外部的 View 拦截点击事件,当用户上下滑动时,需要让内部 View 拦截点击事件。这个时候我们就可以根据它们的特征来解决滑动冲突,具体来说是:根据滑动是水平滑动还是竖直滑动来判断到底由谁来拦截事件,如下图所示,根据滑动过程中两个点之间的坐标就可以得出到底是水平滑动还是竖直滑动。如何根据坐标来得到滑动的方向呢?这个很简单,有很多可以参考,比如可以依据滑动路径和水平方向所形成的夹角,也可以根据水平方向和竖直方向上的距离差来判断,某些特殊时候还可以依据水平和竖直方向的速度差来做判断。这里我们可以通过水平和竖直方向的距离差来判断,比如竖直方向滑动的距离大就判断为竖直滑动,否则判断为水平滑动。根据这个规则就可以进行下一步的解决方法的制定了。

滑动过程示意:

这里写图片描述

对于场景2来说,比较特殊,他无法根据滑动的角度、距离差以及速度差来做判断,但是这个时候一般都能在业务上找到突破点,比如业务上有规定:当处于某种状态时需要外部 View 响应用户的滑动,而处于另外一种状态时则需要内部 View 来响应 View 的滑动,根据这种业务上的需求我们也能得出相应的处理规则,有了处理规则同样可以进行下一步处理。这种场景通过文字描述可能比较抽象,在后面我们会通过实际的例子来演示这种情况的解决方案,那时就容易理解了,这里先有个概念即可。

对于场景3来说,它的滑动规则就更复杂了,和场景2一样,它也无法直接根据滑动的角度、距离差以及速度差来做判断,同样还是只能从业务上找到突破点,具体方法和场景2一样,都是从业务的需求上得出相应的处理规则,和场景2一样在后面我们会通过实际的例子来演示这种情况的解决方案。

滑动冲突的解决方式

在上面我们已经描述了三种典型的滑动冲突场景,这里我们将会意义分析各种场景并给出具体的解决方法。首先我们要分析第一种滑动冲突场景,这也是最简单、最典型的一种滑动冲突,因为它的滑动冲突规则比较简单,不管多复杂的滑动冲突,它们之间的区别仅仅是滑动规则不同而已。抛开滑动规则不说,我们需要找到一种不依赖具体的滑动规则的通用的解决方法,在这里,我们就根据场景 1 的情况来得出通用的解决方案,然后场景 2 和场景 3 我们只需要修改有关滑动规则的逻辑即可。

上面说过,针对场景 1 中的滑动,我们可以根据滑动的距离差来进行判断,这个距离差就是所谓的滑动规则。如果用 ViewPager 去实现场景 1 中的效果,我们不需要手动处理滑动冲突,因为 ViewPager 已经帮我们做了,但是这里为了更好地演示滑动冲突的解决思想,没有采用 ViewPager。其实在滑动过程中得到滑动的角度这个是相当简单的,但是到底要怎么做才能将点击事件交给合适的 View 去处理呢?这个时候就要用到上篇博客中提到的事件分发机制了。针对滑动冲突,我们这里给出两种解决滑动冲突的方式:外部拦截法和内部拦截法。

  • 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;
        }
        return intercepted;
    }

上述代码是外部拦截法的典型逻辑,针对不同的滑动冲突,只需要修改父容器需要当前点击事件这个条件即可,其他均不需要做修改并且也不能修改。这里对上述代码在描述一下,在 onInterceptTouchEvent 方法中,首先是 ACTION_DOWN 这个事件,父容器必须返回 false,即不拦截 ACTION_DOWN 事件,这是因为一旦父容器拦截了 ACTION_DOWN,那么后续的 ACTION_MOVE 和 ACTION_UP 事件都会直接交由父容器处理,这个时候事件就没法再传递给子元素了;其次是 ACTION_MOVE 事件,这个事件可以根据需要来决定是否拦截,如果父容器需要拦截就返回 true,否则就返回 false ;最后是 ACTION_UP 事件,这里必须要返回 false,因为 ACTION_UP 事件本身没有太多意义。

考虑一种情况,假设事件交由子元素处理,如果父容器在 ACTION_UP 时返回了 true,就会导致子元素无法接收到 ACTION_UP事件,这个时候子元素中的 onClick 事情就无法触发,但是父元素比较特殊,一旦它开始拦截任何一个事件,那么后续的事件都会交给它来处理,而 ACTION_UP 作为最后一个事件也必定可以传递给父容器,即便父容器的 onInterceptTouchEvent 方法在 ACTION_UP 时返回了 false。

  • 2、内部拦截法

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

@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 (父容器需要此类点击事件) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(ev);
    }

上述代码是内部拦截法的典型代码,当面对不同的滑动策略是只需要修改里面的条件即可,其他不需要做改动而且也不能有改动。除了子元素需要做处理以外,父元素也要默认拦截除了 ACTION_DOWN 以外的其他事件,这样当子元素调用 parent.requestDisallowInterceptTouchEvent(false) 方法时,父元素才能继续拦截所需的事件。

为什么父元素不能拦截 ACTION_DOWN 事件呢? 那是因为 ACTION_DOWN 事件并不受 FLAG_DISALLOW_INTERCEPT 这个标记位的控制,所以一旦父容器拦截了 ACTION_DOWN 事件,那么所有的事件都无法传递到子元素中去,这样内部拦截法就无法起作用了。父元素所做的修改如下所示:

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

下面通过一个实例来分别介绍这两种方法。我们来实现一个类似于 ViewPager 中嵌套 ListView 的效果,为了制造滑动冲突,我们写一个类似于 ViewPager 的控件即可,名字就叫 HorizontalScrollViewEx,这个控件的具体实现思想我们会在下一篇博客中进行详细介绍,这里只介绍滑动冲突部分,关于这个 Demo 的源码可以点击这里进行下载:
HorizontalScrollViewEx.zip

为了实现 ViewPager 的效果,我们定义了一个类似于水平的 LinearLayout 的东西,只不过它可以水平滑动,初始化时我们在它的内部添加若干个 ListView,这样以来,由于它内部的 ListView 可以竖直滑动,而它本身又可以水平滑动,因此一个典型的滑动冲突场景就出现了,并且这种冲突属于场景 1 的冲突。根据滑动策略,我们可以选择水平和竖直的滑动距离差来解决滑动冲突。

首先来看一下 Activity 中的初始话代码,如下所示:

public class DemoActivity_2 extends Activity {
    private static final String TAG = "DemoActivity_2";

    private HorizontalScrollViewEx mListContainer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.demo_2);
        Log.d(TAG, "onCreate");
        initView();
    }

    private void initView() {
        LayoutInflater inflater = getLayoutInflater();
        mListContainer = (HorizontalScrollViewEx) findViewById(R.id.container);
        final int screenWidth = MyUtils.getScreenMetrics(this).widthPixels;
        final int screenHeight = MyUtils.getScreenMetrics(this).heightPixels;
        for (int i = 0; i < 3; i++) {
            ViewGroup layout = (ViewGroup) inflater.inflate(
                    R.layout.content_layout, mListContainer, false);
            layout.getLayoutParams().width = screenWidth;
            TextView textView = (TextView) layout.findViewById(R.id.title);
            textView.setText("page " + (i + 1));
            layout.setBackgroundColor(Color.rgb(255 / (i + 1), 255 / (i + 1), 0));
            createList(layout);
            mListContainer.addView(layout);
        }
    }

    private void createList(ViewGroup layout) {
        ListView listView = (ListView) layout.findViewById(R.id.list);
        ArrayList<String> datas = new ArrayList<String>();
        for (int i = 0; i < 50; i++) {
            datas.add("name " + i);
        }

        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
                R.layout.content_list_item, R.id.name, datas);
        listView.setAdapter(adapter);
        listView.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view,
                    int position, long id) {
                Toast.makeText(DemoActivity_2.this, "click item",
                        Toast.LENGTH_SHORT).show();

            }
        });
    }
}

上述初始化代码很简单,就是创建了 3 个ListView并且把 ListView 加入到我们自定义的 HorizontalScrollViewEx 控件中,这里 HorizontalScrollViewEx 是父容器,而 ListView 则是子元素,这里就不再过多地介绍了。

首先采用外部拦截法来解决这个问题,按照前面的分析,我们只需要修改父容器需要拦截的条件即可。对于本例来说,父容器的拦截条件就是滑动过程中水平滑动距离差比竖直滑动距离差大,在这种情况下,父容器就拦截当前点击事件,根据这一条件进行相应修改,修改后的 HorizontalScrollViewEx 的 onInterceptTouchEvent 方法如下所示:

@Override
    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;
            //返回 Scroller 是否已完成滚动
            if (!mScroller.isFinished()) {
                //停止动画。与forceFinished(boolean)相反,Scroller滚动到最终x与y位置时中止动画。
                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;
        }
        default:
            break;
        }

        Log.d(TAG, "intercepted=" + intercepted);
        mLastX = x;
        mLastY = y;
        mLastXIntercept = x;
        mLastYIntercept = y;

        return intercepted;
    }

从上面的代码来看,它和外部拦截法的伪代码的差别很小,只是把父容器的拦截事件换成了具体的逻辑。在滑动过程中,当水平方向的距离大时就判断为水平滑动,为了能够水平滑动所以让父容器拦截事件;而竖直距离大时父容器就不拦截事件,于是事件就传递给了 ListView,所以 ListView 也能上下滑动,如此滑动冲突就解决了。至于 mScroller.abortAnimation() 这一句话是为了优化滑动体验而加入的。

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

下面是 HorizontalScrollViewEx 的完整代码,关于 onMeasure 和 onLayout 方法可以先不去关注,在下篇我们对 View的工作原理进行介绍时会详细说到。

public class HorizontalScrollViewEx extends ViewGroup {
    private static final String TAG = "HorizontalScrollViewEx";

    private int mChildrenSize;
    private int mChildWidth;
    private int mChildIndex;

    // 分别记录上次滑动的坐标
    private int mLastX = 0;
    private int mLastY = 0;
    // 分别记录上次滑动的坐标(onInterceptTouchEvent)
    private int mLastXIntercept = 0;
    private int mLastYIntercept = 0;

    private Scroller mScroller;
    private VelocityTracker mVelocityTracker;

    public HorizontalScrollViewEx(Context context) {
        super(context);
        init();
    }

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

    public HorizontalScrollViewEx(Context context, AttributeSet attrs,
            int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        if (mScroller == null) {
            mScroller = new Scroller(getContext());
            mVelocityTracker = VelocityTracker.obtain();
        }
    }

    @Override
    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;
            //返回 Scroller 是否已完成滚动
            if (!mScroller.isFinished()) {
                //停止动画。与forceFinished(boolean)相反,Scroller滚动到最终x与y位置时中止动画。
                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;
        }
        default:
            break;
        }

        Log.d(TAG, "intercepted=" + intercepted);
        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();
            mVelocityTracker.computeCurrentVelocity(1000);
            float xVelocity = mVelocityTracker.getXVelocity();
            if (Math.abs(xVelocity) >= 50) {
                mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
            } else {
                mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
            }
            mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
            int dx = mChildIndex * mChildWidth - scrollX;
            smoothScrollBy(dx, 0);
            mVelocityTracker.clear();
            break;
        }
        default:
            break;
        }

        mLastX = x;
        mLastY = y;
        return true;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measuredWidth = 0;
        int measuredHeight = 0;
        final int childCount = getChildCount();
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        if (childCount == 0) {
            setMeasuredDimension(0, 0);
        } else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(measuredWidth, measuredHeight);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            setMeasuredDimension(measuredWidth, heightSpaceSize);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childLeft = 0;
        final int childCount = getChildCount();
        mChildrenSize = childCount;

        for (int i = 0; i < childCount; i++) {
            final View childView = getChildAt(i);
            if (childView.getVisibility() != View.GONE) {
                final int childWidth = childView.getMeasuredWidth();
                mChildWidth = childWidth;
                childView.layout(childLeft, 0, childLeft + childWidth,
                        childView.getMeasuredHeight());
                childLeft += childWidth;
            }
        }
    }

    private void smoothScrollBy(int dx, int dy) {
        mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        mVelocityTracker.recycle();
        super.onDetachedFromWindow();
    }
}

如果采用 内部拦截法 也是可以的,按照前面对内部拦截法的分析,我们只需要修改 ListView 的 dispatchTouchEvent 方法中的父容器的拦截逻辑,同时让父容器拦截 ACTION_DOWN 以外的其他事件即可。为了重写 ListView 的 dispatchTouchEvent 方法,我们必须自定义一个 ListView,称为 ListViewEx,然后对内部拦截法的模板代码进行修改,根据需要,ListViewEx 的实现如下所示:

public class ListViewEx extends ListView {

    //用于分别记录上次滑动的坐标
    private int mLastX = 0;
    private int mLastY = 0;

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

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

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

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

除了上面对 ListView 所做的修改,我们还需要修改 HorizontalScrollViewEx 的 onInterceptTouchEvent 方法,修改后的类暂且叫 HorizontalScrollViewEx2,其 onInterceptTouchEvent 方法如下所示:

@Override
    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: {
                mLastX = x;
                mLastY = y;
                //返回 Scroller 是否已完成滚动
                if (!mScroller.isFinished()) {
                    //停止动画。与forceFinished(boolean)相反,Scroller滚动到最终x与y位置时中止动画。
                    mScroller.abortAnimation();
                    return true;
                }
                return false;
            }
            case MotionEvent.ACTION_MOVE:
            case MotionEvent.ACTION_UP:
                return true;
        }
        return true;
    }

上面的代码就是 内部拦截法 的实例,其中 mScroller.abortAnimation()这一句不是必须的,在当前这种情形下主要是为了优化滑动体验。从实现上来看,内部拦截法 的操作要稍微复杂一些,因此推荐采用外部拦截法来解决常见的滑动冲突。

前面说过,只要我们根据场景 1 的情况来得出通用的解决方案,那么对于 场景 2 和 场景 3 来说我们只需要修改相关滑动规则的逻辑即可,下面我们就来演示如何利用 场景 1 得出通用的解决方案来解决更复杂的滑动冲突。这里只详细分析 场景 2 中的滑动冲突,对于 场景 3 中的叠加型滑动冲突,由于它可以拆解为单一的滑动冲突,所以其滑动冲突的解决思想和 场景 1 、场景 2 中的单一滑动冲突的解决思想是一致的,只需要分别解决每层之间的滑动冲突即可,在加上本篇的篇幅有限,这里就不对 场景 3 进行详细的介绍了。

对于 场景 2 来说,它的解决方法和 场景 1 一样,只是滑动规则不同而已,在前面我们已经得出了通用的解决方案,因此这里我们只需要替换父容器的拦截规则即可。注意,这里不在演示如何通过内部拦截法来解决 场景 2 中的滑动冲突,因为 内部拦截法 没有 外部拦截法 简单易用,所以推荐采用 外部拦截法 来解决常见的滑动冲突。

下面通过一个实际的例子来分析 场景 2 ,首先我们提供一个可以上下滑动的父容器,这里就叫 StickyLayout,它看起来就像是可以上下滑动的竖直的 LinearLayout ,然后在它的内部分别放一个 Header 和 一个 ListView,这样内外两层都能够上下滑动,于是就形成了 场景 2 中的滑动冲突了。当然这个 StickyLayout 是有滑动规则的:当 Header 显示时或者 ListView 滑动到顶部时,由 StickyLayout 拦截事件;当 Header 隐藏时,这要分情况,如果 ListView 已经滑动到顶部并且当前手势是向下滑动的话,这个时候还是 StickyLayout 拦截事件,其他情况则由 ListView 拦截事件。这种滑动规则看起来有点复杂,为了解决它们之间的滑动冲突,我们还是需要重写父容器 StickyLayout 的 onInterceptTouchEvent 方法,至于 ListView 则不用做任何修改,我们来看一下 StickyLayout 的具体实现,滑动冲突相关的主要代码如下所示:

public class StickyLayout extends LinearLayout {
    private Scroller mScroller;
    private int mLastY;
    private int mLastYIntercept;

    public StickyLayout(Context context) {
        this(context, null);
    }

    public StickyLayout(Context context, AttributeSet attrs) {
        this(context, attrs, -1);
    }

    public StickyLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mScroller = new Scroller(context);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercepted = false;
                break;
            case MotionEvent.ACTION_MOVE:
                int scrollY = getScrollY();
                //表示开始滑动时,根据状态来判断是否要拦截事件
                if (scrollY > 0) {
                    //表示头部已经消失,且是向下滑动时由父元素拦截事件,反之由子元素拦截
                    if (scrollY == getChildAt(0).getMeasuredHeight()) {
                        int deltaY = y - mLastYIntercept;
                        if (deltaY > 0) {
                            intercepted = true;
                        } else {
                            intercepted = false;
                        }
                    } else {
                        intercepted = true;
                    }
                } else {
                    //表示初始状态时,由父元素拦截事件
                    intercepted = true;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
        }
        mLastY = y;
        mLastYIntercept = y;
        return intercepted;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaY = y - mLastY;
                scrollBy(0, -deltaY);
                break;
            case MotionEvent.ACTION_UP:
                int scrollY = getScrollY();
                int dy = 0;
                //这个判断写的并不是很严谨,当头部隐藏 下拉显示头部到1/2时显示完整头部,当头部显示时,上拉到大于头部的1/2时隐藏头部
                if (scrollY > 0) {
                    Log.e("======", scrollY + "");
                    if (scrollY <= getChildAt(0).getMeasuredHeight() / 2) {
                        Log.e("======", scrollY + "-----");
                        dy = -scrollY;
                    } else {
                        dy = getChildAt(0).getMeasuredHeight() - scrollY;
                        Log.e("======", scrollY + "=========");
                    }
                } else {
                    dy = -scrollY;
                }
                smoothScrollBy(0, dy);
                break;
        }
        mLastY = y;
        return true;
    }

    private void smoothScrollBy(int dx, int dy) {
        mScroller.startScroll(0, getScrollY(), 0, dy, 500);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }
}

源代码下载地址:StickLayout.zip 源码

上面的代码我们写的并不是非常的严谨,还有好多地方需要优化,这里仅仅是来介绍下 场景 2 的冲突解决。我们来分析下上面这段代码的逻辑,当 Header 是显示的时候,滑动是 父容器 StickyLayout 拦截事件,只有当 Header 隐藏的时候,并且是向上滑动的时候,ListView才能接收到事件。当 Header 隐藏的时候,并且是处于 ListView 的顶端时, StickyLayout 再次获取拦截事件

好了,到这里关于滑动冲突的所有东西都介绍完了,如果想要深入的同学,可以多找些绚丽的自定义 View 瞅瞅,来加深下。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值