全文解析ViewGourp事件分发以及滑动冲突。(三)

1.滑动冲突的场景

好,上一篇文章我们已经基本讲述了View的分发机制了。全文解析ViewGourp事件分发以及滑动冲突。(二)
那我们就开始讲述一下滑动冲突的问题。
常见的滑动冲突的场景:
(1)滑动方向不相同,ViewPage和ListView
(2)滑动方向相同,ViewPage和Scrollview
(3)符合嵌套(禁止套娃)
在这里插入图片描述
当然做过一段Android开发的同志已经开始发表疑问了,我ViewPage+ListView并没有发生这样的事情啊,那是因为Google的大佬早就想到这个问题,ViewPage已经处理了这个冲突了,如果我们自定义一个ViewGroup再用ListView,那么就要我们自己处理滑动冲突了。
在这里插入图片描述

2.滑动冲突解决方案

针对第一种场景,很容易就让人联想到,其实只要知道是向上下滑/向左右滑,就知道交给谁处理了。而这种判断,我们只需要根据水平坐标差垂直坐标差就可以进行判断了。
在这里插入图片描述
针对第二种场景,那我们就没办法根据滑动的方向进行判断了,那么咱办呢?那也只能根据业务逻辑View的位置去进行判断。

上述两种场景的冲突本质区别上,只是判断逻辑不同而已,但其实处理套路是一样的。

3.滑动冲突解决套路

套路一 外部拦截法
顾名思义,一开始是从父容器慢慢分发进去到子View的,那就是一旦判断条件是父容器的滑动,那么父容器就会将事件进行拦截。那我们将逻辑处理放到父View的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;
    }

需要注意的点:
ACTION_DOWN返回flase.否则子容器将接受不到ACTION_MOVE和ACION_UP的事件。
ACTION_UP,返回false.否则子容器也接收不到ACTION_UP事件,OnClick也不会被触发。

套路二 内部拦截法
父View不拦截任何事件,所有事件都传递给子View,子View根据需要决定是自己消费事件还是给父View处理。这需要子View使用requestDisallowInterceptTouchEvent方法才能正常工作。requestDisallowInterceptTouchEvent()方法的作用就是请求父类容器是否拦截。True–>不拦截,flase–>拦截。
(伪代码)

    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;
            }
            //ACTION_DOWN,不拦截,到子View-->ACTION_MOVE.
            //告诉父View拦截,父VIEW开始拦截除了Down之外的事件,此时就不会执行子View的TouchEVent了
            //这样就是执行父View的TouchEvent
            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);
    }

父View重写onInterceptTouchEvent方法:

    public boolean onInterceptTouchEvent(MotionEvent event) {

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

注意的点:这里的逻辑可能会稍微有点绕,意思就是父VIew一开始是不会拦截任何事情到子View,这是因为子View用了requestDisallowInterceptTouchEvent(true)
到了子View的ACTION_MOVE后,判断到了需要截止的事情了,那么子View告诉父容器,你要拦截事情了。那么父View就会开始拦截,不会再执行子View的onTouchEvent,而是执行父的onTouchEvent

4.喜闻乐见的试验环节

之前说过,ViewPage帮我们处理过了滑动冲突,而他是作为ViewGroup,我们把他已经帮忙处理的操作弄掉,为了造成滑动冲突,那我们自定义一个ViewPage,并重写onInterceptTouchEvent方法,默认返回flase.

public class BadViewPager extends ViewPager {

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

    //除了Down,全都拦截了
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getAction() & MotionEvent.ACTION_MASK;
        if (action == MotionEvent.ACTION_DOWN){
            super.onInterceptTouchEvent(ev);
            return false;
        }
        return true;
    }
}

好,接下来新建一个ScrollConflictActivity来测试冲突。

public class ScrollConflictActivity extends AppCompatActivity {
    private BadViewPager mViewPager;
    private List<View> mViews;
    private Context mContext = this;
    List<View> gListView = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_scroll_conflict);
        initViews();
        initData(false);
    }

    protected void initViews() {
        mViewPager = findViewById(R.id.Badviewpager);
        mViews = new ArrayList<>();
    }

    protected void initData(final boolean isListView) {
        if(isListView){
            for (int  i =0;i<3;i++) {
                BadEnternalConflictFixListView listView = new BadEnternalConflictFixListView(mContext);
                final ArrayList<String> datas = new ArrayList<>();
                for(int j=0;j<20;j++){
                    datas.add("data"+j);
                }
                //初始化adapter
                ArrayAdapter<String> adapter = new ArrayAdapter<>
                        (mContext, android.R.layout.simple_list_item_1, datas);
                listView.setAdapter(adapter);
                View a = listView;
                gListView.add(a);
            }
        }else {
            TextView textView = new TextView(mContext);
            textView.setGravity(Gravity.CENTER);
            textView.setText("1");
            textView.setClickable(true);
            gListView.add(textView);
            TextView textView2 = new TextView(mContext);
            textView2.setGravity(Gravity.CENTER);
            textView2.setText("2");
            gListView.add(textView2);
            TextView textView3 = new TextView(mContext);
            textView3.setGravity(Gravity.CENTER);
            textView3.setText("3");
            gListView.add(textView3);
        }

        //将ListView赋值给当前View
        mViewPager.setAdapter(new PageAdapterViewPage(gListView));
    }

    //ViewPage适配器
    public class PageAdapterViewPage extends PagerAdapter {
        private List<View> mViewList;
        private int ModifyViewPosition = 0;
        public PageAdapterViewPage(List<View> mviewlist) {
            this.mViewList = mviewlist;

        }



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


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

        @NonNull
        @Override
        public Object instantiateItem(@NonNull ViewGroup container, int position) {
            //创建item时,设置tag,有item的position放入tag

            container.addView(mViewList.get(position));
            return mViewList.get(position);
        }

        @Override
        public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
            container.removeView(mViewList.get(position));
        }

    }
}

好了,我们先是用往我们的BadViewPage加了三个TextView.并把第一个TextView.setClickable(true)。这样当我们进入的时候,就发现,ViewPage无法被拖动了。
为什么我们这里要将TextVIew设置为可以点击的呢?
因为TextView本身的不可单击的,那么它的的onTouchEvent是不会消费事件的,即返回False,所以如果不设置的话,事件将会返回到VIewGroup,那就可以拖动了。
此时为True,即消费了事件,所以ViewGroup本身存在的onTouchEvent方法就使用不了,就无法拖动了。造成的原因就是父View不能正确相应事件。

接下来我们修改一下代码,initData传入为True,那么ViewPage加入的就是ListView.
显然ListView是可以滑动的,BadViewPager是不能滑动的。我们分别通过外部拦截和内部拦截方法来对BadViewPager进行修复。

1.外部拦截法


public class BadExternalConflictViewPage extends ViewPager {
    private int mLastXIntercept;
    private int mLastYIntercept;
    private String TAG="BadExternalConflictViewPage";
    public BadExternalConflictViewPage(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int x = (int)ev.getX();
        int y = (int)ev.getY();
        final int action = ev.getAction() & MotionEvent.ACTION_MASK;
        switch (action){
            case MotionEvent.ACTION_DOWN:
                //调用ViewPager的onInterceptTouchEvent方法初始化mActivePointerId
                //这个Id默认值是-1,在onTouchEvent中会靠这个判断这个事件是否被子View消耗了.
                //如果消耗了,那么也到不了这里的Move
                super.onInterceptTouchEvent(ev);
                break;
            case MotionEvent.ACTION_MOVE:
                //横坐标增量
                int deltaX = x - mLastXIntercept;
                //纵坐标位移增量
                int deltaY = y - mLastYIntercept;
                if (Math.abs(deltaX)>Math.abs(deltaY)){
                    intercepted = true;//相当于把事件拦截到这里,不到子View了,自己消耗了。
                }else{
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
            default:
                break;
        }
        mLastXIntercept = x;
        mLastYIntercept = y;
        Log.d(TAG, "intercepted = : "+intercepted);
        return intercepted;
    }
}

我们在Actiivty将BadPageView替换到我们的BadExternalConflictViewPage
根据外部拦截的理论,我们只需要重写ViewGroup的onInterceptTouchEvent,并且ACTION_DOWN和ACTION_UP都返回False.
在ACITON_MOVE的时候,逻辑判断了手势是左右滑动,所以,此时返回True,即在这里,ViewPage就将这个事件拦截了,就会触发ViewPage的onInterceptTouchEvent。

这里我们在ACTION_DOWN
当中还调用了super.onInterceptTouchEvent(ev);即ViewPager的onInterceptTouchEvent方法。主要是为了初始化ViewPager的成员变量mActivePointerId。mActivePointerId默认值为-1,mActivePointerId不进行初始化,ViewPager会认为这个事件已经被子View给消费了

在这里插入图片描述

2.内部拦截法
内部拦截法需要重写ListView的dispatchTouchEvent方法,所以我们自定义一个ListView:

public class BadEnternalConflictFixListView extends ListView {

    private static final String TAG = "FixListView";

    private int mLastX;
    private int mLastY;

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

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

        final int action = ev.getAction() & MotionEvent.ACTION_MASK;

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                //这个方法是请求父类不要拦截
                getParent().requestDisallowInterceptTouchEvent(true);
                Log.d(TAG, "ACTION_DOWN: "+1);
                break;
                //ACTION_DOWN,不拦截,到子View-->ACTION_MOVE.
                //告诉父View拦截,父VIEW开始拦截除了Down之外的事件,此时就不会执行子View的TouchEVent了
                //这样就是执行父View的TouchEvent
            case MotionEvent.ACTION_MOVE:
                Log.d(TAG, "ACTION_MOVE: "+2);

                //水平移动的增量
                int deltaX = x - mLastX;
                //竖直移动的增量
                int deltaY = y - mLastY;
                //当水平增量大于竖直增量时,表示水平滑动,此时需要父View去处理事件
                if (Math.abs(deltaX) > Math.abs(deltaY)){
                    //请求父类拦截?。。
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }
        mLastX = x;
        mLastY = y;


        return super.dispatchTouchEvent(ev);
    }
}

再看BadViewPager,需要重写拦截方法

public class BadViewPager extends ViewPager {

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

    //除了Down,全都拦截了
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getAction() & MotionEvent.ACTION_MASK;
        if (action == MotionEvent.ACTION_DOWN){
            super.onInterceptTouchEvent(ev);
            return false;
        }
        return true;
    }
}

其实可以看得到,我们只不过判断的逻辑一个放在VIewGroup里面,一个是放在View里面。一个是在ViewGroup判断我是否直接把这个事件“吃”掉,不留了。一个是VIew判断,告诉VIewGroup你要不要把这个事件“吃”掉。其实方法效果和逻辑是一样的。

好了,只要理解这两种方法,各种复杂的嵌套,只要细心分析逻辑操作,只要对其中的方法进行重写,我们就可以套用这种套路去解决。 当然,我们现在看得觉得外部拦截法比较简单,而且也比较符合我们的线性逻辑。所以,首先用外部拦截发是一个很好的方法来处理滑动冲突~
OVER~
在这里插入图片描述
大佬的文章:
事件分发:
https://bthvi-leiqi.blog.csdn.net/article/details/105333893
滑动冲突
https://www.jianshu.com/p/982a83271327

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值