摘要:事件沖突解決思路與方案目錄介紹
1.事件機制簡單介紹 1.1觸摸事件1.2分發事件1.3攔截事件
2.解決滑動沖突的思路及方法 2.1第一種情況,滑動方向不同2.2第二種情況,滑動方法相同2.3第三種情況,以上兩種情況嵌套
3.案例解決方法 3.1針對2問題的解決思路3.2滑動方向不同,解決沖突的外部解決法3.3滑動方向不同,解決沖突的內部解決法3.4ViewPager嵌套ViewPager內部解決法3.5滑動方向相同,解決沖突的外部解決法3.6解決ScrollView和ViewPage
事件沖突解決思路與方案
目錄介紹
1.事件機制簡單介紹
1.1 觸摸事件
1.2 分發事件
1.3 攔截事件
2.解決滑動沖突的思路及方法
2.1 第一種情況,滑動方向不同
2.2 第二種情況,滑動方法相同
2.3 第三種情況,以上兩種情況嵌套
3.案例解決方法
3.1 針對2問題的解決思路
3.2 滑動方向不同,解決沖突的外部解決法
3.3 滑動方向不同,解決沖突的內部解決法
3.4 ViewPager嵌套ViewPager內部解決法
3.5 滑動方向相同,解決沖突的外部解決法
3.6 解決ScrollView和ViewPager,RecycleView滑動沖突
1.事件機制簡單介紹
1.1 觸摸事件
/**
- 觸摸事件
- 如果返回結果為false表示不消費該事件,并且也不會截獲接下來的事件序列,事件會繼續傳遞
- 如果返回為true表示當前View消費該事件,阻止事件繼續傳遞
- 在這里要強調View的OnTouchListener。如果View設置了該監聽,那么OnTouch()將會回調。
- 如果返回為true那么該View的OnTouchEvent將不會在執行 這是因為設置的OnTouchListener執行時的優先級要比onTouchEvent高。
- 優先級:OnTouchListener > onTouchEvent > onClickListener
- @param event
- @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e("onEvent","MyLinearLayout onTouchEvent");
return super.onTouchEvent(event);
}
1.2 分發事件
/**
- 分發事件
- 根據內部攔截狀態,向其child或者自己分發事件
- @param ev
- @return
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e("onEvent","MyLinearLayout dispatchTouchEvent");
return super.dispatchTouchEvent(ev);
}
1.3攔截事件
/**
- 攔截事件
- 默認實現是返回false,也就是默認不攔截任何事件
- 判斷自己是否需要截取事件
- 如果該方法返回為true,那么View將消費該事件,即會調用onTouchEvent()方法
- 如果返回false,那么通過調用子View的dispatchTouchEvent()將事件交由子View來處理
- @param ev
- @return
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e("onEvent","MyLinearLayout onInterceptTouchEvent");
return super.onInterceptTouchEvent(ev);
}
2.解決滑動沖突的思路及方法
2.1 第一種情況,滑動方向不同
2.2 第二種情況,滑動方法相同
2.3 第三種情況,以上兩種情況嵌套
3.案例解決方法
3.1 針對2問題的解決思路
看了上面三種情況,我們知道他們的共同特點是 父View 和 子View 都想爭著響應我們的觸摸事件,但遺憾的是我們的觸摸事件 同一時刻 只能被某一個View或者ViewGroup攔截消費,所以就產生了滑動沖突?那既然同一時刻只能由某一個View或者ViewGroup消費攔截,那我們就只需要 決定在某個時刻由這個View或者ViewGroup攔截事件,另外的 某個時刻由 另外一個View或者ViewGroup攔截事件不就OK了嗎?綜上,正如 在 《Android開發藝術》 一書提出的,總共 有兩種解決方案
3.2 滑動方向不同,解決沖突的外部解決法【以ScrollView與ViewPager為例 】
舉例子:以ScrollView與ViewPager為例
從 父View 著手,重寫 onInterceptTouchEvent 方法,在 父View 需要攔截的時候攔截,不要的時候返回false,代碼大概如下
public class MyScrollView extends ScrollView {
public MyScrollView(Context context) {
super(context);
}
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(21)
public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
private float mDownPosX = 0;
private float mDownPosY = 0;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final float x = ev.getX();
final float y = ev.getY();
final int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mDownPosX = x;
mDownPosY = y;
break;
case MotionEvent.ACTION_MOVE:
final float deltaX = Math.abs(x - mDownPosX);
final float deltaY = Math.abs(y - mDownPosY);
// 這里是夠攔截的判斷依據是左右滑動,讀者可根據自己的邏輯進行是否攔截
if (deltaX > deltaY) {
return false;
}
}
return super.onInterceptTouchEvent(ev);
}
}
3.3 滑動方向不同,解決沖突的內部解決法【以ScrollView與ViewPager為例 】
從子View著手,父View 先不要攔截任何事件,所有的 事件傳遞給 子View,如果 子View 需要此事件就消費掉,不需要此事件的話就交給 父View 處理。
實現思路 如下,重寫 子View 的 dispatchTouchEvent 方法,在 Action_down 動作中通過方法 requestDisallowInterceptTouchEvent(true) 先請求 父View 不要攔截事件,這樣保證 子View 能夠接受到Action_move事件,再在Action_move動作中根據 自己的邏輯是否要攔截事件,不要的話再交給 父View 處理
public class MyViewPager extends ViewPager {
private static final String TAG = "yc";
int lastX = -1;
int lastY = -1;
public MyViewPager(Context context) {
super(context);
}
public MyViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getRawX();
int y = (int) ev.getRawY();
int dealtX = 0;
int dealtY = 0;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
dealtX = 0;
dealtY = 0;
// 保證子View能夠接收到Action_move事件
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
dealtX += Math.abs(x - lastX);
dealtY += Math.abs(y - lastY);
Log.i(TAG, "dealtX:=" + dealtX);
Log.i(TAG, "dealtY:=" + dealtY);
// 這里是夠攔截的判斷依據是左右滑動,讀者可根據自己的邏輯進行是否攔截
if (dealtX >= dealtY) {
getParent().requestDisallowInterceptTouchEvent(true);
} else {
getParent().requestDisallowInterceptTouchEvent(false);
}
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_CANCEL:
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(ev);
}
}
3.4 ViewPager嵌套ViewPager內部解決法
從 子View ViewPager著手,重寫 子View 的 dispatchTouchEvent方法,在 子View 需要攔截的時候進行攔截,否則交給 父View 處理,代碼如下
public class ChildViewPager extends ViewPager {
private static final String TAG = "yc";
public ChildViewPager(Context context) {
super(context);
}
public ChildViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int curPosition;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
curPosition = this.getCurrentItem();
int count = this.getAdapter().getCount();
Log.i(TAG, "curPosition:=" +curPosition);
// 當當前頁面在最后一頁和第0頁的時候,由父親攔截觸摸事件
if (curPosition == count - 1|| curPosition==0) {
getParent().requestDisallowInterceptTouchEvent(false);
} else {//其他情況,由孩子攔截觸摸事件
getParent().requestDisallowInterceptTouchEvent(true);
}
}
return super.dispatchTouchEvent(ev);
}
}
3.5 滑動方向相同,解決沖突的外部解決法【解決ScrollView和RecycleView滑動沖突】
public class RecyclerScrollview extends ScrollView {
private int downY;
private int mTouchSlop;
public RecyclerScrollview(Context context) {
super(context);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
public RecyclerScrollview(Context context, AttributeSet attrs) {
super(context, attrs);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
public RecyclerScrollview(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
int action = e.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
downY = (int) e.getRawY();
break;
case MotionEvent.ACTION_MOVE:
int moveY = (int) e.getRawY();
if (Math.abs(moveY - downY) > mTouchSlop) {
return true;
}
}
return super.onInterceptTouchEvent(e);
}
}
**注意:RecycleView一定要被嵌套里面**
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:descendantFocusability="blocksDescendants">
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
3.6 解決ScrollView和ViewPager,RecycleView滑動沖突
對于ScrollView
public class BottomScrollView extends ScrollView {
private OnScrollToBottomListener mOnScrollToBottomListener;
public BottomScrollView(Context context) {
super(context);
}
public BottomScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public BottomScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public BottomScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt){
super.onScrollChanged(l,t,oldl,oldt);
// 滑動的距離加上本身的高度與子View的高度對比
if(t + getHeight() >=getChildAt(0).getMeasuredHeight()){
// ScrollView滑動到底部
if(mOnScrollToBottomListener != null) {
mOnScrollToBottomListener.onScrollToBottom();
}
} else {
if(mOnScrollToBottomListener != null) {
mOnScrollToBottomListener.onNotScrollToBottom();
}
}
}
public void setScrollToBottomListener(OnScrollToBottomListener listener) {
this.mOnScrollToBottomListener = listener;
}
public interface OnScrollToBottomListener {
void onScrollToBottom();
void onNotScrollToBottom();
}
}
**// ViewPager滑動沖突解決**
mViewPager.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN) {
// 記錄點擊到ViewPager時候,手指的X坐標
mLastX = event.getX();
}
if(action == MotionEvent.ACTION_MOVE) {
// 超過閾值,禁止SwipeRefreshLayout下拉刷新,禁止ScrollView截斷點擊事件
if(Math.abs(event.getX() - mLastX) > THRESHOLD_X_VIEW_PAGER) {
mRefreshLayout.setEnabled(false);
mScrollView.requestDisallowInterceptTouchEvent(true);
}
}
// 用戶抬起手指,恢復父布局狀態
if(action == MotionEvent.ACTION_UP) {
mRefreshLayout.setEnabled(true);
mScrollView.requestDisallowInterceptTouchEvent(false);
}
return false;
}
});
**// ListView滑動沖突解決**
mListView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN) {
mLastY = event.getY();
}
if(action == MotionEvent.ACTION_MOVE) {
int top = mListView.getChildAt(0).getTop();
float nowY = event.getY();
if(!isSvToBottom) {
// 允許scrollview攔截點擊事件, scrollView滑動
mScrollView.requestDisallowInterceptTouchEvent(false);
} else if(top == 0 &;&; nowY - mLastY > THRESHOLD_Y_LIST_VIEW) {
// 允許scrollview攔截點擊事件, scrollView滑動
mScrollView.requestDisallowInterceptTouchEvent(false);
} else {
// 不允許scrollview攔截點擊事件, listView滑動
mScrollView.requestDisallowInterceptTouchEvent(true);
}
}
return false;
}
});
此篇文章來源于:https://www.aliyun.com/jiaocheng/4207.html
或者為了加深理解,下面這篇博文講解的很好
ScrollView嵌套RecyclerView滑動沖突相關問題
一、應用場景
在解決具體問題之前,先介紹下實際應用場景及問題狀況。
從圖中可以看出,一個ScrollView內部嵌套三個RecyclerView,其中兩個RecyclerView是橫向,一個RecyclerView是縱向。
在這個場景下,出現了滑動沖突問題,主要表現為橫向RecyclerView滑動不靈敏,縱向RecyclerView滑動卡頓。
二、問題分析
1.橫向RecyclerView滑動不靈敏
該問題所產生的滑動沖突如上圖所示。
針對該問題,解決的方案是根據當前滑動方向,水平還是垂直來判斷這個事件到底該交給誰來處理。
一般情況下根據滑動路徑形成的夾角(或者說是斜率如下圖)、水平和豎直方向滑動速度差來判斷。
2.縱向RecyclerView滑動卡頓
該問題所產生的滑動沖突如上圖所示。
針對該問題,一般情況下必需通過業務邏輯來進行判斷,決定到底誰來處理該事件。
三、滑動沖突解決方法
針對滑動沖突,一般有兩個解決方法。
1.外部攔截法
事件都先經過父容器的攔截處理,如果不需要此事件就不攔截,這樣就可以解決滑動沖突的問題。外部攔截法需要重寫父容器的onInterceptTouchEvent()方法,在內部完成相應的攔截即可
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.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;
}
}
return intercepted;
}
ACTION_DOWN 這個事件里父容器必須返回 false,即不攔截ACTION_DOWN事件,因為一旦攔截了那么后續的 ACTION_MOVE、ACTION_UP都由父容器去處理,事件就無法傳到子view了
ACTION_MOVE 事件可以根據需要來進行攔截或者不攔截
ACTION_UP 這個事件必須返回false,就會導致子View無法接受到UP事件,這個時候子元素中的onClick()事件就無法處觸發。
2.內部攔截法
父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要此事件就直接消耗掉,否則就交由父容器進行處理。這種方法需要配合requestDisallowInterceptTouchEvent()方法才能正常工作。
主要是修改子view的dispatchTouchEvent()方法
@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:
{ if (父容器需要此類事件) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP:
{
break;
}
}
return super.dispatchTouchEvent(ev);
}
父容器需要重寫onInterceptTouchEvent()方法
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if(action == MotionEvent.ACTION_DOWN){
return false;
}else {
return true;
}
}
父容器攔截ACTION_DOWN以外的其他事件,因為ACTION_DOWN 事件不受 FLAG_DISALLOW_INTERCEPT這個標記的控制,所以一旦父容器攔截了ACTION_DOWN 事件那么所有的事件都無法傳到子view中去了,這樣內部攔截法就不起作用了。
四、問題解決
下面就來實際解決本文中遇到的滑動沖突問題。通過上述分析可知,本文所遇到的問題通過外部攔截法,重寫ScrollView的onInterceptTouchEvent()方法即可快速簡單的解決。
public class FScrollView extends ScrollView {
private float mLastXIntercept = 0f;
private float mLastYIntercept = 0f;
public FScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
float x = ev.getX();
float y = ev.getY();
int action = ev.getAction() & MotionEvent.ACTION_MASK;
switch (action) {
case MotionEvent.ACTION_DOWN: {
intercepted = false; //初始化mActivePointerId super.onInterceptTouchEvent(ev);
break;
}
case MotionEvent.ACTION_MOVE: {
//橫坐標位移增量
float deltaX = x - mLastXIntercept;
//縱坐標位移增量
float deltaY = y - mLastYIntercept;
if (Math.abs(deltaX) < Math.abs(deltaY)) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
}
Math.abs(deltaX) < Math.abs(deltaY)表示橫向位移增量小于于豎向位移增量,即豎直滑動,則ScrollView 攔截事件。
super.onInterceptTouchEvent(ev),初始化mActivePointerId,避免出現Invalid pointerId=-1 in onTouchEvent問題。
縱向RecyclerView的滑動被攔截,交給ScrollView處理,需要測量高度,會默認加載所有item,相當于LinearLayout,從而導致復用效率大大降低。所以如果情況復雜,建議采用頭布局。
此篇文章來源于:http://www.lxweimin.com/p/d456f4e88697