处理View滑动冲突的一种方法

  当界面中内外两层View都可以滑动时,就会产生滑动冲突。比如一个列表型的ViewGroup里面盛放了若干个View,ViewGroup可以垂直滚动,而里面的View又支持水平滑动的手势,当用户在ViewGroup和View的重叠区域滑动手指时,就会产生滑动冲突——无法判断当前滑动操作应当由ViewGroup还是View来进行处理。本文通过一个简单的例子,说明滑动冲突的一种处理方法。

1. 构造滑动冲突场景

  首先来构造滑动冲突的场景。这里使用的场景如前所述,在VerticalScrollView中放置BlockView,VerticalScrollView要支持垂直滚动,BlockView要支持水平滑动,由此产生滑动冲突。布局文件包含一个VerticalScrollView:

<com.nex3z.examples.simplecustomview.VerticalScrollView
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
</com.nex3z.examples.simplecustomview.VerticalScrollView>

【完整文件】

  VerticalScrollView中放置的item布局如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <com.nex3z.examples.simplecustomview.BlockView
        android:id="@+id/block"
        android:layout_width="300dp"
        android:layout_height="100dp"
        android:paddingBottom="10dp"
        android:layout_gravity="center_horizontal"/>

</LinearLayout>

【完整文件】

  然后填充VerticalScrollView:

public class MainActivity extends AppCompatActivity {

    private VerticalScrollView mContainer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
    }

    private void init() {
        mContainer = (VerticalScrollView) findViewById(R.id.container);
        LayoutInflater inflater = getLayoutInflater();
        Random rnd = new Random();
        for (int i = 0; i < 10; ++i) {
            ViewGroup layout = (ViewGroup) inflater.inflate(R.layout.item, mContainer, false);

            BlockView blockView = (BlockView) layout.findViewById(R.id.block);
            int color = Color.argb(255, rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256));
            blockView.setBlockColor(color);

            mContainer.addView(layout);
        }
    }
}

2. 通过自定义ViewGroup对触摸事件的拦截解决滑动冲突

  对触摸事件的拦截处理是解决滑动冲突的关键。简单来说,被ViewGroup拦截的触摸事件不会被传递到其子元素中。在当前场景,如果VerticalScrollView不加区分地拦截了所有滑动事件,那么VerticalScrollView的垂直滚动是可以正常进行的,但在其中的BlockView的水平滑动的手势就会失效。

  由于VerticalScrollView和BlockView所支持的滑动方向不同,那么就可以根据滑动方向,来判断应当把触摸事件交给谁处理。在VerticalScrollView的onInterceptTouchEvent() 中:

  1. 当发生MotionEvent.ACTION_DOWN ,用户手指按下屏幕,此时不进行拦截,因为一旦拦截MotionEvent.ACTION_DOWN ,当次事件序列之后的所有触摸事件都会被拦截,相当于VerticalScrollView拦截了所有触摸事件;
  2. 当发生MotionEvent.ACTION_MOVE ,用户手指在屏幕上移动,获取当前移动的垂直距离和水平距离,只有当垂直滑动距离大于水平滑动距离时,认为当前用户在进行垂直滑动,才对触摸事件进行拦截;
  3. 当发生MotionEvent.ACTION_UP ,用户手指离开屏幕,取消拦截。

  具体代码如下:

@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: {
            int dx = x - mLastXIntercept;
            int dy = y - mLastYIntercept;
            intercepted = Math.abs(dy) > Math.abs(dx);
            break;
        }
        case MotionEvent.ACTION_UP: {
            intercepted = false;
            break;
        }
        default: {
            break;
        }
    }

    Log.v(LOG_TAG, "onInterceptTouchEvent(): intercepted = " + intercepted);
    mLastXIntercept = x;
    mLastYIntercept = y;

    return intercepted;
}

【完整文件】

  完整代码可以在这里找到。

3. 解决滑动冲突的其他方法

  在《Android开发艺术探索》一书中,给出了两种解决滑动冲突的方法:一种是外部拦截法,在父容器中有选择地拦截事件,也就是上面使用的方法;还有一种内部拦截法,父容器不拦截任何事件,全部事件都交给子元素,如果子元素需要处理当前的触摸事件,就直接消耗掉,否则就交给父容器处理,需配合requestDisallowInterceptTouchEvent() ,略显复杂。