安卓开发学习之滑动冲突的解决方案

背景

滑动冲突,就是我们滑动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方法

你可能感兴趣的:(view滚动,安卓开发)