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的事件分發 內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: