常见的滑动冲突场景可以简单分为以下三种:
场景1:外部滑动方向和内部滑动方向不一致
场景2:外部滑动方向和内部滑动方向一致
场景3:上面两种情况的嵌套
如图:
场景1,主要是将ViewPager和Fragment配合使用所组成的页面滑动效果,主流应用几乎都会使用这个效果。在这个效果中可以通过左右滑动来切换页面,而每个页面内部往往又是一个ListView,所以就造成了滑动冲突,但是在ViewPager内部处理了这种滑动冲突,因此在采用ViewPager时我们就无须关注这个问题,而如果把ViewPager换成ScrollView,那就必须自己手动处理,不然造成的结果就是内外两层只能一层能够滑动。
场景2,就复杂一点,当内外两层都在同一个方向可以滑动的时候,显然存在逻辑问题。因为当手指开始滑动的时候,系统无法知道用户到底是想让哪一层滑动,所以当手指滑动的时候就会出现问题,要么只有一层滑动,要么就是内外两层都滑动但很卡顿。
场景3,是场景1和场景2两种情况的嵌套,显得更复杂了。比如外部有一个SlideMenu效果,内部有一个ViewPager,ViewPager的每一个页面中又是一个ListView。虽然场景3滑动冲突看起来很复杂,但都是几个单一的滑动冲突的叠加,因此需要一一拆解开来即可。
一般来说,不管滑动冲突有多么复杂,它都有既定的规则,根据这些规则我们就可以选择合适的方法去处理。
对于场景1,它的处理规则就是:当用户左右滑动时,需要让外部的View拦截点击事件,当用户上下滑动,需要让内部View拦截点击事件。具体来说就是根据滑动是水平滑动还是竖直滑动来判断到底是由谁来拦截事件。
如图:
简单来说,就是根据水平方向和竖直方向的距离差来判断,如果是Dx>Dy,那么则是水平滑动,如果是Dy>Dx,那么则是竖直滑动。
场景2,则是比较特殊,它无法根据滑动的角度,距离差以及速度差来做判断。这个时候就需要从业务上找到突破点,比如,当处于某种状态时需要外部View响应用户的滑动,而处于另外一种状态时需要内部View来响应View的滑动
对于场景3的话,它的滑动规则也更复杂,和场景2一样,同样是从业务上找到突破点。
外部拦截法是指点击事件都是先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件,就不拦截了,这样就可以解决滑动冲突的问题,外部拦截法需要重写父容器的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; break;
} case MotionEvent.ACTION_MOVE: { if (父容器需要点击当前事件) {
intercepted = true;
} else {
intercepted = false;
} break;
} case MotionEvent.ACTION_UP: {
intercepted = false; break;
} default: break;
}
mLastXIntercept = x;
mLastYIntercept = y; return intercepted;
}
首先ACTION_DOWN这个事件,父容器必须返回false,这样保证后续move和up的事件可以传递给子View,根据move事件来决定是否拦截,如果父容器拦截就返回true,否则返回false。
实现一个自定义类似ViewPager的控件,嵌套ListView的效果,源代码如下:
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() {
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; 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;
} 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; 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 (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);
} else { final View childView = getChildAt(0);
measuredWidth = childView.getMeasuredWidth() * childCount;
measuredHeight = childView.getMeasuredHeight(); setMeasuredDimension(measuredWidth, measuredHeight);
}
} @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();
}
}
这个情况的拦截条件就是父容器在滑动过程中水平距离差比垂直距离差大,那么就进行拦截,否则就不拦截,继续传递事件。
内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器进行处理,这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起来较外部拦截法复杂。伪代码如下:
@Override
public boolean dispatchTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: {
mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(true); break;
} case MotionEvent.ACTION_MOVE: { int deltaX = x - mLastX; int deltaY = y - mLastY; if (父容器需要此类点击事件) {
mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(false);
} break;
} case MotionEvent.ACTION_UP: { break;
} default: break;
}
mLastX = x;
mLastY = y; return super.dispatchTouchEvent(event);
}
当子元素调用requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需的事件。
前面是用自定义类似的ViewPager,现在重写一个ListView,我们可以自定义一个ListView,叫做ListViewEx,然后对内部拦截法的模板代码进行修改即可。
public class ListViewEx extends ListView { private static final String TAG = "ListViewEx"; private HorizontalScrollViewEx2 mHorizontalScrollViewEx2; // 分别记录上次滑动的坐标
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 defStyle) { super(context, attrs, defStyle);
} public void setHorizontalScrollViewEx2(
HorizontalScrollViewEx2 horizontalScrollViewEx2) {
mHorizontalScrollViewEx2 = horizontalScrollViewEx2;
} @Override
public boolean dispatchTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: {
mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(true); break;
} case MotionEvent.ACTION_MOVE: { int deltaX = x - mLastX; int deltaY = y - mLastY;
Log.d(TAG, "dx:" + deltaX + " dy:" + deltaY); if (Math.abs(deltaX) > Math.abs(deltaY)) {
mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(false);
} break;
} case MotionEvent.ACTION_UP: { break;
} default: break;
}
mLastX = x;
mLastY = y; return super.dispatchTouchEvent(event);
}
}
同时对于包含ListViewEx外部布局进行修改,在onInterceptTouchEvent事件上不进行拦截
public class HorizontalScrollViewEx2 extends ViewGroup { private static final String TAG = "HorizontalScrollViewEx2"; 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 HorizontalScrollViewEx2(Context context) { super(context); init();
} public HorizontalScrollViewEx2(Context context, AttributeSet attrs) { super(context, attrs); init();
} public HorizontalScrollViewEx2(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init();
} private void init() {
mScroller = new Scroller(getContext());
mVelocityTracker = VelocityTracker.obtain();
} @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;
}
} @Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "onTouchEvent action:" + event.getAction());
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;
Log.d(TAG, "move, deltaX:" + deltaX + " deltaY:" + deltaY); scrollBy(-deltaX, 0); break;
} case MotionEvent.ACTION_UP: { int scrollX = getScrollX(); int scrollToChildIndex = scrollX / mChildWidth;
Log.d(TAG, "current index:" + scrollToChildIndex);
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();
Log.d(TAG, "index:" + scrollToChildIndex + " dx:" + dx); 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 (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);
} else { final View childView = getChildAt(0);
measuredWidth = childView.getMeasuredWidth() * childCount;
measuredHeight = childView.getMeasuredHeight(); setMeasuredDimension(measuredWidth, measuredHeight);
}
} @Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
Log.d(TAG, "width:" + getWidth()); 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();
}
}
这个拦截规则也是父容器在滑动过程中水平距离差与垂直距离差相比。
亿速云「云服务器」,即开即用、新一代英特尔至强铂金CPU、三副本存储NVMe SSD云盘,价格低至29元/月。点击查看>>
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。