叙述
滑动冲突可以说是日常开发中比较常见的一类问题,也是比较让人头疼的一类问题,尤其是在使用第三方框架的时候,两个原本完美的控件,组合在一起之后,忽然发现整个世界都不好了。
关于滑动冲突
滑动冲突分类
滑动冲突,总的来说就是两类。
-
同方向滑动冲突
比如ScrollView嵌套ListView,或者是ScrollView嵌套自己 -
不同方向滑动冲突
比如ScrollView嵌套ViewPager,或者是ViewPager嵌套ScrollView,这种情况其实很典型。现在大部分应用最外层都是ViewPager+Fragment 的底部切换(比如微信)结构,这种时候,就很容易出现滑动冲突。不过ViewPager里面无论是嵌套ListView还是ScrollView,滑动冲突是没有的,毕竟是官方的东西,可能已经考虑到了这些,所以比较完善。
复杂一点的滑动冲突,基本上就是这两个冲突结合的结果。
滑动冲突解决思路
滑动冲突,就其本质来说,两个不同方向(或者是同方向)的View,其中有一个是占主导地位的,每次总是抢着去处理外界的滑动行为,这样就导致一种很别扭的用户体验,明明只是横向的滑动了一下,纵向的列表却在垂直方向发生了动作。就是说,这个占主导地位的View,每一次都身不由己的拦截了这个滑动的动作,因此,要解决滑动冲突,就是得明确告诉这个占主导地位的View,什么时候你该拦截,什么时候你不应该拦截,应该由下一层的View去处理这个滑动动作。
这里不明白的同学,可以去了解一下Android Touch事件的分发机制,这也是解决滑动冲突的核心知识。
第二种滑动冲突,解决起来是比较简单的。这里就结合例子说一下。
滑动冲突
这里,说一下背景情况。之前做下拉刷新、上拉加载更多时一直使用的是PullToRefreshView这个控件,因为很方便,不用导入三方工程。在其内部可以放置ListView,GridView及ScrollView,非常方便,用起来可谓是屡试不爽。但是直到有一天,因项目需要,在ListView顶部加了一个轮播图控件BannerView。结果发现轮播图滑动的时候,和纵向的下拉刷新组件冲突了。
如之前所说,解决滑动冲突的关键,就是明确告知接收到Touch的View,是否需要拦截此次事件。
解决方法
解决方案1,从外部拦截机制考虑
这里,相当于是PullToRefreshView嵌套了ViewPager,那么每次优先接收到Touch事件的必然是PullToRefreshView。因为正常情况下,父控件会优先接收到touch事件。这样就清楚了,看代码:
在PullToRefreshView的onInterceptTouchEvent方法中:
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
int y = (int) e.getRawY(); int x = (int) e.getRawX(); boolean resume = false; switch (e.getAction()) { case MotionEvent.ACTION_DOWN: // 发生down事件时,记录y坐标 mLastMotionY = y; mLastMotionX = x; resume = false; break; case MotionEvent.ACTION_MOVE: // deltaY > 0 是向下运动,< 0是向上运动 int deltaY = y - mLastMotionY; int deleaX = x - mLastMotionX; if (Math.abs(deleaX) > Math.abs(deltaY)) { resume = false; } else { //当前正处于滑动 if (isRefreshViewScroll(deltaY)) { resume = true; } } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: break; } return resume; }
这里最关键的代码就是这行
if (Math.abs(deleaX) > Math.abs(deltaY)) {
resume = false; }
横向滑动距离大于纵向时,无须拦截这次滑动事件,滑动事件会传递到下一层的view,也就是这里的轮播图控件,这样横向滑动轮播图的时候,PullToRefreshView就不会有下拉的动作了。其实,就是这么简单,但前提是你必须明确了解Android Touch事件的传递机制,期间各个方法执行的顺序及意义。
ps: 关于上文中提到的isRefreshViewScroll 方法代码(这个方法其实是PullToRefreshView这个控件自带的一个方法)
/** * 是否应该到了父View,即PullToRefreshView滑动
*
* @param deltaY , deltaY > 0 是向下运动,< 0是向上运动
* @return
*/
private boolean isRefreshViewScroll(int deltaY) {
if (mHeaderState == REFRESHING || mFooterState == REFRESHING) { return false; } // 对于ListView和GridView if (mAdapterView != null) { // 子view(ListView or GridView)滑动到最顶端 if (deltaY > 0) { View child = mAdapterView.getChildAt(0); if (child == null) { // 如果mAdapterView中没有数据,不拦截 return false; } if (mAdapterView.getFirstVisiblePosition() == 0 && child.getTop() == 0) { mPullState = PULL_DOWN_STATE; return true; } int top = child.getTop(); int padding = mAdapterView.getPaddingTop(); if (mAdapterView.getFirstVisiblePosition() == 0 && Math.abs(top - padding) <= 8) {// 这里之前用3可以判断,但现在不行,还没找到原因 mPullState = PULL_DOWN_STATE; return true; } } else if (deltaY < 0) { View lastChild = mAdapterView.getChildAt(mAdapterView .getChildCount() - 1); if (lastChild == null) { // 如果mAdapterView中没有数据,不拦截 return false; } // 最后一个子view的Bottom小于父View的高度说明mAdapterView的数据没有填满父view, // 等于父View的高度说明mAdapterView已经滑动到最后 if (lastChild.getBottom() <= getHeight() && mAdapterView.getLastVisiblePosition() == mAdapterView .getCount() - 1) { mPullState = PULL_UP_STATE; return true; } } } // 对于ScrollView if (mScrollView != null) { // 子scroll view滑动到最顶端 View child = mScrollView.getChildAt(0); if (deltaY > 0 && mScrollView.getScrollY() == 0) { mPullState = PULL_DOWN_STATE; return true; } else if (deltaY < 0 && child.getMeasuredHeight() <= getHeight() + mScrollView.getScrollY()) { mPullState = PULL_UP_STATE; return true; } } return false;
解决方案2,从内容逆向思维分析
有时候,我们不想去修改或者是无法修改最先接收到Touch事件的View 时,比如这里我不想去修改PullToRefreshView的代码。就必须考虑从当前从Touch传递事件中最后的那个View逆向考虑。首先,由Android中View的Touch事件传递机制,我们知道Touch事件,首先必然由最外层View接收到,并很有可能被它拦截,如果无法更改这个最外层View,那么是不是就没辙了呢?其实不然,Android这么高大上的系统必然考虑到了这个问题,好了废话不说,先看代码
private BannerView carouselView;
private Context mContext;
private PullToRefreshView refreshView;
refreshView.setOnTouchListener(new View.OnTouchListener() {
@Override public boolean onTouch(View v, MotionEvent event) { carouselView.getParent().requestDisallowInterceptTouchEvent(false); return false; } }); carouselView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { carouselView.getParent().requestDisallowInterceptTouchEvent(true); int x = (int) event.getRawX(); int y = (int) event.getRawY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: lastX = x; lastY = y; break; case MotionEvent.ACTION_MOVE: int deltaY = y - lastY; int deltaX = x - lastX; if (Math.abs(deltaX) < Math.abs(deltaY)) { carouselView.getParent().requestDisallowInterceptTouchEvent(false); } else { carouselView.getParent().requestDisallowInterceptTouchEvent(true); } default: break; } return false; } });
首先说一下这个方法
public abstract void requestDisallowInterceptTouchEvent (boolean disallowIntercept)
Called when a child does not want this parent and its ancestors to intercept touch events with onInterceptTouchEvent(MotionEvent).
This parent should pass this call onto its parents. This parent must obey this request for the duration of the touch (that is, only clear the flag after this parent has received an up or a cancel.Parameters
disallowIntercept
True if the child does not want the parent to intercept touch events.
API里的意思很明确,子View如果不希望其父View拦截Touch事件时,可调用此方法。当disallowIntercept这个参数为true时,父View将不拦截。
好了,言归正传。这里拦截直接也很明确,在carouselView的onTouch方法中每次进入就设定父View不拦截此次事件,然后在MOTION_MOVE时候,根据滑动的距离判断再决定是父View是否有权利拦截Touch事件(即滑动行为)。
关键的处理逻辑就是这里:
if (Math.abs(deltaX) < Math.abs(deltaY)) {
carouselView.getParent().requestDisallowInterceptTouchEvent(false);
} else { carouselView.getParent().requestDisallowInterceptTouchEvent(true); }
这个结合上面对这个方法的解释,应该很好理解了,就不多做阐述了。
可以看到,解决这种滑动冲突的方法很简单,最根本的还是得充分了解Touch事件的传递机制,只有这样,才能明白该在哪里做什么事情。当然,横竖滑动的冲突很好理解,但同一方向的滑动冲突情况就有点复杂了,下次再说。