处理滑动冲突

滑动冲突可以分为三类

  1. 外部滑动方向和内部滑动方向不一致

    1. 场景:类似viewpager嵌套listview的情况
    2. 处理思路:直接根据逻辑来就好,(外左右内上下时)当用户左右滑动,需要让外部view拦截事件,上下滑动就让内部view拦截事件
    3. 具体方法:根据滑动的特征是水平滑动还是竖直滑动来判断到底由谁来拦截事件
      1. 如何根据滑动过程两点间的坐标得到滑动方向?有好几个思路

        1. 根据滑动路径和水平方向的夹角
        2. 根据水平方向和竖直方向上的距离差[比较方便]
        3. 还可以根据水平和竖直方向的速度差
      2. 拦截事件:事件分发
  2. 外部滑动方向和内部滑动方向一致
    1. 处理思路:

      1. 这个时候根据逻辑来行不通,用户滑动的时候不知道用户到底是想让哪一层滑动;
      2. 但是这个时候一般能在业务上找到突破点,根据业务需求得到具体的处理规则
  3. 上面两种情况嵌套
    1. 场景:外部有一个slidemenu,内部有一个嵌套了listview的viewpager
    2. 处理思路:
      1. 虽然是前面两种情况的叠加,看起来跟复杂,但是只要分开处理内层和中层,中层和外层之间的冲突,逐个击破就好了,具体的处理方式是和场景1、2相同的
      2. 通用也是无法根据滑动特征来判断,只能从业务入手

本质上说这三类的复杂度是相同的,区别只是解决滑动冲突的策略不同,具体解决的方法是通用的



不依赖滑动规则(距离差/角度/逻辑/业务)的通用的解法

1.外部拦截法[建议用这种方法]

  1. 所有的点击事件都先经过父容器拦截处理,如果父容器需要拦截就拦截,不需要就传给内部的View
  2. 这种方法符合点击事件的分发机制

外部拦截法的典型逻辑,重写父view 的onInterceptTouchEvent 方法即可:


public boolean onInterceptTouchEvent(MotionEvent event) {
    boolean intercepted = false;

switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            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;

}

其中:

  1. 针对不同的滑动冲突,只需要修改父容器需要当前点击事件这个条件即可,其他均不需做修改并且也不能修改
  2. ACTION_DOWN这个事件是不能拦截的,因为一旦拦截后续的事件都会由父容器处理了,无法再传递给子view
  3. 应该在ACTION_MOVE中根据需求决定是否拦截
  4. 最后ACTION_UP必须返回false,不拦截,因为这个事件本身没太多意义,但返回true会引发问题:
    1. 假如在ACTION_MOVE中把事件交给子view处理了,但是如果在ACTION_UP中又返回true,就会导致子view无法收到ACTION_UP事件,于是子view的onclick事件就会无法触发

2.内部拦截法

  1. 父容器不拦截任何事件,所有事件都传给子元素。如果子元素需要此事件就直接消耗,否则就交给父容器进行处理。
  2. 因为这种方法和android中的事件处理流有些不一致(因为...),完成这个功能需要配合requestDisallowInterceptTouchEvent()方法才可,这个方法表示是否让父容器拦截事件

重写子view的dispatchTouchEvent方法:


public boolean dispatchTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();

switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            parent.requestDisallowInterceptTouchEvent(true);
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            if (满足父容器的拦截要求) {
                parent.requestDisallowInterceptTouchEvent(false);
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            break;
        }
        default:
            break;
    }
    mLastX = x;
    mLastY = y;
    return super.dispatchTouchEvent(event);
}

  1. 同样,针对不同的滑动策略,只需要修改这个条件即可,其他均不需做修改并且也不能修改

除了子view需要处理外,父view也要拦截除了ACTION_DOWN以外所有的事件

  1. 这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)时,父元素才能拦截所需的事件
  2. 不能拦截ACTION_DOWN的原因是
    1. ACTION_DOWN事件不受这个标记位控制,所以一旦父view拦截这个事件,那么所有的事件都无法传递到子view中,这样内部拦截就无法起作用了

父view的修改:


public boolean onInterceptHoverEvent(MotionEvent event) {
    int action = event.getAction();
    if(action == MotionEvent.ACTION_DOWN){
        return false;
    }else {
        return true;
    }
}


以场景1为例做了外部拦截法讲解

场景2,3的做法与1类似,只不过是根据业务需要制定处理规则

这个例子

  1. 实现了一个类似viewpager嵌套listview的效果
  2. 不过把viewpager改成了自己写的一个HorizontalScrollViewEx,这样就制造了滑动冲突,使用滑动距离差了解决冲突
    1. 这是一个类似水平linearlayout的东西,不过它可以水平滑动

[1]activity

  1. 这段代码很简单,就是创建了三个listview并且把listview加入到自定义的HorizontalScrollViewEx中,成为父子关系

public class DemoActivity_1 extends Activity {
    private HorizontalScrollViewEx mListContainer;

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.demo_1);
       
        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]水平滑动的View

其中:

  1. 前面 if (!mScroller.isFinished()) 这句代码的意图:

    1. 如果用户正在水平滑动(外层在滑动),但是在水平滑动停止之前如果用户再迅速进行竖直滑动,就会导致界面在水平方向无法滑动到终点从而处于一种中间状态
    2. 为了避免这种情况,当水平方向正在滑动时,下一个序列的点击事件仍然直接交给父view处理,也就是继续水平滑(不用管这个时候的操作是水平多还是竖直多),这样水平方向就不会停在中间状态了
  2. 后边还重写了onTouchEvent方法:
    1. 就是拦截下来之后真正对事件进行处理,这就不属于滑动冲突的部分了
    2. 主要实现的是弹性吸附的效果

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: {
                Log.d(TAG, "onInterceptTouchEvent: ACTION_DOWN");
                intercepted = false;

if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                    intercepted = true;
                }
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                Log.d(TAG, "onInterceptTouchEvent: 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: {
                Log.d(TAG, "onTouchEvent: ACTION_DOWN");
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                Log.d(TAG, "onTouchEvent: ACTION_MOVE");
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                Log.d(TAG, "onTouchEvent: deltaX" + deltaX);
                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) >= 100) {
                    mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
                } else {
                    //滑动的距离超过一半,就进入下一页
                    mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
                }

//保证在0页和最后一页滑动时不会越界
                mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));

//没有达到进入下一页的要求,恢复原样
                int dx = mChildIndex * mChildWidth - scrollX;
                smoothScrollBy(dx, 0);

Log.d(TAG, "onTouchEvent: dx = " + dx);
                mVelocityTracker.clear();
                break;
            }
            default:
                break;
        }

mLastX = x;
        mLastY = y;
        //通通返回true
        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 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;
            }
        }
    }

@Override
    protected void onDetachedFromWindow() {
        mVelocityTracker.recycle();
        super.onDetachedFromWindow();
    }
}

参考资料:

《安卓开发艺术探索》

时间: 2024-07-31 08:49:37

处理滑动冲突的相关文章

Android 解决下拉刷新控件和ScrollVIew的滑动冲突问题。

最近项目要实现ScrollView中嵌套广告轮播图+RecyleView卡片布局,并且RecyleView按照header和内容的排列样式,因为RecyleView的可扩展性很强,所以我毫无疑问的选择了它,而且让RecyleView实现了可拖拽的效果, 最后我再加上了下拉刷新的效果(这里我用的下拉刷新控件是三方的SmartRefreshLayout).记得刚开始实现这个效果的时候还是十分的得心印手.可是当我测试的时候,发现RecyleView的子item的拖拽效果并不流畅,起初我以 为是由于Re

(转)ViewPager,ScrollView 嵌套ViewPager滑动冲突解决

ViewPager,ScrollView 嵌套ViewPager滑动冲突解决 本篇主要讲解一下几个问题 粗略地介绍一下View的事件分发机制 解决事件滑动冲突的思路及方法 ScrollView 里面嵌套ViewPager导致的滑动冲突 ViewPager里面嵌套ViewPager 导致的滑动冲突 轮播图的几种实现方式 先看一下效果图 ScrollView里面嵌套ViewPager ViewPager里面嵌套ViewPager View的 事件分发机制 这篇博客大打算详细讲解View的事件分发机制

关于Android滑动冲突的解决方法(二)

之前的一遍学习笔记主要就Android滑动冲突中,在不同方向的滑动所造成冲突进行了了解,这样的冲突非常easy理解,当然也非常easy解决.今天,就同方向的滑动所造成的冲突进行一下了解,这里就先以垂直方向的滑动冲突为背景,这也是日常开发中最常见的一种情况. 这里先看一张效果图 由于GIF 图片大小的限制.截图效果不是非常好 上图是在购物软件上常见的上拉查看图文详情,关于这中动画效果的实现.事实上实现总体的效果,办法是有非常多的,网上有非常多相关的样例,可是对某些细节的处理不是非常清晰.比方,下拉

完美解决ScrollView嵌套ViewPager滑动失效和无法正常滑动冲突问题

/******************************************************************************* * Copyright 2011, 2012 Chris Banes. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the Li

解决侧滑中ViewPager和SlidingMenu的滑动冲突

当我们在使用开源框架SlidingMenu时,如果要是使用到ViewPager,就会出现滑动冲突. 解决方案: }/** 解决ViewPager和侧滑冲突 */ public void changeSlidingMenuTOUCHMODE(int arg0) { switch (arg0) { case 0: if (getActivity() instanceof SlidingFragmentActivity) { SlidingFragmentActivity activity = (Sl

扩展ViewFlow避免和ViewPager滑动冲突,同时支持无限循环,并完美和CircleFlowIndicator结合

首先,为了避免滑动冲突,我们要继承ViewFlow,重写onInterceptTouchEvent 1 public class MyViewFlow extends ViewFlow { 2 private ViewPager mPager; 3 4 public MyViewFlow(Context context, AttributeSet attrs) { 5 super(context, attrs); 6 } 7 8 9 public void setViewPager(ViewPa

一个Demo带你彻底掌握View的滑动冲突

最近在重新学习Android自定义View这一块的内容,遇到了平时开发中经常碰到的一个棘手问题:View的滑动冲突.相信不少小伙伴都有相同的感觉,看似简单真正做起来却又不知道从何下手.今天就从一个简单的Demo带你彻底掌握解决View滑动冲突的办法. 老规矩,先上图: 示例图中是一个常见的下拉回弹,手指向下滑动的时候,整个布局会一起滑动.下拉到一定距离的时候松手,布局会自动回弹到开始的位置:手指向上滑动的时候,布局的子View会滑动到最底部,然后手指再向下滑动,布局的子View会滑动到最顶部,最

view的滑动冲突解决方案

一.常见的滑动冲突场景 1.外部滑动方向和内部滑动方向不一致 2.外部滑动方向和内部滑动方向一致 3.上面两种情况的嵌套 二.滑动冲突处理的原则 场景1的处理原则是:当用户左右滑动时,需要让外部的view拦截点击事件,当用户上下滑动时,需要让内部的view拦截点击事件.场景2和场景3比较特殊,无法如同场景1一样原则的处理冲突,需要在业务上寻找突破点.比如业务上规定:当处于某种状态时需要外部View响应用户的滑动,而处于另一种状态时则需要内部View来响应View的滑动,根据这种业务上的需求我们也

Android 滑动冲突处理

要想解决滑动冲突就必须好好理解 Android 的事件分发机制.不了解 Android 事件分发机制的请先参考资料学习一下. 一般有 2 种方法 1 外部拦截法 这个非常简单,因为事件是从父 view 向子 view 进行分发的,所以我们可以重写父控件的 onInterceptTouchEvent, 如果父容器需要某个事件就拦截,如果不需要就不拦截交给子view处理. 伪代码如下 public boolean onInterceptTouchEvent(MotionEvent event) {

Android中ViewPager与HorizontalListView的滑动冲突处理

Android开发中,有不少的控件都有点击或滑动冲突事件,比如ListView的onitemclick事件与item上的Button(如果有Button的话)等.今天在工作中用ViewPager里面的页面套用HorizontalListView,横向 划动也有冲突,解决办法很简单,只要在HorizontalListView中重写onInterceptTouchEvent(MotionEvent ev)方法中添加 getParent().requestDisallowInterceptTouchE