处理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() 中:
- 当发生MotionEvent.ACTION_DOWN ,用户手指按下屏幕,此时不进行拦截,因为一旦拦截MotionEvent.ACTION_DOWN ,当次事件序列之后的所有触摸事件都会被拦截,相当于VerticalScrollView拦截了所有触摸事件;
- 当发生MotionEvent.ACTION_MOVE ,用户手指在屏幕上移动,获取当前移动的垂直距离和水平距离,只有当垂直滑动距离大于水平滑动距离时,认为当前用户在进行垂直滑动,才对触摸事件进行拦截;
- 当发生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() ,略显复杂。