其實ViewPager對於觸摸事件的分發已經做得非常好了,HorizontalScrollView以及使用了橫向LinearLayoutManager的RecyclerView或者某些第三方banner輪播控件,基本沒什麼問題,能滾動到最後的,纔會觸發ViewPager的橫向切換滑動。
但是在某些情況下,比如我這邊使用場景是多個菜單欄,使用了第三方類似ViewPager的LayoutManager:PagerGridLayoutManager(https://xiaozhuanlan.com/topic/5841730926) 這個功能確實厲害。解決了List數據需要分頁滑動顯示問題。使用了這個的話,觸摸就不太靈活了,必須是完全橫向才能觸發RecyclerView,不然觸摸就會被ViewPager消費掉。
現在想要實現的是:觸摸在菜單欄的RecyclerView上時,不會觸發ViewPager的橫向滾動。
在這之前,先看看這個問題普遍的解決辦法:
1、把ViewPager設置成不響應滑動觸摸,它就是一個純粹的容器,也很容易實現:不攔截觸摸,不消費觸摸(public boolean onTouchEvent(MotionEvent ev) { return false; } public boolean onInterceptTouchEvent(MotionEvent ev) { return false; } ) 很遺憾,這個辦法被產品否決了。
2、使用NestedScrolling,ViewPager實現NestedScrollingParent。但是這個實現起來是有2個難點:NestedScrolling的2個接口Parent和Child是說,Child要滑動的時候,都會把自己的行動告訴Parent,Parent來判斷是否需要消費,消費多少觸摸距離。但是ViewPager是直接就攔截了觸摸(之所以這樣說,看底下),根本到不了底下的View。因此ViewPager的onInterceptTouchEvent需要大改,然後在onNestedPreScroll來判斷是否消費滑動;第二個難點是對於我來說的,我的菜單欄是作爲一個header放在另一個垂直滑動的RecyclerView內的,沒試過套2層實現NestedScrolling,應該會有很多問題。
3、重寫ViewPager的觸摸了
本篇算是採用了第三種方法,但是比較簡單。
理解這篇文章的前提,是需要對觸摸事件分發有一點小小的瞭解。自上而下 自下而上 當前View onInterceptTouchEvent返回false的時候,事件繼續向下傳遞,返回true,事件到當前view的onTouchEvent處理,onTouchEvent返回false則往上返回。因此假如一個ViewGroup包裹一個View 然後不設置onClick之類的,觸摸View,事件從activity -> ViewGoup -> View -> ViewGoup ->activity 這塊不深究的話比較簡單。
先寫一下開始的思路:開始猜測的是,底層View的問題,也就是上面的RecyclerView。當然,其他控件都沒問題,單單使用了PagerGridLayoutManager就有問題,肯定是PagerGridLayoutManager和ViewPager有衝突。因此我的目光看到了最下層的RecyclerView(下面用rvMenu代替)上,我的想法是rvMenu沒完全消費完觸摸事件,然後就到了上層的ViewPager上,因此寫了個TouchView用來包裹rvMenu:
public class TouchView extends LinearLayout {
public TouchView(Context context) {
super(context);
}
public TouchView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public TouchView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return true;
}
}
不攔截觸摸事件,因此觸摸事件會繼續往下傳遞,onTouchEvent返回true 把觸摸事件都消費掉,這樣ViewPager就接收不到觸摸事件了(說一句,不要去動RecyclerView的觸摸事件,因爲它還是要傳遞給子View的,不能簡單的返回true或者false)。想法很美好,結果並沒有什麼改變。因此可以得出上面說的結論:ViewPager是直接就攔截了觸摸然後進行滑動。
現在目光就轉到了ViewPager上了:現在的思路是,當我觸摸在rvMenu上時,onInterceptTouchEvent返回false,觸摸其他位置時,默認不作處理,那麼整體框架就出來了:
public class MyViewPager extends ViewPager {
public MyViewPager(Context context) {
super(context);
}
public MyViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (是rvMenu的話) {
return false;
}
return super.onInterceptTouchEvent(ev);
}
}
其實文章到這裏就算結束了,判斷返回false的時機,看具體情況。假如你是需要某個特定的ViewPager下的包裹的東西不響應的話,比如說在CFragment的時候橫向滑動不響應,就可以這樣寫:
public class MyViewPager extends ViewPager {
public MyViewPager(Context context) {
super(context);
}
public MyViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Fragment fragment = ((FragmentPagerAdapter) getAdapter()).getItem(getCurrentItem());
if (fragment instanceof CFragment) {
return false;
}
return super.onInterceptTouchEvent(ev);
}
}
我的情況是子View的子View,因此採用的是,MotionEvent的getRawX()和getRawY()來對比rvMenu的位置,如果是處於rvMenu的位置的話,返回false:
public class MyViewPager extends ViewPager {
public MyViewPager(Context context) {
super(context);
}
public MyViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
View v = ((ViewAdapter) getAdapter()).getItem(getCurrentItem());
if (v instanceof MainFeedView) {
if (((MainFeedView) v).isOnTouch((int)ev.getRawX(), (int)ev.getRawY())) {
return false;
} else {
return super.onInterceptTouchEvent(ev);
}
}
return super.onInterceptTouchEvent(ev);
}
}
//MainFeedView裏的isOnTouch方法:
public boolean isOnTouch(int x, int y) {
return isTouchPointInView(rvMenu, x, y);
}
//(x,y)是否在view的區域內
public static boolean isTouchPointInView(View view, int x, int y) {
if (view == null) {
return false;
}
int[] location = new int[2];
view.getLocationOnScreen(location);
int left = location[0];
int top = location[1];
int right = left + view.getMeasuredWidth();
int bottom = top + view.getMeasuredHeight();
//view.isClickable() &&
if (y >= top && y <= bottom && x >= left
&& x <= right) {
return true;
}
return false;
}