滑动冲突,就是我们滑动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 listViews;
public MyViewPagerAdapter(LinkedList 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 listViews = new LinkedList<>();
private ArrayList 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 arrayAdapter = new ArrayAdapter(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);
}
大体代码就是这样,可以根据业务逻辑再改,但都是换汤不换药
解决滑动冲突的前提是了解安卓里的事件分发机制,大家可以参考我的这几篇文章
安卓事件分发学习之dispatchTouchEvent方法
安卓事件分发学习之onInterceptTouchEvent方法
安卓事件分发学习之onTouchEvent方法