场景:在界面中内外两层同时可以滑动,则会产生滑动冲突
常见的滑动冲突场景
- 场景1:外不滑动方向和内部滑动方向不一致
- 场景2:外不滑动方向和内部滑动方向一致
- 场景3:上面两种情况结合
场景1:主要是将 ViewPager 和 Fragment 配合使用所组成的页面滑动效果,在这种效果中,可以通过左右滑动来切换页面,而每个页面内部往往又是一个ListView。本来这种情况下是由滑动冲突的,但是ViewPager 内部处理了这种滑动冲突,故采用ViewPager时我们无需关注这个问题,如果我们采用的不是ViewPager 而是 ScrollView 等,则必须手动处理滑动冲突了,否则就会造成内外两层只能有一层能够滑动,这是因为两者之间的滑动事件有冲突。
场景2:当内外两层都在同一个地方可以滑动的时候,显然存在逻辑问题。因为当手指开始滑动时,系统无法知道用户到底是想让那一层滑动,所以当手指滑动的时候就会出现问题,要么只有一层能滑动,要么就是内外两层都滑动的很卡顿。
场景3:场景3是场景1和场景2 两种情况的嵌套,因此场景3 的滑动冲突看起来更复杂。比如在很多应用中会有这么一个效果:内层有一个场景1的滑动效果,外层有一个场景2 的滑动效果。具体就是外部有一个SlideMenu效果,然后内部有一个ViewPager,ViewPager 的每一页中又是一个ListView,它是几个单一的滑动冲突的叠加,因此只需要分别处理内层和中层,中层和外层之间的滑动冲突即可
本质上上述三种滑动冲突场景的浮渣度其实是相同的,只是它们的滑动策略不同而已,至于解决滑动冲突的方法,它们几个是通用的
滑动冲突的处理规则:
场景1:它的处理规则是:当用户左右滑动的时候,需要让外部的View 拦截点击事件,当用户上下滑动的时候,需要让内部的View 拦截点击事件。此时我们可根据它们的滑动特征来解决滑动冲突,具体就是根据滑动是水平滑动还是竖直滑动来判断由谁来拦截事件
如下图所示,可以根据滑动过程中两个点之间的坐标得到是水平滑动还是竖直滑动,比如参考滑动路径和水平方向的夹角,或者是根据水平方向和竖直方向上的距离差,亦或者是根据水平和竖直方向的速度差来做判断
场景2:它无法根据滑动的角度、距离差以及速度差来做判断,但是此时一般都能在业务上遭到突破点,比如业务上有规定:当处于某种状态时需要外部View 响应用户的滑动,而处于另外一种状态时则需要内部View 来响应View 的滑动,更具这种业务上的需求我们也能得出相应的处理规则
场景3:同场景2一样无法通过滑动的角度、距离差以及速度差来做判断,只能从业务上找到突破点
滑动冲突的解决方式:
不管多复杂的滑动冲突,它们之间的区别仅仅是滑动规则不同而已
1、外部拦截法
即点击事件都要先经过父容器的拦截处理,如果父容器需要该事件就拦截,如果不需要则不拦截,比较符合点击事件的分发机制。外部拦截法需要重写父容器的 onInterceptTouchEvent 方法,在内部做相应的拦截即可,此方法的伪代码如下所示:
@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: { //必须为false 由事件分发机制可知,如果父容器拦截了DOWN 事件,则后续的MOVE/UP事件均不会下发给子元素,而是由自己处理 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, 由事件分发机制可知,如果父容器拦截了DOWN 事件,则后续的MOVE/UP事件均不会下发给子元素,而是由自己处理
ACTION_MOVE:
根据需要来决定是否拦截,如果父容器需要则返回 true,否则返回 false
ACTION_UP:
必须返回false,因为UP事件本身没多大意义,即使事件交由子元素处理,如果父容器在UP事件中返回了true,则子元素无法接收到UP事件,此时子元素中的onClick 事件不发触发。
由于父容器的特殊性,即一旦开始拦截任何一个事件,则后续事件都会交给它处理,而UP作为最后一个事件也必定可以传递给父容器,即便父容器的onInterceptTouchEvent 方法在ACTION_UP 时返回了 false。(针对的是父容器拦截其他的事件,如MOVE、DOWN 事件)
栗子:
HorizontalScrollViewEx :
package com.example.yhadmin.viewsliding.view; /* * @项目名: ViewSliding * @包名: com.example.yhadmin.viewsliding.view * @文件名: HorizontalScrollViewEx * @创建者: YHAdmin * @创建时间: 2018/5/3 15:22 * @描述: TODO */ 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 mChildWidth; private int mChildSize; private int mChildIndex; //分别记录上次互动的坐标(onInterceptTouchEvent) private int mLastXIntercept = 0; private int mLastYIntercept = 0; //分别记录上次互动的坐标 private int mLastX = 0; private int mLastY = 0; private VelocityTracker mVelocityTracker; private Scroller mScroller; public HorizontalScrollViewEx(Context context) { super(context); init(); } public HorizontalScrollViewEx(Context context, AttributeSet attrs) { super(context, attrs); init(); } public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { mScroller = new Scroller(getContext()); mVelocityTracker = VelocityTracker.obtain(); } @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; int childCount = getChildCount();//获取子元素 mChildSize = childCount; for (int i = 0; i < childCount; i++) {//遍历子元素 View childView = getChildAt(i); if (childView.getVisibility() != View.GONE) {//获取当前显示的子元素 int childWidth = childView.getMeasuredWidth();//获取子元素的宽 mChildWidth = childWidth; //重绘子元素 childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight()); childLeft += childWidth; } } } @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; 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 = (int) 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, mChildSize-1)); int dx = mChildIndex * mChildWidth - scrollX; smoothScrollBy(dx, 0); //释放占用的内存资源 mVelocityTracker.clear(); break; } default: break; } mLastX = x; mLastY = y; return true; } 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 资源 mVelocityTracker.recycle(); super.onDetachedFromWindow(); } }
DemoActivity_1
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(); } }); } }
2、内部拦截法
即父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件则直接消费掉,否则就交由父容器处理,需要配合 requestDisallowInterceptTouchEvent 方法才能正常工作,用起来比较复杂,需要重写子元素的 dispatchTouchEvent 方法,典型伪代码如下:
@Override public boolean dispatchTouchEvent(MotionEvent ev) { int x = (int) ev.getX(); int y = (int) ev.getY(); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: { //子元素需要此类点击事件,返回 true parent.requestDisallowInterceptTouchEvent(true); break; } case MotionEvent.ACTION_MOVE: { int delaX = x - mLastX; int delaY = x - mLastY; if (父容器需要此类点击事件) { parent.requestDisallowInterceptTouchEvent(false);//返回false } break; } case MotionEvent.ACTION_UP: { break; } default: break; } mLastX = x; mLastY = y; return super.dispatchTouchEvent(ev); }同时父容器的onInterceptTouchEvent 方法需要默认拦截除了 ACTION_DOWN 以外的其他事件,这样当子元素调用 parent.requestDisallowInterceptTouchEvent(false);时,父元素才能继续拦截所需的事件。
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { int action = ev.getAction(); if(action == MotionEvent.ACTION_DOWN){ return false; }else{ return true; } }栗子:
package com.example.yhadmin.viewsliding.view; /* * @项目名: ViewSliding * @包名: com.example.yhadmin.viewsliding.view * @文件名: HorizontalScrollViewEx * @创建者: YHAdmin * @创建时间: 2018/5/3 15:22 * @描述: TODO */ import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewGroup; import android.widget.Scroller; public class HorizontalScrollViewEx2 extends ViewGroup { private static final String TAG = "HorizontalScrollViewEx"; private int mChildWidth; private int mChildSize; private int mChildIndex; //分别记录上次互动的坐标(onInterceptTouchEvent) private int mLastXIntercept = 0; private int mLastYIntercept = 0; //分别记录上次互动的坐标 private int mLastX = 0; private int mLastY = 0; private VelocityTracker mVelocityTracker; private Scroller mScroller; public HorizontalScrollViewEx2(Context context) { super(context); init(); } public HorizontalScrollViewEx2(Context context, AttributeSet attrs) { super(context, attrs); init(); } public HorizontalScrollViewEx2(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { mScroller = new Scroller(getContext()); mVelocityTracker = VelocityTracker.obtain(); } @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; int childCount = getChildCount();//获取子元素 mChildSize = childCount; for (int i = 0; i < childCount; i++) {//遍历子元素 View childView = getChildAt(i); if (childView.getVisibility() != View.GONE) {//获取当前显示的子元素 int childWidth = childView.getMeasuredWidth();//获取子元素的宽 mChildWidth = childWidth; //重绘子元素 childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight()); childLeft += childWidth; } } } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { int x = (int) ev.getX(); int y = (int) ev.getY(); int action = ev.getAction(); if (action == MotionEvent.ACTION_DOWN) {//拦截除了DOWN 事件外的其他事件 mLastX = x; mLastY = y; if (!mScroller.isFinished()) { mScroller.abortAnimation(); return true; } return false; } else { return true; } } @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 = (int) 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, mChildSize - 1)); int dx = mChildIndex * mChildWidth - scrollX; smoothScrollBy(dx, 0); //释放占用的内存资源 mVelocityTracker.clear(); break; } default: break; } mLastX = x; mLastY = y; return true; } 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 资源 mVelocityTracker.recycle(); super.onDetachedFromWindow(); } }
DemoActivity_2:
public class DemoActivity_2 extends Activity { private static final String TAG = "DemoActivity_2"; private HorizontalScrollViewEx2 mListContainer; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.demo_2); Log.d(TAG, "onCreate"); initView(); } private void initView() { LayoutInflater inflater = getLayoutInflater(); mListContainer = (HorizontalScrollViewEx2) 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_layout2, 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) { ListViewEx listView = (ListViewEx) 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.setHorizontalScrollViewEx2(mListContainer); listView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Toast.makeText(DemoActivity_2.this, "click item", Toast.LENGTH_SHORT).show(); } }); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { Log.d(TAG, "dispatchTouchEvent action:" + ev.getAction()); return super.dispatchTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { Log.d(TAG, "onTouchEvent action:" + event.getAction()); return super.onTouchEvent(event); } }
ListViewEx:
package com.example.yhadmin.viewsliding.view; /* * @项目名: ViewSliding * @包名: com.example.yhadmin.viewsliding.view * @文件名: ListViewEx * @创建者: YHAdmin * @创建时间: 2018/5/9 10:08 * @描述: TODO */ import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.widget.ListView; public class ListViewEx extends ListView { private static final String TAG = "ListViewEx"; private HorizontalScrollViewEx2 mHorizontalScrollViewEx2; // 分别记录上次滑动的坐标 private int mLastX; private int mLastY; public ListViewEx(Context context) { super(context); } public ListViewEx(Context context, AttributeSet attrs) { super(context, attrs); } public ListViewEx(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public void setHorizontalScrollViewEx2( HorizontalScrollViewEx2 horizontalScrollViewEx2) { mHorizontalScrollViewEx2 = horizontalScrollViewEx2; } @Override public boolean dispatchTouchEvent(MotionEvent ev) { int x = (int) ev.getX(); int y = (int) ev.getY(); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: { mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(true);//告诉父容器不要拦截事件 break; } case MotionEvent.ACTION_MOVE: { int deltaX = x - mLastX; int deltaY = y - mLastY; 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(ev); } }总结:解决滑动冲突最主要的就是划分滑动规则,依据这些规则结合上述的2种解决方式来解决冲突问题