背景
滑动冲突,就是我们滑动view的时候,系统不知道该滑哪个。主要的场景像ViewPager里套ListView,ViewPager里套ViewPager,ListView里套ListView等。分类的话可以分为三类:同向水平滑动冲突、同向垂直滑动冲突和异向滑动冲突。
解决方案思路就是:利用事件分发解决
大致步骤:1、让子view的onTouchEvent返回true
2、在父view的onInterceptTouchEvent的action_move事件处理中,根据不同的业务逻辑来决定拦截与否,也就是返回true还是false
第一步的原因主要是确保Action_Move能传到父view的onInterceptTouchEvent中去,因为如果没有子view处理事件的话,除了Action_Down,其他事件都不会传到父view的onInterceptTouchEvent里,详情参见文章安卓事件分发学习之dispatchTouchEvent方法
实现
以异向滑动冲突(ViewPager里套ListView)为例,解决一下滑动冲突,顺便实现ViewPager的无限循环左右滑
1、自定义ViewPager
直接贴代码:
public class MyViewPager extends ViewPager { private int mLastDownX; // 按下位置的X坐标 private int mLastDownY; // 按下位置的Y坐标 private boolean mNext = false; // 是否翻到下一页(为false表示翻到上一页) private int mCurrPos = 0; // 当前页数 public MyViewPager(Context context) { super(context); } public MyViewPager(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mLastDownX = (int) ev.getX(); mLastDownY = (int) ev.getY(); return false; // down事件不拦截,保证子view能收到move事件 case MotionEvent.ACTION_MOVE: int newX = (int) ev.getX(); int newY = (int) ev.getY(); int deltaX = newX - mLastDownX; // 横向偏移量 int deltaY = newY - mLastDownY; // 纵向偏移量 boolean shouldIntercept = Math.abs(deltaX) - Math.abs(deltaY) > 15; // 如果横向偏移量大于纵向偏移量,就说明是横向滑动,那么ViewPager拦截事件(15是个阈值,避免过度灵敏) if (shouldIntercept) { mNext = deltaX < 0; // 如果拦截了事件,判断是翻到上一页还是下一页 } return shouldIntercept; case MotionEvent.ACTION_UP: return false; // up不拦截,在不拦截move事件的前提下,保证子view能收到up事件。因为onClick是在up事件处理时调用的 } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_UP: // 如果move事件被拦下,就是横向滑动。只在处理up事件的时候进行翻页,因为一次滑动,up事件肯定是唯一的 mCurrPos = mNext ? getCurrentItem() + 1 : getCurrentItem() - 1; int itemCount = getAdapter().getCount(); if (mCurrPos >= itemCount) { mCurrPos = 0; } else if (mCurrPos < 0) { break; } setCurrentItem(mCurrPos, true); break; } return super.onTouchEvent(ev); } }
代码注释很清楚,如果不知道为何onClick是在处理up事件被调用的,请参见文章安卓事件分发学习之onTouchEvent方法
2、自定义ListView
这个ListView就是ViewPager的子view了,代码如下
public class MyListView extends ListView { public MyListView(Context context) { super(context); } public MyListView(Context context, AttributeSet attrs) { super(context, attrs); } public MyListView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public MyListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @Override public boolean onTouchEvent(MotionEvent ev) { super.onTouchEvent(ev); // 调用父类的onTouchEvent,让父类处理滚动 return true; // 但是一定要返回true } }
onTouchEvent方法为何返回true,文章开头就说了,此处不再赘言
3、定义ViewPager的适配器
左右无限滑主要是在这儿完成的,代码如下
public class MyViewPagerAdapter extends PagerAdapter { public static final String TAG = "MyViewPagerAdapter"; private LinkedList<MyListView> listViews; public MyViewPagerAdapter(LinkedList<MyListView> listViews) { this.listViews = listViews; } @Override public int getCount() { return Integer.MAX_VALUE; // 无限滑,就是把viewPager的子项设为最大 } @Override public boolean isViewFromObject(View view, Object object) { return view == object; } @Override public Object instantiateItem(ViewGroup container, int position) { int index = position % listViews.size(); // 别忘了取余 MyListView itemView = listViews.get(index); container.addView(itemView); return itemView; } @Override public void destroyItem(ViewGroup container, int position, Object object) { container.removeView(listViews.get(position % listViews.size())); // 左右滑的话,必须实现此方法 } }
4、MainActivity里初始化数据
代码如下
public class MainActivity extends Activity { private MyViewPager mViewPager; private MyViewPagerAdapter mAdapterForViewPager; private LinkedList<MyListView> listViews = new LinkedList<>(); private ArrayList<String> dataPerPage; // 每一页的数据 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mViewPager = findViewById(R.id.root_viewPager); for (int i = 0; i < 5; i++) { dataPerPage = new ArrayList<>(); for (int j = 0; j < 20; j++) { dataPerPage.add("第" + (i + 1) + "页,第" + (j + 1) + "条"); } MyListView listView = new MyListView(this); ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, dataPerPage); // 为了简单,直接用ArrayAdapter listView.setAdapter(arrayAdapter); listViews.add(listView); } mAdapterForViewPager = new MyViewPagerAdapter(this, listViews); mViewPager.setAdapter(mAdapterForViewPager); mViewPager.setCurrentItem(100 * listViews.size()); // 一开始不要设成0,否则程序开始就向左滑的话,会报错 } }
代码很简单,主要是最后setCurrItem不能设成0,因为这样的话,程序开始就向左滑,PagerAdapter.instantiateItem()方法会报错:“当前子view已经有父view”,我在那个方法里尝试了各种removeView,但都无功而返。无奈,只能在最开始设置成一个比较大的是listViews.size()的整数倍的数(以便显示第一页)来实现了
5、效果
6、关于同向冲突的思考
思路还是和上面一样的,只是判断拦截的条件变了
如果是同向竖直滑动冲突的话,比如ListView里套ListView,这种情况就可以考虑:在父ListView的onInterceptTouchEvent方法的Action_Move里,如果mListView(此时最好把子ListView设成父ListView的属性)到头或到底了,父view就拦截事件,代码如下
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mLastDownY = (int) ev.getY(); return false; case MotionEvent.ACTION_MOVE: int newY = (int) ev.getY(); int deltaY = newY - mLastDownY; boolean shouldIntercept = false; if ((deltaY < 0 && mListView.getFirstVisiblePosition() <= 0) || ( deltaY > 0 && mListView.getLastVisiblePosition() >= mListView.getAdapter().getCount() - 1)) { shouldIntercept = true; } return shouldIntercept; case MotionEvent.ACTION_UP: return false; } return super.onInterceptTouchEvent(ev); }
如果是横向滑动冲突的话,比如ViewPager里套ViewPager,几乎和上面的纵向一样,只不过除了把Y相关的换成X,再把getFirstVisiblePosition和getLastVisiblePosition换成getCurrItem就可以,代码如下
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mLastDownX = (int) ev.getX(); return false; case MotionEvent.ACTION_MOVE: int newX = (int) ev.getX(); int deltaX = newX - mLastDownX; boolean shouldIntercept = false; if ((deltaX < 0 && mViewPager.getCurrItem() <= 0) || ( deltaX > 0 && mViewPager.getCurrItem() >= mViewPager.getAdapter().getCount() - 1)) { shouldIntercept = true; } return shouldIntercept; case MotionEvent.ACTION_UP: return false; } return super.onInterceptTouchEvent(ev); }
大体代码就是这样,可以根据业务逻辑再改,但都是换汤不换药
结语
解决滑动冲突的前提是了解安卓里的事件分发机制,大家可以参考我的这几篇文章