View的滑动冲突指的是当有内外两层View同时可以滑动的时候,这个时候就会产生滑动冲突。那么应该如何解决滑动呢,其实要用到View的事件分发机制。
View的滑动冲突主要有以下三个场景:
场景一:外部滑动方向和内部滑动方向不一致;
场景二:外部滑动方向和内部滑动方向一致;
场景三:以上两种情况的嵌套。
这里主要讨论场景一的滑动冲突的解决,其他两种思想都是类似的,根据具体情况而定。
对于场景一,它的滑动冲突处理规则是:当用户左右滑动时,需要外部的View拦截点击事件,当用户上下滑动时,需要内部的View拦截点击事件。解决滑动冲突的思想就是根据滑动郭晨各种两个点之间的坐标就可以得出到底是水平滑动还是竖直滑动。那么如何判断是水平滑动还是竖直滑动呢?这个有很多参考,比如我们可以根据水平方向和竖直方向的距离差来判断,某些情况还可以根据速度差来判断。这里根据距离差来处理滑动冲突。
当水平方向的滑动距离大于竖直方向的滑动距离时,判断为水平滑动,否则判断为竖直滑动。如下图所示
也就是当dx > dy时判断为水平滑动,否则为竖直滑动。针对滑动冲突,这里给出两种解决滑动冲突的方式:外部拦截法,内部拦截法。
1、外部拦截法
所谓外部拦截法是指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题。这种方法也比较符合点击事件的分发机制。外部拦截发需要重写父容器的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; if (!mScroller.isFinished()) { mScroller.abortAnimation(); intercepted = true; } break; } case MotionEvent.ACTION_MOVE: { int deltaX = x - mLastXIntercept; int deltaY = y - mLastYIntercept; if (父容器需要当前点击事件) { 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; }
上述代码是外部拦截法的典型逻辑,针对不同的滑动冲突,只需要修改父容器的当前点击事件这个条件即可,其他的均不需要修改。
另外针对上述代码再说明一下,
首先down事件,父容器必须返回false,即不拦截down事件,如果拦截了down事件,那么后续的move和up事件都交给父容器处理了,不存在什么滑动了,也就是不存在什么滑动冲突的解决了,更不能将事件传递给子元素。
然后是move事件,这是我们要处理的核心事件,这个时候需要根据具体需要看判断是否要拦截事件。如果父容器需要就拦截,不需要就不拦截。
最后是up事件,必须要返回false,因为本身没有什么意义。考虑一种情况,假设父容器up返回了true,那么元素的onClick事件无法触发。
为了实现场景一的效果,定义了一个类似于水平的LinearLayout的东西,只不过它可以水平滑动,初始化时我们在它的内部添加若干个ListView,这样一来,由于它内部的ListView可以竖直滑动,而它本身又可以水平滑动,因此一个典型的场景一滑动冲突就出现了,这里根据水平和竖直距离差来判断。
首先贴上Activity代码:
package com.ryg.chapter_3; import java.util.ArrayList; import com.ryg.chapter_3.R; import com.ryg.chapter_3.ui.HorizontalScrollViewEx; import com.ryg.chapter_3.utils.MyUtils; import android.app.Activity; import android.graphics.Color; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import android.widget.AdapterView.OnItemClickListener; public class DemoActivity_1 extends Activity { private static final String TAG = "DemoActivity_1"; private HorizontalScrollViewEx mListContainer; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.demo_1); 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_1.this, "click item", Toast.LENGTH_SHORT).show(); } }); } }
上述代码只是创建了3个ListView并且把ListView加入到我们自定义的HorizontalScrollViewEx中,HorizontalScrollViewEx就是父容器,上面提到的类似水平LinearLayout的东西。而ListView是子元素。
采用外部拦截法,它的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; 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; int deltaY = y - mLastY; scrollBy(-deltaX, 0); break; } case MotionEvent.ACTION_UP: { int scrollX = getScrollX(); int scrollToChildIndex = scrollX / mChildWidth; 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; }
其实就是修改外部拦截伪代码的拦截条件,在滑动过程中,当水平方向的距离大于就判断为水平滑动,为了能够水平滑动就让父容器拦截事件,而竖直距离大时就不拦截事件,于是事件就传递给了ListView,所以ListView也能上下滑动,这样滑动冲突就解决了。
HorizontalScrollViewEx的完整代码如下;代码参考了Android开发艺术探索一书。
package com.ryg.chapter_3.ui; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewGroup; import android.widget.Scroller; 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; int deltaY = y - mLastY; scrollBy(-deltaX, 0); break; } case MotionEvent.ACTION_UP: { int scrollX = getScrollX(); int scrollToChildIndex = scrollX / mChildWidth; 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(); } }