Android手勢事件的衝突跟點擊事件的分發過程息息相關,由三個重要的方法來共同完成,分別是:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。
public boolean dispatchTouchEvent(MotionEvent ev)
這個方法用來進行事件的分發。如果事件傳遞到view,那麼這個方法一定會被調用,返回結果受當前View的onTouchEvent和下級View的dispatchTouchEvent方法的影響,表示是否消耗當前事件。
public boolean onInterceptTouchEvent(MotionEvent event)
在上述方法內部調用,用來判斷是攔截某個事件,如果當前View攔截了某個事件,那麼在同一個事件序列當中,此方法不會被再次調用,返回結果表示是否攔截當前事件。
public boolean onTouchEvent(MotionEvent event)
在dispathcTouchEvent方法中調用,用來處理點擊事件,返回結果表示是否消耗當前事件,如果不消耗,則在同一個事件序列中,當前View無法再次接到事件。
例:
public boolean dispatchTouchEvent(MotionEvent ev){ boolean consume = false; if(onInterceptTouchEvent(ev)){ consume = onTouchEvent(ev); } else { consum = child.dispathcTouchEvent(ev); } return consume; }
手勢衝突的解決方案就是用上面的三個方法;主要分為兩種解決方案:·1外部攔截法 2內部攔截法
1.常見的滑動衝突情境
1.1 外部滑動方向和內部滑動的方向不一致
這種情況我們經常遇見,比如使用viewpaper+listview時,在這種效果中,可以通過左右滑動切換頁面,而每一個頁面往往又是一個listview,本來在這種情況下是有衝突的,但是Viewpaper內部處理了這個滑動衝突,因此採用viewpaper我們無需關注這個問題,如果我們採用的不是Viewpaper而是ScrollView等,那麼必須手動處理滑動衝突,否則內外兩層只能有一層滑動,那就是滑動衝突。另外內部左右滑動,外部上下滑動也同樣屬於該類。
1.2 外部滑動方向和內部滑動方向一致
這種情況就比較複雜,當內外兩層都在同一個方向可以滑動的時候,顯然存在邏輯問題,因為當手指開始滑動的時候,系統無法知道使用者到底是想讓那一層動,所以當手指滑動的時候就會出現問題,要麼只能一層動,要麼內外兩成動的都很卡頓。
2.給出解決方案
2.1 外部攔截法
針對情境1,我們可以發現外部和內部的滑動方向不一樣也就是說只要判斷當前dy和dx的大小,如果dy>dx,那麼當前就是豎直滑動,否則就是水平滑動。明確了這個我就就可以根據當前的手勢開始攔截了。
從上一節中我們分析了view的事件分發,我們知道點擊事件的分發順序是 通過父布局分發,如果父布局沒有攔截,即onInterceptTouchEvent返回false,才會傳遞給子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; }
上面的代碼差多就是外部攔截的通用模板了,在onInterceptTouchEvent方法中,
首先是ACTION_DOWN這個事件,父容器必須返回false,即不攔截事件,因為一旦父容器攔截了ACTION_DOWN這個事件,那麼後續的ACTION_MOVE和ACTION_UP事件將直接交給父容器處理,這個時候事件沒法繼續傳遞給子項目了;
然後是ACTION_MOVE這個事件,這個事件可以根據需要決定是否攔截,如果父容器需要攔截就返回true,否則返回false;
最後是ACTION_UP這個事件,這裡必須返回false,因為這個事件本身也沒有太多意義。
下面我們來具體做一下攔截的操作,我們需要在水平滑動的時候父容器攔截事件。
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: { int deltaX=x-mLastXIntercept; int deltaY=y=mLastYIntercept; if(Math.abs(deltaX)>Math.abs(deltaY)){ intercepted=true; }else{ intercepted=false; } break; } case MotionEvent.ACTION_UP: { intercepted = false; break; } default: break; } mLastXIntercept=x; mLastYIntercept=y; return intercepted; }
從上面的代碼來看,我們只是修改了一下攔截條件而已,所以說外部攔截還是很簡單方便的。在滑動的過程中,當水平方向的距離大時就判定水平滑動。
還是一貫我們做實驗來證明理論的風格,我們來自訂一個HorizontalScrollView來體現一下用外部攔截法解決衝突的快感。
先上一下代碼:
package com.gxl.viewtest;import android.animation.Animator;import android.animation.ObjectAnimator;import android.content.Context;import android.text.LoginFilter;import android.util.AttributeSet;import android.util.Log;import android.view.GestureDetector;import android.view.MotionEvent;import android.view.VelocityTracker;import android.view.View;import android.view.ViewGroup;import android.widget.Scroller;/** * s * Created by GXL on 2016/7/25 0025. */public class HorizontalScrollView extends ViewGroup { private final String TAG = "HorizontalScrollView"; private VelocityTracker mVelocityTracker; private Scroller mScroller; private int mChildrenSize; private int mChildWidth; private int mChildIndex; //上次滑動的座標 private int mLastX = 0; private int mLastY = 0; //上次上次攔截滑動的座標 private int mLastXIntercept = 0; private int mLastYIntercept = 0; public HorizontalScrollView(Context context) { super(context); init(context); } public HorizontalScrollView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public HorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } public void init(Context context) { mVelocityTracker = VelocityTracker.obtain(); mScroller = new Scroller(context); } 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: { int deltaX = x - mLastXIntercept; int deltaY = y - mLastYIntercept; if (Math.abs(deltaX) > Math.abs(deltaY)) { intercepted = true; } else { intercepted = false; } break; } case MotionEvent.ACTION_UP: { intercepted = false; break; } default: break; } mLastX = x; mLastY = y; mLastXIntercept = x; mLastYIntercept = y; return intercepted; } @Override public boolean onTouchEvent(MotionEvent event) { mVelocityTracker.addMovement(event); int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: int deltaX = x - mLastX; if((getScrollX()-deltaX)>=0&&(getScrollX()-deltaX)<=(getMeasuredWidth()-ScreenUtils.getScreenWidth(getContext()))) { scrollBy(-deltaX, 0); } break; case MotionEvent.ACTION_UP: mVelocityTracker.computeCurrentVelocity(1000); float xVelocityTracker = mVelocityTracker.getXVelocity(); if (Math.abs(xVelocityTracker) > 50) { if (xVelocityTracker > 0) { Log.i(TAG, "快速向右劃"); } else { Log.i(TAG, "快速向左劃"); } } mVelocityTracker.clear(); break; } mLastX = x; mLastY = y; return true; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int measuredWidth = 0; int measureHeight = 0; final int childCount = getChildCount(); measureChildren(widthMeasureSpec, heightMeasureSpec); int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec); int widthSpaceMode = MeasureSpec.getMode(widthMeasureSpec); int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec); int heightSpaceMode = MeasureSpec.getMode(heightMeasureSpec); if (childCount == 0) { setMeasuredDimension(0, 0); } else if (heightSpaceMode == MeasureSpec.AT_MOST && widthSpaceMode == MeasureSpec.AT_MOST) { final View childView = getChildAt(0); measuredWidth = childView.getMeasuredWidth() * childCount; measureHeight = childView.getMeasuredHeight(); setMeasuredDimension(measuredWidth, measureHeight); } else if (heightSpaceMode == MeasureSpec.AT_MOST) { measureHeight = getChildAt(0).getMeasuredHeight(); setMeasuredDimension(widthSpaceSize, measureHeight); } else if (widthSpaceMode == MeasureSpec.AT_MOST) { final View childView = getChildAt(0); measuredWidth = childView.getMeasuredWidth() * childCount; setMeasuredDimension(measuredWidth, heightSpaceSize); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { Log.i(TAG, "onLayout: " + getMeasuredWidth()); int childleft = 0; final int childCount = getChildCount(); mChildrenSize = childCount; for (int i = 0; i < mChildrenSize; i++) { final View childView = getChildAt(i); if (childView.getVisibility() != View.GONE) { final int childWidth = childView.getMeasuredWidth(); mChildWidth = childWidth; childView.layout(childleft, 0, childleft + mChildWidth, childView.getMeasuredHeight()); childleft += childWidth; } } } private void smoothScrollTo(int destX,int destY) { int scrollX=getScrollX(); int delta=destX-scrollX; mScroller.startScroll(scrollX,0,delta,0,1000); } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); } }}
再來看一下布局檔案
<com.gxl.viewtest.HorizontalScrollView android:layout_width="wrap_content" android:layout_height="match_parent" android:background="#00ff00" > <ListView android:id="@+id/listview1" android:layout_width="600dp" android:layout_height="match_parent" android:background="@color/colorPrimary" > </ListView> <ListView android:id="@+id/listview2" android:layout_width="600dp" android:layout_height="match_parent" android:background="@color/colorAccent" > </ListView> <ListView android:id="@+id/listview3" android:layout_width="600dp" android:layout_height="match_parent" android:background="#ff0000" > </ListView> </com.gxl.viewtest.HorizontalScrollView>
以上就是外部處理滑動衝突的代碼,認真看一下,思路還是很清晰的。裡面還涉及了一些自訂View的知識,我會在後面的博文中認真分析一下代碼,你先看一下onInterceptTouchEvent處理滑動衝突的部分。
看一下效果圖哈。
2.2 內部攔截法
內部攔截法是指父容器不攔截任何事件,所有的事件都傳遞給子項目,如果子項目需要此事件就直接消耗掉,否則就交給父容器去處理,這種方法和Android中的事件分發機制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,這個方法的大體解釋就是:
requestDisallowInterceptTouchEvent是ViewGroup類中的一個公用方法,參數是一個boolean值,官方介紹如下
Called when a child does not want this parent and its ancestors to intercept touch events with ViewGroup.onInterceptTouchEvent(MotionEvent).
This parent should pass this call onto its parents. This parent must obey this request for the duration of the touch (that is, only clear the flag after this parent has received an up or a cancel.
android系統中,一次點擊事件是從父view傳遞到子view中,每一層的view可以決定是否攔截並處理點擊事件或者傳遞到下一層,如果子view不處理點擊事件,則該事件會傳遞會父view,由父view去決定是否處理該點擊事件。在子view可以通過設定此方法去告訴父view不要攔截並處理點擊事件,父view應該接受這個請求直到此次點擊事件結束。
使用起來外部攔截事件略顯複雜一點。下面我也先來看一下它的通用模板(注意下面的代碼是定義在子View中的):
public boolean onInterceptTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { parent.requestDisallowInterceptTouchEvent(true); //父布局不要攔截此事件 break; } case MotionEvent.ACTION_MOVE: { int deltaX=x-mLastXIntercept; int deltaY=y=mLastYIntercept; if(父容器需要攔截的事件){ parent.requestDisallowInterceptTouchEvent(false); //父布局需要要攔截此事件 } break; } case MotionEvent.ACTION_UP: { intercepted = false; break; } default: break; } mLastXIntercept=x; mLastYIntercept=y; return super.dispathTouchEvent(event); }
上述代碼是內部攔截法的典型代碼,當面對不同的滑動策略時只需要修改裡面的條件即可,其他不需要修改做改動而且也不能改動。
以上就是本文的全部內容,希望對大家的學習有所協助,也希望大家多多支援雲棲社區。