欢迎访问移动开发之家(rcyd.net),关注移动开发教程。移动开发之家  移动开发问答|  每日更新
页面位置 : > > > 内容正文

View事件分发原理和ViewPager+ListView嵌套滑动冲突,

来源: 开发者 投稿于  被查看 39844 次 评论:183

View事件分发原理和ViewPager+ListView嵌套滑动冲突,


目录

    前言:

    一个touch事件序列包括:down、move、up(其中move事件会多次触发,就是说如果手指在屏幕上多次滑动的时候会多次触发move事件,可以利用这一点实现view 的移动)

    ViewGroup:用来进行事件分发 View:用来对事件的处理

    分发流程: Activity#dispatchTouchEvent -> PhoneWindow#superDispatchTouchEvent -> DecorView#superDispatchTouchEvent ->ViewGroup#dispatchTouchEvent -> View#dispatchTouchEvent ->View#OnTouchEvent

    从下往上看,先看事件如何被处理的,先看一个例子

    btn.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        Log.e("hover","onCLick");
      }
    });
    btn.setOnTouchListener(new View.OnTouchListener() {
      @Override
      public boolean onTouch(View v, MotionEvent event) {
        Log.e("hover", "onTouch:" + event.getAction());
        //return false;//return false的话两个打印日志都有
        return true;//只有onTouch日志会打印
      }
    });

    对同一个组件设置两个监听,此处有三个面试题:

    • 1、onTouch的返回值有什么用
    • 2、onTouch和onClick哪个先调用(还有一个onTouchEvent)
    • 3、在哪里调用的

    带着问题看看View的dispatchTouchEvent 

     从代码中可以看出,onTouch的优先级要高于onTouchEvent,而onClick是在onTouchEvent中调用的,因此onClick的优先级最低

    注意以上三个方法可能都不会执行,因为三个方法都是在View的dispatchTouchEvent的执行的,如果连dispatchTouchEvent都不执行的话,那么三个方法就都不会执行了

    什么情况下View的dispatchTouchEvent会不执行呢:父容器不分发事件给View,就不会执行,即父容器不会调用子View的dispatchTouchEvent方法

    那什么时候父容器不会分发事件给View呢?这就需要看看事件分发的过程了: Activity#dispatchTouchEvent -> PhoneWindow#superDispatchTouchEvent -> DecorView#superDispatchTouchEvent -> ViewGroup#dispatchTouchEvent

    ViewGroup中的dispatchTouchEvent中的核心地方可以用两句伪代码来阐述(摘自Android开发艺术探索):

    如果,ViewGroup的onInterceptTouchEvent方法执行了,则表示ViewGroup拦截了当前事件,去执行自己的 onTouchEvent逻辑,否则将事件分发给子View去执行

    ViewGroup的分发逻辑主要有三个部分:

    第一部分:判断是否拦截该事件: 

    第二部分:分发事件给View,看哪个子View处理事件(源码太多了,只粘了后半部分)

    注意:当Move事件来的时候不会走第二部分代码!!!

    第三部分:执行事件:单指操作还是多指操作

    • 1、上下滑动的时候,此时ViewPager被设置为不允许拦截,所以事件交给了ListView,ListView正常上下滑动没问题
    • 2、左右滑动的时候,此时ViewPager被设置为允许拦截,而在ViewPager的onInterceptTouchEvent方法中move事件返回了true,所以拦截事件成功,ViewPager会执行自己的ontouchevent,实现左右滑动

    子View去执行事件逻辑:

    dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
    handled = child.dispatchTouchEvent(event);    

    总结:

    1、如果父View拦截了事件并消费了事件,则子View 的dispatchTouchEvent就不会执行 2、如果父View并没有拦截事件,但是所有的子View都没有消费此事件,则最后也是执行父View的dispatchTouchEvent 3、如果父View没有拦截事件,且某个子View拦截了此事件消费了,事件就不会再向下个子View传递,如果没有消费,则会继续遍历下一个子View(这段逻辑再第二部分的for循环中)

    如果子View处理了就提前break

    如何解决自定View 的滑动冲突呢:根据实际情况去分配事件

    • 1、在子View中去处理(内部拦截法) 通常也会涉及父类的改动
    • 2、在父View中去处理(外部拦截法)

    以内部拦截法做一个例子:ViewPager中嵌套ListView

    public class SlideInflictFragment extends Fragment {
      private BasePager mPager;
      List<MyListView> mListViews = new ArrayList<>();
      private String[] data={"Apple","Banana","Orange","Watermelon","Pear","Grape","Pineapple",
          "Strawberry","Cherry","Mango","Apple","Banana","Orange","Watermelon","Pear","Grape",
          "Pineapple","Strawberry","Cherry","Mango"};
      @Nullable
      @Override
      public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.slide_inflict_view_layout, container, false);
      }
      @Override
      public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    
      }
      @Override
      public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        mPager = view.findViewById(R.id.viewPager);
        initListViews();
        mPager.setAdapter(new MyPagerAdapter(mListViews));
      }
      private void initListViews(){
        MyListView l1 = new MyListView(getContext());
        MyListView l2 = new MyListView(getContext());
        MyListView l3 = new MyListView(getContext());
        ArrayAdapter<String> adapter = new ArrayAdapter<>(getActivity(),R.layout.slide_inflict_list_item,data);
        l1.setAdapter(adapter);l2.setAdapter(adapter);l3.setAdapter(adapter);
        mListViews.add(l1);mListViews.add(l2);mListViews.add(l3);
      }
      public class MyPagerAdapter extends PagerAdapter{
        public List<MyListView> mListViews;
        public MyPagerAdapter(List<MyListView> mListViews) {
          this.mListViews = mListViews;
        }
        @Override
        public int getCount() {
          return mListViews.size();
        }
    
        @Override
        public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
          return view == object;
        }
        @NonNull
        @Override
        public Object instantiateItem(@NonNull ViewGroup container, int position) {
          container.addView(mListViews.get(position));
          return mListViews.get(position);
        }
        @Override
        public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
          container.removeView(mListViews.get(position));
        }
      }
    }
    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);
      }
      private int mLastX,mLastY;
      @Override
      public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()){
          case MotionEvent.ACTION_DOWN:
            getParent().requestDisallowInterceptTouchEvent(true);//表示父容器不能拦截此事件
            break;
          case MotionEvent.ACTION_MOVE:
            int deltaX = x-mLastX;
            int deltaY = x-mLastY;
            if(Math.abs(deltaX)>Math.abs(deltaY)){
              getParent().requestDisallowInterceptTouchEvent(false);//表示可以拦截
            }
            break;
          default:
            break;
        }
        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
    
      }
    }
    public class MyPager extends ViewPager {
      public MyPager(@NonNull Context context) {
        super(context);
      }
      public MyPager(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
      }
      @Override
      public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
          super.onInterceptTouchEvent(ev);
          return false;//必须要在down事件return false,否则listView就接收不到down事件,也就无法处理touchevent
        }
        return true;
      }
    }

    原理解释:

    首先down事件传递给MyPager, down事件来的时候ViewGroup会重置标志位,而且onInterceptTouchEvent方法一定会执行,所以这里一定要返回false,ListView才会收到Down事件,否则listView是否发下拉的

    按照上述代码,此后ViewGroup应该会执行第二块代码块去分发事件,即listView去处理事件,在ListView中的down事件调用getParent().requestDisallowInterceptTouchEvent(true)方法,会改变ViewGroup中mGroupFlags标志位,进而影响ViewPager中对后续事件的拦截回调的执行与否

    当Move事件到来的时候,由于ListView在Down事件的时候设置了不拦截事件,则ViewPager也不会拦截Move事件,所以此事件落到listView去处理,在ListView中根据手指滑动情况去设置ViewPager是否拦截move事件:

    • 1、上下滑动的时候,此时ViewPager被设置为不允许拦截,所以事件交给了ListView,ListView正常上下滑动没问题
    • 2、左右滑动的时候,此时ViewPager被设置为允许拦截,而在ViewPager的onInterceptTouchEvent方法中move事件返回了true,所以拦截事件成功,ViewPager会执行自己的ontouchevent,实现左右滑动

    到此这篇关于View事件分发原理和ViewPager+ListView嵌套滑动冲突的文章就介绍到这了,更多相关View的事件分发 内容请搜索3672js教程以前的文章或继续浏览下面的相关文章希望大家以后多多支持3672js教程!

    您可能感兴趣的文章:
    • Android编程实现ListView头部ViewPager广告轮询图效果
    • Android 新闻界面模拟ListView和ViewPager的应用
    • Android View的事件分发机制
    • Android View的事件分发详解
    • 如何自己实现Android View Touch事件分发流程
    • Android 深入探究自定义view之事件的分发机制与处理详解

    用户评论