Android基础-滑动冲突

·  阅读 55

「这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战

事件分发流程

注:

  • 整个流程分三层:Activity、ViewGroup、View
  • 整个事件从Activity开始,由Activity的dispatchTouchEvent分发
  • 虚线上的字母代表了这个方法的返回值,分别为super、true、false
  • 图中的事件为ACTION_DOWN
  • View中没有onInterceptTouchEvent方法

结合图,可以得出事件流走向的几个结论:

1.返回值为super.xxx()的情况:事件默认实现都是返回值为super,即我们没有对控件里的方法进行重写或拦截,而是直接用super调用父类的默认实现,那么整个事件流应该是Activity -> ViewGroup -> View,从上往下调用dispatchTouchEvent方法,直到叶子节点(View)的时候,在由View - ViewGroup - Activity从下往上调用onTouchEvent方法。若是ViewGroup,则向下传递的时候会传给onInterceptTouchEvent再传给下层的dispatchTouchEvent,整个事件的流向是U型

2.返回值为false的情况:对于dispatchTouchEvent和onTouchEvent,除了Activity返回值为false表示自己消费了该事件,ViewGroup和View都会将事件传递给父控件的onTouchEvent来处理。而onInterceptTouchEvent返回值为false的时候代表不拦截,默认的super也是不拦截,继续传递给下层的dispatchTouchEvent来处理。

3.返回值为true的情况:对于dispatchTouchEvent和onTouchEvent来说,无论是Activity、ViewGroup、View返回值为true都代表自身来消费该事件,不再向下传递了,对于onInterceptTouchEvent来说,返回值为true代表拦截事件传递,不会向下传递,交给自身的onTouchEvent来处理。

注意: 上面讲解的都是针对ACTION_DOWN的事件,ACTION_MOVE和ACTION_UP在传递的过程中并不是和ACTION_DOWN 一样,你在执行ACTION_DOWN的时候返回了false,(case :ACTION_DOWN的返回值false,不是dispatchTouchEvent的返回值为false)后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个事件(如ACTION_DOWN)返回true,才会收到ACTION_MOVE和ACTION_UP的事件。


对于ACTION_MOVE、ACTION_UP终极总结:
ACTION_DOWN事件在哪个控件消费了(return true), 那么ACTION_MOVE和ACTION_UP就会从上往下(通过dispatchTouchEvent)做事件分发往下传,就只会传到这个控件,不会继续往下传,如果ACTION_DOWN事件是在dispatchTouchEvent消费,那么事件到此为止停止传递,如果ACTION_DOWN事件是在onTouchEvent消费的,那么会把ACTION_MOVE或ACTION_UP事件传给该控件的onTouchEvent处理并结束传递。

滑动冲突解决方案

介绍完了事件分发机制的基本流程,我们来看看滑动冲突。滑动冲突的基本形式分为两种,其他复杂的滑动冲突都可以拆成这两种基本形式:

  • 1:外部滑动方向与内部方向不一致。
  • 2:外部方向与内部方向一致。

先来看第一种, 滑动方向不一致的情况。

举个例子, 比如你用ViewPaper和Fragment搭配,而Fragment里往往是一个竖直滑动的ListView这种情况是就会产生滑动冲突,但是由于ViewPaper本身已经处理好了滑动冲突,所以我们无需考虑,不过若是换成ScrollView,我们就得自己处理滑动冲突了。图示如下:

再看看第二种,这种情况下,因为内部和外部滑动方向一致,系统会分不清你要滑动哪个部分,所以会要么只有一层能滑动,要么两层一起滑动得很卡顿。图示如下:

对于这两种情况,我们有不同的方法来处理它。

第一种:第一种的冲突主要是一个横向的,一个竖向的,所以在开发中我们只要判断滑动方向是竖向还是横向的,再让对应的View滑动即可。判断的方法有很多,比如竖直距离与横向距离的大小比较,哪个距离大就判定为向哪个方向滑动的;滑动路径与水平形成的夹角等等。


第二种:对于这种情况,比较特殊,我们没有通用的规则,得根据业务逻辑来得出相应的处理规则。举个最常见的例子,ListView下拉刷新功能,需要ListView自身滑动实现滑动,但是当滑动到头部时需要ListView和Header一起滑动,也就是整个父容器的滑动,这就涉及到滑动冲突问题了,如果不处理好滑动冲突,就会出现各种意想不到情况。对于这种情况的解决,我们可以采用拦截法:


1.外部拦截法(由父容器决定事件的传递):让事件都经过父容器的拦截处理(onInterceptTouchEvent ),如果父容器需要则拦截,如果不需要则不拦截,称为外部拦截法,其伪代码如下:

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: {
            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;
}
复制代码


代码注释:

a:首先down事件父容器必须返回false ,因为若是返回true,也就是拦截了down事件,那么后续的move和up事件就都会传递给父容器(onTouchEvent),子元素就没有机会处理事件了。

b:其次是up事件也返回了false,一是因为up事件对父容器没什么意义,其次是因为若事件是子元素处理的,却没有收到up事件会让子元素的onClick事件无法触发。


2:内部拦截法(自己决定事件的传递):父容器不拦截任何事件,将所有事件传递给子元素,如果子元素需要则消耗掉,如果不需要则通过requestDisallowInterceptTouchEvent方法(请求父类不要拦截,返回值为true时不拦截,返回值为false时为拦截)交给父容器处理,称为内部拦截法,使用起来稍显麻烦,伪代码如下:

首先我们需要重写子元素的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: {
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            if (父容器需要此类点击事件) {
                parent.requestDisallowInterceptTouchEvent(false);
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            break;
        }
        default:
            break;
    }

    mLastX = x;
    mLastY = y;
    return super.dispatchTouchEvent(event);
}
复制代码

然后修改父容器的onInterceptTouchEvent方法:

public boolean onInterceptTouchEvent(MotionEvent event) {

    int action = event.getAction();
    if (action == MotionEvent.ACTION_DOWN) {
        return false;
    } else {
        return true;
    }
}
复制代码

使用内部拦截法需要注意:

  • 内部拦截法要求父View不能拦截ACTION_DOWN事件,由于ACTION_DOWN不受FLAG_DISALLOW_INTERCEPT标志位控制,一旦父容器拦截ACTION_DOWN那么所有的事件都不会传递给子View。
  • 滑动策略的逻辑放在子View的dispatchTouchEvent方法的ACTION_MOVE中,如果父容器需要获取点击事件则调用 parent.requestDisallowInterceptTouchEvent(false)方法,让父容器去拦截事件。

实战

例:

创建Activity

public class MainActivity extends AppCompatActivity {

    private BadViewPager mViewPager;
    private List<View> mViews;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mViewPager = findViewById(R.id.bad_pager);
        mViews = new ArrayList<>();

        initViews(false);
    }

    private void initViews(boolean flag) {
        for (int i = 0; i < 5; i++) {
            if (flag) {
                TextView textView = new TextView(this);
                textView.setGravity(Gravity.CENTER);
                textView.setTextColor(Color.WHITE);
                textView.setText("View-" + i);
                mViews.add(textView);
            } else {
                final List<String> datas = new ArrayList<>();
                for (int j = 0; j < 50; j++) {
                    datas.add("data " + j);
                }
                ListView listView = new ListView(this);
                ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, datas);
                listView.setAdapter(adapter);
                mViews.add(listView);
            }
        }

        mViewPager.setAdapter(new BasePagerAdapter(mViews));
    }
}
复制代码

创建Adapter

public class BasePagerAdapter extends PagerAdapter {

    private List<View> mViews;

    public BasePagerAdapter(List<View> views) {
        mViews = views;
    }

    @Override
    public int getCount() {
        return mViews.size();
    }

    @Override
    public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
        return view == object;
    }

    @NonNull
    @Override
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        View textView = (View) mViews.get(position);
        container.addView(textView);
        return textView;
    }

    @Override
    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        container.removeView((View) object);
    }
}
复制代码

当使用BadViewPager嵌套ListView时,横向滑动失效。

1.外部拦截法

public class BadViewPager extends ViewPager {
    private int mLastPositionX;
    private int mLastPositionY;

    public BadViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercept = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercept = false;
                super.onInterceptTouchEvent(ev);
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastPositionX;
                int deltaY = y - mLastPositionY;
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    intercept = true;
                } else {
                    intercept = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercept = false;
                break;
            default:
                break;
        }

        mLastPositionX = x;
        mLastPositionY = y;

        return intercept;
    }
}
复制代码

2.内部拦截法

重写ListView的dispatchTouchEvent方法

public class FixListView extends ListView {
    private int mLastX;
    private int mLastY;

    public FixListView(Context context) {
        super(context);
    }

    public FixListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int x = (int) ev.getX();
        int y = (int) ev.getY();

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }

        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(ev);
    }
}
复制代码

在横向滑动大于竖向滑动时,请求父级ViewGroup拦截。

ViewGroup中的改动,默认拦截除ACTION_DOWN之外的事件,等待子View通知

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {

    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        super.onInterceptTouchEvent(ev);
        return false;
    }

    return true;
}
复制代码
分类:
Android
标签: