第三章-View事件體系(事件分發機制、滑動衝突解決)

一、View的事件分發
1、點擊事件的傳遞規則

在介紹點擊事件的傳遞規則之前,首先明白分析的對象就是MotionEvent,即點擊事件。所謂點擊事件的事件分發,其實就是對MotionEvent事件的分發過程,即當一個MoonEvent產生了以後,系統需要把這個事件傳遞給一個具體的View,而這個傳遞的過程就是分發過程。點擊事件的分發過程由三個很重要的分發來完成dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent,下面我們先介紹一下這幾個方法。

在這裏插入圖片描述
上述三個方法到底有什麼區別呢?它們是什麼關係呢?其實它們的關係可以用如下僞代碼可以瞭解

public boolean dispatchTouchEvent(MotionEvent ev) {

	boolean consume = false;
	if(onInterceptTouchEvent(ev)){
		consume = onTouchEvent(ev);
	}else {
		consume = child.dispatchTouchEvent(ev);
	}
	return consume;

}

上述的僞代碼已經將三者的區別說明了,我們可以大致的瞭解傳遞的規則就是,對於一個根ViewGroup來說,點擊事件產生以後,首先傳遞給它,這時它的dispatchTouchEvent就會被調用,如果這個ViewGroup的onIntereptTouchEvent方法返回true,就表示它要攔截當前事件,接着事件就會交給這個ViewGroup處理,則他的onTouchEvent方法就會被調用;如果這個ViewGroup的onIntereptTouchEvent方法返回false就表示不需要攔截當前事件,這時當前事件就會繼續傳遞給它的子元素,接着子元素的onIntereptTouchEvent方法就會被調用,如此反覆直到事件被最終處理。

在這裏插入圖片描述

關於事件傳遞的機制,這裏給出一些結論,根據這些結論可以更好地理解整個傳遞機制,如下所示。

(1)同一個事件序列是指從手指接觸屏幕的那一刻起,到手指離開屏慕的那一刻結束,在這個過程中所產生的一系列事件,這個事件序列以down事件開始,中間含有數量不定的move事件,最後以up結束。

(2)正常情況下,一個事件序列只能被一個View攔截且消耗。這一條的原因可以參考(3),因爲一旦一個元素攔截了某此事件,那麼同一個事件序列內的所有事件都會直接交給它處理,因此同一個事件序列中的事件不能分別由兩個View同時處理,但是通過特殊手段可以做到,比如一個Vew將本該自己處理的事件通過onTouchEvent強行傳遞給其他View處理。

(3)某個View一旦決定攔截,那麼這一個事件序列都只能由它來處理(如果事件序列能夠傳遞給它的話),並且它的onInterceprTouchEvent不會再被調用。這條也很好理解,就是說當一個View決定攔截一個事件後,那麼系統會把同一個事件序列內的其他方法都直接交給它來處理,因此就不用再調用這個View的onInterceptTouchEvent去詢問它是否要攔截了。

(4)**某個View一旦開始處理事件,如果它不消耗ACTON_DOWN事件(onTouchEvent返回了false),那麼同一事件序列中的其他事件都不會再交給它來處理,並且事件將重新交由它的父元素去處理,即父元素的onTouchEvent會被調用。**意思就是事件一旦交給一個View處理,那麼它就必須消耗掉,否則同一事件序列中剩下的事件就不再交給它來處理了,這就好比上級交給程序員一件事,如果這件事沒有處理好,短期內上級就不敢再把事情交給這個程序員做了,二者是類似的道理。

(5)如果View不消耗除ACTION_DOWN以外的其他事件,那麼這個點擊事件會消失,此時父元素的onTouchEvent並不會被調用,並且當前View可以持續收到後續的事件,最終這些消失的點擊事件會傳遞給Activity處理。

(6) ViewGroup默認不攔截任何事件。Android源碼中ViewGroup的onInterceptTouchEvent方法默認返回false

(7)View沒有onInterceptTouchEvent方法,一旦有點擊事件傳遞給它,那麼它的onTouchEvent方法就會被調用。

(8)view的onTouchEvent默認都會消耗事件(返回true),除非它是不可點擊的(clickable和longClickable同時爲false),View的longClickable屬性默認都爲false,clickable屬性要分情況,比如Button的clickable屬性默認爲true,而TextView 的clickable屬性默認爲false

(9) view 的enable屬性不影響onTouchEvent的默認返回值。哪怕一個View是disable狀態的,只要它的clickable或者longclickable有一個爲true,那麼它的onTouchEvent就返會true。

(10)onClick會發生的前提實際當前的View是可點擊的,並且他收到了down和up的事件

(11) 事件傳遞過程是由外到內的,理解就是事件總是先傳遞給父元素,然後再由父元素分發給子View,通過requestDisallowInterptTouchEvent方法可以在子元素中干預父元素的事件分發過程,但是ACTION_DOWN除外。

二、事件分發的源碼解析
1.Activity對點擊事件的分發過程
在這裏插入圖片描述

//Activity#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
	if (ev.getAction() == MotionEvent.ACTION_DOWN) {
		onUserInteraction();
	}
	if (getWindow().superDispatchTouchEvent(ev)) {
		return true;
	}
	return onTouchEvent(ev);
}

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

2.頂級View對事件的分發過程
在這裏插入圖片描述

// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
		|| mFirstTouchTarget != null) {
	final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
	if (!disallowIntercept) {
		intercepted = onInterceptTouchEvent(ev);
		ev.setAction(action); // restore action in case it was changed
	} else {
		intercepted = false;
	}
} else {
	// There are no touch targets and this action is not an initial down
	// so this view group continues to intercept touches.
	intercepted = true;
}

在這裏插入圖片描述

ViewGroup不攔截事件的時候,事件會向下分發由他的子View進行處理

final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
	final int childIndex = getAndVerifyPreorderedIndex(
			childrenCount, i, customOrder);
	final View child = getAndVerifyPreorderedView(
			preorderedList, children, childIndex);

	// If there is a view that has accessibility focus we want it
	// to get the event first and if not handled we will perform a
	// normal dispatch. We may do a double iteration but this is
	// safer given the timeframe.
	if (childWithAccessibilityFocus != null) {
		if (childWithAccessibilityFocus != child) {
			continue;
		}
		childWithAccessibilityFocus = null;
		i = childrenCount - 1;
	}

	if (!canViewReceivePointerEvents(child)
			|| !isTransformedTouchPointInView(x, y, child, null)) {
		ev.setTargetAccessibilityFocus(false);
		continue;
	}

	newTouchTarget = getTouchTarget(child);
	if (newTouchTarget != null) {
		// Child is already receiving touch within its bounds.
		// Give it the new pointer in addition to the ones it is handling.
		newTouchTarget.pointerIdBits |= idBitsToAssign;
		break;
	}

	resetCancelNextUpFlag(child);
	if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
		// Child wants to receive touch within its bounds.
		mLastTouchDownTime = ev.getDownTime();
		if (preorderedList != null) {
			// childIndex points into presorted list, find original index
			for (int j = 0; j < childrenCount; j++) {
				if (children[childIndex] == mChildren[j]) {
					mLastTouchDownIndex = j;
					break;
				}
			}
		} else {
			mLastTouchDownIndex = childIndex;
		}
		mLastTouchDownX = ev.getX();
		mLastTouchDownY = ev.getY();
		newTouchTarget = addTouchTarget(child, idBitsToAssign);
		alreadyDispatchedToNewTouchTarget = true;
		break;
	}

3.View對點擊事件的處理

View對點擊事件的處理稍微有點簡單, 這裏注意,這裏的View不包含ViewGroup,先看他的dispatchTouchEvent方法

public boolean dispatchTouchEvent(MotionEvent event) {
	boolean result = false;
	...
	if (onFilterTouchEventForSecurity(event)) {
		if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
			result = true;
		}
		//noinspection SimplifiableIfStatement
		ListenerInfo li = mListenerInfo;
		if (li != null && li.mOnTouchListener != null
				&& (mViewFlags & ENABLED_MASK) == ENABLED
				&& li.mOnTouchListener.onTouch(this, event)) {
			result = true;
		}

		if (!result && onTouchEvent(event)) {
			result = true;
		}
	}
	...	
	return result;
}

因爲他只是一個View,他沒有子元素所以無法向下傳遞,所以只能自己處理點擊事件,從上門的源碼可以看出View對點擊事件的處理過程,首選會判斷你有沒有設置onTouchListener**,如果onTouchListener中的onTouch爲true,那麼onTouchEvent就不會被調用,可見onTouchListener的優先級高於onTouchEvent,這樣做到好處就是方便在外界處理點擊事件**;
接着我們再來分析下onTouchEvent的實現,先看當View處於不可用的狀態下點擊事件的處理過程,如下,很顯然,不可用狀態下的View照樣會消耗點擊事件,儘管他看起來不可用。

下面再看一下onTouchEvent中點擊事件的具體處理,如下所示:

if (((viewFlags & CLICKABLE) == CLICKABLE ||
		(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
		(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
	switch (action) {
		case MotionEvent.ACTION_UP:
			boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
			if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
				// take focus if we don't have it already and we should in
				// touch mode.
				boolean focusTaken = false;
				if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
					focusTaken = requestFocus();
				}

				if (prepressed) {
					// The button is being released before we actually
					// showed it as pressed.  Make it show the pressed
					// state now (before scheduling the click) to ensure
					// the user sees it.
					setPressed(true, x, y);
			   }

				if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
					// This is a tap, so remove the longpress check
					removeLongPressCallback();

					// Only perform take click actions if we were in the pressed state
					if (!focusTaken) {
						// Use a Runnable and post this rather than calling
						// performClick directly. This lets other visual state
						// of the view update before click actions start.
						if (mPerformClick == null) {
							mPerformClick = new PerformClick();
						}
						if (!post(mPerformClick)) {
							performClick();
						}
					}
				}
		 black;
	 }
....
return true;
}

在這裏插入圖片描述

public boolean performClick() {
	final boolean result;
	final ListenerInfo li = mListenerInfo;
	if (li != null && li.mOnClickListener != null) {
		playSoundEffect(SoundEffectConstants.CLICK);
		li.mOnClickListener.onClick(this);
		result = true;
	} else {
		result = false;
	}

	sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
	return result;
}

在這裏插入圖片描述

public void setOnClickListener(@Nullable OnClickListener l) {
	if (!isClickable()) {
		setClickable(true);
	}
	getListenerInfo().mOnClickListener = l;
}

public void setOnLongClickListener(@Nullable OnLongClickListener l) {
	if (!isLongClickable()) {
		setLongClickable(true);
	}
	getListenerInfo().mOnLongClickListener = l;
}

二、View的滑動衝突

滑動衝突時怎樣產生的?其實在界面中只要內外兩層同時可以滑動,這個時候就會產生滑動衝突,如何解決滑動衝突?這既是一鍵困難的事情又是一件簡單的事情,說困難時因爲許多開發者面對滑動衝突都會顯得束手無策,說簡單是因爲滑動衝突的解決辦法有固定的套路。

1.常見的滑動衝突場景

這裏是引用

先說場景1,主要是將ViewPager和Fragment配合使用所組成的頁面滑動效果,主流應用幾乎都會使用這個效果。在這種效果中,可以通過左右滑動來切換頁面,而每個頁面內部往往又是一個Listview。本來這種情況下是有滑動衝突的,但是viewPager內部處理了這種滑動衝突,因此採用ViewPager時我們無須關注這個問題,如果我們採用的不是ViewPager而是ScrollView等,那就必須手動處理滑動衝突了,否則造成的後果就是內外兩層只能有一層能夠滑動,這是因爲兩者之間的滑動事件有衝突。除了這種典型情況外,還存在其他情況,比如外部上下滑動、內部左右滑動等,但是它們屬於同一類滑動衝突。

再說場景2,這種情況就稍微複雜一些,當內外兩層都在同一個方向可以滑動的時候,顯然存在邏輯問題。因爲當手指開始滑動的時候,系統無法知道用戶到底是想讓哪一層滑動,所以當手指滑動的時候就會出現問題,要麼只有一層能滑動,要麼就是內外兩層都滑動得很卡頓。在實際的開發中,這種場景主要是指內外兩層同時能上下滑動或者內外兩層,同時能左右滑動。

最後說下場景3,場景3是場景1和場景2兩種情況的嵌套,因此場景3的滑動衝突看起來就更加複雜了。比如在許多應用中會有這麼一個效果:內層有一個場景1中的滑動效果,然後外層又有一個場景2中的滑動效果。具體說就是,外部有一個SlidingMenu效果,然後內部有一個ViewPager,ViewPager的每一個頁面中又是一個Listview。雖然說場景3的滑動衝突看起來更復雜,但是它是幾個單一的滑動衝突的疊加,因此只需要分別處理內層和中層、中層和外層之間的滑動衝突即可,而具體的處理方法其實是和場景1、場景2相同的。
從本質上來說,這三種滑動衝突場景的複雜度其實是相同的,因爲它們的區別僅僅是滑動策略的不同,至於解決滑動衝突的方法,它們幾個是通用的。

2.滑動衝突的處理規則

在這裏插入圖片描述
在這裏插入圖片描述
3.滑動衝突的解決方式

主要有2種:外部攔截法(推薦使用)、內部攔截法。

a.外部攔截法
在這裏插入圖片描述

@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;//必須不能攔截,如果攔截了,後續move和up都由父元素處理了
			break;
		case MotionEvent.ACTION_MOVE:
			if("父容器需要當前點擊事件"){
				intercepted = true;
			}else {
				intercepted = false;
			}
			break;
		case MotionEvent.ACTION_UP:
			intercepted = false;//必須不能攔截,如果攔截了,子元素的onClick點擊事件就無法觸發
			break;
	}
	mLastXIntercept = x;
	mLastYIntercept = x;
	return intercepted;
}

在這裏插入圖片描述

b.內部攔截法

內部攔截法是指父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要此事件就直接消耗掉,否則就交由父容器進行處理,這種方法和Android中的事件分發機制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起來較外部攔截法稍顯複雜。它的僞代碼如下,我們需要重寫子元素的dispatchTouchEvent方法:

@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("父容器需要此類點擊事件"){
				getParent().requestDisallowInterceptTouchEvent(false);//設置攔截
			}
			break;
		case MotionEvent.ACTION_UP:

			break;
	}
	mLastX = x;
	mLastY = y;
	return super.dispatchTouchEvent(event);//這個值取決於子view的處理結果,
	//例如這個view如果繼承ListView,就需要看listview的這個方法處理結果。並不是再調用了父容器的這個方法。
}

在這裏插入圖片描述

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
	int action = ev.getAction();
	if(action == MotionEvent.ACTION_DOWN){
		return false;//不攔截
	}else {
		return true;
	}
}

總結下:使用外部攔截只需要處理父容器的OnInterceptTouchEvent方法。使用內部攔截需要處理子容器的dispatchTouchEvent和重寫父元素的OnInterceptTouchEvent方法。所以建議使用外部攔截法會比較方便。

下面演示一個外部攔截法的實例:

自定義一個HorizontalScrollViewEx

package com.example.viewsample;


import android.content.Context;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.ViewGroup;
import android.widget.Scroller;

public class HorizontalScrollViewEx extends ViewGroup {

    public static final String TAG = "HorizontalScrollViewEx";

    private int mChindrensize;
    private int mChindrenWidth;
    private int mChindrenIndex;
    //分別記錄上次滑動的座標
    private int mLastX = 0;
    private int mLastY = 0;
    //分別記錄上次滑動的座標
    private int mLastXIntercept = 0;
    private int mLastYIntercept = 0;

    private Scroller mScroller;
    private VelocityTracker mVelocityTracker;

    private void init() {
        mScroller = new Scroller(getContext());
        mVelocityTracker = VelocityTracker.obtain();
    }

    public HorizontalScrollViewEx(Context context) {
        super(context);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

    }

    @Override
    public boolean onInterceptHoverEvent(MotionEvent event) {
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercepted = true;
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                    intercepted = true;
                }
                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;
        }
        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:
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                scrollBy(-deltaX, 0);//攔截後處理邏輯
                break;
            case MotionEvent.ACTION_UP:
                int scrollX = getScrollX();
                int scrollToChildIndex = scrollX / mChindrenWidth;
                mVelocityTracker.computeCurrentVelocity(1000);
                float xVelocity = mVelocityTracker.getXVelocity();
                if (Math.abs(xVelocity) >= 50) {
                    mChindrenIndex = xVelocity > 0 ? mChindrenIndex - 1 : mChindrenIndex + 1;
                } else {
                    mChindrenIndex = (scrollX + mChindrenWidth / 2) / mChindrenWidth;
                }
                mChindrenIndex = Math.max(0, Math.min(mChindrenIndex, mChindrensize - 1));
                int dx = mChindrenIndex * mChindrenWidth - scrollX;
                ssmoothScrollBy(dx, 0);
                mVelocityTracker.clear();
                break;
        }
        mLastX = x;
        mLastY = y;
        return true;
    }

	//這個方法上面一章講解過,Scroller的彈性滑動原理
    private void ssmoothScrollBy(int dx, int i) {
        mScroller.startScroll(getScrollX(),0,dx,500);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if(mScroller.computeScrollOffset()){
            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            postInvalidate();
        }
    }
}


測試:

public class MainActivity extends AppCompatActivity {

    public static final String TAG = "MainActivity";
    private HorizontalScrollViewEx mListContainer;
    private int w,h;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.i(TAG,"onCreate");
        initView();
    }

    private void initView() {
        LayoutInflater inflater = getLayoutInflater();
        mListContainer = findViewById(R.id.container);
        //屏幕寬高
        WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
        w = wm.getDefaultDisplay().getWidth();
        h = wm.getDefaultDisplay().getHeight();
        for (int i = 0; i < 3; i++) {
            ViewGroup layout = inflater.inflate(R.layout.content_layout,mListContainer,false);
            layout.getLayoutParams().width = w;
            TextView textview = (TextView) layout.findViewById(R.id.title);
            textview.setText("page"  + (i+1));
            layout.setBackgroundColor(Color.rgb(255/(i+1),255/(i+1),0));
            createList(layout);
            mListContainer.addView(layout);
        }
    }

    private void createList(ViewGroup layout) {
        ListView listview = (ListView) layout.findViewById(R.id.list);
        ArrayList<String>datas= new ArrayList<>();
        for (int i = 0; i < 50; i++) {
            datas.add("names" + i);
        }
        ArrayAdapter<String>adapter = new ArrayAdapter<String>(this,R.layout.content_list_item,R.id.name,datas);
        listview.setAdapter(adapter);
    }
}

內部攔截參考上面的僞代碼自行研究下。滑動衝突解決基本就掌握了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章