android父子控制元件手勢衝突的解決

Monkeyhahaha發表於2013-08-21
在android介面開發中,經常可以遇到一些多層控制元件巢狀的情況,如果父子控制元件都有對應的手勢操作(如scrollview中巢狀pageview),那麼他們都手勢操作就有可能相互干擾,影響介面的流暢性和體驗。

  

首先,要談一下android父子控制元件之間事件的分發,對於事件的分發有幾個原則需要了解。

     (1) android事件分發是從父控制元件向子控制元件逐級分發傳遞的。

     (2) 每一層控制元件都可能消費這個事件,消費後不再向下傳遞(這也是父子控制元件對於手勢操作衝突的主要原因)。          

     (3) android系統中的每個ViewGroup的子類都具有下面三個和事件分發處理密切相關的方法:

              1.public boolean dispatchTouchEvent(MotionEvent ev)            這個方法用來分發TouchEvent

              2.public boolean onInterceptTouchEvent(MotionEvent ev)         這個方法用來攔截TouchEvent

              3.public boolean onTouchEvent(MotionEvent ev)                  這個方法用來處理TouchEvent

      (4) android的觸控是由一個ACTION_DOWN(按下),多個ACTION_MOVE(移動),一個ACTION_UP(抬起)組成。


父子控制元件間的事件傳遞,用一張圖可以更好的體現:

1

(1)、distachTouchEvent用於分發事件,true直接消費事件並不在分發,false向intercertTouchEvent分發

(2)、internceptTouchEvent用於攔截事件,true直接分發事件至自己view的onTouchEvent方法經行處理,false繼續分發

         到子view的dispathcTouchEvent。

(3)、onTouchEevent作為事件處理的部分,響應由多個touchevent所組成的手勢。


     下面我們用這些內容來處理一個具體的問題,在豎直滑動的scrollview中巢狀水平滑動的viewpager的問題。viewpager中巢狀listview也可以用類似的方法解決。

     由於scrollview處於佈局的外層,來自系統的觸控事件會先傳遞至外層的scrollview,scrolliew對於事件的消將直接影響事件繼續分發至內層的viewpager。

先來看一下scrollview的distachTouchEvent原始碼:

   @Override
361     public boolean More ...onInterceptTouchEvent(MotionEvent ev) {
362         /*
363          * This method JUST determines whether we want to intercept the motion.
364          * If we return true, onMotionEvent will be called and we do the actual
365          * scrolling there.
366          */
367 
368         /*
369         * Shortcut the most recurring case: the user is in the dragging
370         * state and he is moving his finger.  We want to intercept this
371         * motion.
372         */
373         final int action = ev.getAction();
374         if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
375             return true;
376         }
377 
378         if (!canScroll()) {
379             mIsBeingDragged = false;
380             return false;
381         }
382 
383         final float y = ev.getY();
384 
385         switch (action) {
386             case MotionEvent.ACTION_MOVE:
387                 /*
388                  * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
389                  * whether the user has moved far enough from his original down touch.
390                  */
391 
392                 /*
393                 * Locally do absolute value. mLastMotionY is set to the y value
394                 * of the down event.
395                 */
396                 final int yDiff = (int) Math.abs(y - mLastMotionY);
397                 if (yDiff > mTouchSlop) {
398                     mIsBeingDragged = true;
399                 }
400                 break;
401 
402             case MotionEvent.ACTION_DOWN:
403                 /* Remember location of down touch */
404                 mLastMotionY = y;
405 
406                 /*
407                 * If being flinged and user touches the screen, initiate drag;
408                 * otherwise don't.  mScroller.isFinished should be false when
409                 * being flinged.
410                 */
411                 mIsBeingDragged = !mScroller.isFinished();
412                 break;
413 
414             case MotionEvent.ACTION_CANCEL:
415             case MotionEvent.ACTION_UP:
416                 /* Release the drag */
417                 mIsBeingDragged = false;
418                 break;
419         }
420 
421         /*
422         * The only time we want to intercept motion events is if we are in the
423         * drag mode.
424         */
425         return mIsBeingDragged;
426     }
427 

  397-399可以看到,在scrollview中,當其捕捉到手指上下滑動的距離大於mTouchSlop時,onInterceptTouchEvent方法返回true,攔截事件至ontouchEvent中並消費,內層的viewpager將得不到觸控事件的傳遞。

    雖然onscroll對事件是否攔截有一定的處理,但是有時僅僅這種效果不能滿足需要。這種處理策略意味著,我們水平動viewpager時,必須保持上下滑動的距離小於mTouchSlop,否則,觸控事件會被上層的scrollview直接消費。為了解決這種衝突,個人想法是上層的scrollview不僅要限制豎直方向的TouchSlop,同時要限制水平方向的TouchSlop,避免viewpager的衝突。這樣需要我們重寫scrollview的onInterceptTouchEvent()方法。

具體的實現方法如下:

public class MScrollView extends ScrollView {
	private float FistXLocation;
	private float FistYlocation;
	private boolean Istrigger = false;
	public Animation animationUp;
	public Animation animationDown;
	   private final int TRIGER_LENTH = 50;
	   private final int HORIZOTAL_LENTH = 20;
	private TMHDItemsGridsAvtivity faAvtivity;
	public TMHDScrollView(Context context, AttributeSet attrs) {
		super(context, attrs);
		// TODO Auto-generated constructor stub
	}

	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		// TODO Auto-generated method stub
		
		
		int deltaX = 0;
		int deltaY = 0;

		final float x = ev.getX();
		final float y = ev.getY();

		switch (ev.getAction()) {
		case MotionEvent.ACTION_MOVE:
			deltaX = (int)(FistXLocation - x);
			deltaY = (int)(FistYlocation - y);
		if (Math.abs(deltaY) > TRIGER_LENTH
					&& Math.abs(deltaX) < HORIZOTAL_LENTH) {
            
				Istrigger = true;
				return super.onInterceptTouchEvent(ev);
			//攔截這個手勢剩下的部分  ,使他不會響應viewpager的相關手勢
			}

			return false;//沒有觸發攔截條件,不攔截事件,繼續分發至viewpager

		case MotionEvent.ACTION_DOWN:
			FistXLocation = x;
			FistYlocation = y;
			if(getScaleY()<-400){
				System.out.println(getScaleY());
			}
			requestDisallowInterceptTouchEvent(false); 
			return  super.onInterceptTouchEvent(ev);

		case MotionEvent.ACTION_CANCEL:
		case MotionEvent.ACTION_UP:
			if (Istrigger) {

				Istrigger = false;
				return  super.onInterceptTouchEvent(ev);
			}

			break;
		}
		return super.onInterceptTouchEvent(ev);
		
	}
}


下面這部分就是自己定義的水平和垂直的事件攔截觸發條件。

	private final int TRIGER_LENTH = 50;
	private final int HORIZOTAL_LENTH = 20;

重寫的MScrollView,自定義了onInterceptTouchEvent()方法,從而自定義了攔截的條件。解決了父子兩個控制元件之間相互衝突的問題。











相關文章