1、前言
Android應用開發中,經常會遇到touch事件分發的問題,甚至還會遇到滑動沖突問題,如果解決滑動沖突、理解touch事件分發原理等,很有必要。
應用開發三部曲系列文章,已經完成兩篇了
結合本文闡述的touch事件分發,一些常見的問題就能輕松解決了。
2、Touch事件分發原理
Touch事件分發涉及到三個重要的方法。
- dispatchTouchEvent,touch事件分發的入口,決定把touch事件交給哪個view處理
- onInterceptTouchEvent,負責touch事件攔截,如果返回為true,則調用onTouchEvent,如果返回為false,則將touch事件交給子view處理,調用子view的dispatchTouchEvent方法
- onTouchEvent,處理touch事件,實現如滑屏等等
具體流程如下圖:
如果子view的onInterceptTouchEvent也返回為false,此時會調用父view的onTouchEvent方法。
View中沒有onInterceptTouchEvent方法,只有ViewGroup才有。
在三級嵌套的頁面中,touch事件分發log為:
05-19 10:45:43.334: I/okunu(28182): root dispatchTouchEvent action = 0
05-19 10:45:43.334: I/okunu(28182): root onInterceptTouchEvent action = 0
05-19 10:45:43.335: I/okunu(28182): viewgroup dispatchTouchEvent action = 0
05-19 10:45:43.335: I/okunu(28182): viewgroup onInterceptTouchEvent action = 0
05-19 10:45:43.335: I/okunu(28182): view dispatchTouchEvent action = 0
05-19 10:45:43.335: I/okunu(28182): view onTouchEvent action = 0
05-19 10:45:43.335: I/okunu(28182): viewgroup onTouchEvent action = 0
05-19 10:45:43.335: I/okunu(28182): root onTouchEvent action = 0
結合log與上圖,touch事件的分發流程就很清楚了。
3、Touch事件分發源碼走讀
查看ViewGroup.java的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (onFilterTouchEventForSecurity(ev)) {
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
//是否允許攔截,如果不允許攔截,則不調用onInterceptTouchEvent方法
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//調用onInterceptTouchEvent,是否攔截此touch事件
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
//不攔截,將touch事件交給子view處理
if (!canceled && !intercepted) {
if (newTouchTarget == null && childrenCount != 0) {
//遍歷子view,找出合適的子view處理此touch事件
for (int i = childrenCount - 1; i >= 0; i--) {
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
//將touch事件交給子view處理
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {}
}
}
}
//如果攔截此touch事件
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
while (target != null) {
final boolean cancelChild = resetCancelNextUpFlag(target.child)|| intercepted;
//如果攔截此touch事件或者子view不處理此事件時,事件由父view處理
if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {
}
}
}
}
}
為了使代碼更簡明,更容易看懂,本人刪減了一些東西,只關注脈絡性內容
查看上述代碼,如果父view攔截touch事件,則調用dispatchTransformedTouchEvent方法,如果父view不攔截touch事件,也要調用此方法。dispatchTransformedTouchEvent方法負責將touch事件最終分發
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
。。。。。。
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
。。。。。。
}
dispatchTransformedTouchEvent方法邏輯比較簡單,和源碼相比上述代碼省略了很多,但其實意思一樣,根據各種條件運算,最后如果傳過來的child為空,則父view處理,如果不為空,則child處理。
可能有細心的同學會問,onTouchEvent方法在哪里調用的呢?查看View.java的dispatchTouchEvent方法,onTouchEvent的此被調用。
4、touch事件處理源碼走讀
touch事件是一個很廣義的范疇,點擊事件、長按事件也會產生touch事件,那View是如何區分touch事件、點擊事件和長按事件的呢?
public boolean onTouchEvent(MotionEvent event) {
switch (action) {
case MotionEvent.ACTION_UP:
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
//如果mHasPerformedLongPress為false,沒有按住一定時間,則將長按消息remove
//remove長按消息,就不會再觸發長按事件了
removeLongPressCallback();
//檢測一定條件,如果條件滿足,則觸發單擊事件
if (!focusTaken) {
if (!post(mPerformClick)) {
performClick();
}
}
}
break;
case MotionEvent.ACTION_DOWN:
//傳來down事件時,將mHasPerformedLongPress設置為false
//mHasPerformedLongPress,表征長按事件是否發生
mHasPerformedLongPress = false;
setPressed(true, x, y);
//發送處理長按事件的消息,該消息將在一定時間后響應
checkForLongClick(0);
break;
}
}
如果LongClick事件執行了,那么mHasPerformedLongPress值為true。
可以查看下systemui中的虛擬按鍵的onTouchEvent方法,和上述代碼非常相似,這樣就能做到點擊、長按和滑動的區分了。
5、滑動沖突
常見的滑動沖突如網易新聞,可以橫向滑動也可以縱向滑動,需要明確區分兩種滑動事件,并且將touch事件交由正確的view處理。
目前有兩種常見的滑動沖突處理方法:
- 外部攔截法,由父View根據條件進行攔截
- 內部攔截法,由子View設置父View的標志位,當子View不需要處理touch事件時將事件交由父view處理
5.1 外部攔截法
本節使用示例說明,示例中父View有三個子View,listView,橫向滑動時切換listview,縱向滑動時,listview響應用戶。
外部攔截法的思想,由父view根據情況攔截touch事件,本例中,則是在move事件下發時,如果橫向滑動距離超過縱向滑動距離,那么則由父view攔截,否則由子view攔截。
父View的onInterceptTouchEvent方法:
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:
mInterceptX = x;
mInterceptY = y;
intercepted = false;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
intercepted = true;
}
break;
case MotionEvent.ACTION_MOVE:
int deltax = Math.abs(mInterceptX - x);
int deltay = Math.abs(mInterceptY - y);
if (deltax > deltay) {
intercepted = true;
}else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
Util.log("intercepted = " + intercepted + " action = " + ev.getAction());
return intercepted;
}
本例中的橫向滑動事件處理具有代表性意義,滑動一屏,這種場景非常多見,可參考本例中代碼
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
mTracker.addMovement(event);
ViewConfiguration configuration = ViewConfiguration.get(getContext());
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX = x;
mLastY = y;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
if (mFirstTouch) {
mLastX = x;
mLastY = y;
mFirstTouch = false;
}
int deltax = x - mLastX;
scrollBy(-deltax, 0);
break;
case MotionEvent.ACTION_UP:
int scrollx = getScrollX();
mTracker.computeCurrentVelocity(1000, configuration.getScaledMaximumFlingVelocity());
float xV = mTracker.getXVelocity();
if (Math.abs(xV) > configuration.getScaledMinimumFlingVelocity()) {
mChildIndex = xV < 0 ? mChildIndex + 1 : mChildIndex - 1;
}else {
mChildIndex = (scrollx + mContentWidth/2)/mContentWidth;
}
mChildIndex = Math.min(getChildCount() - 1, Math.max(mChildIndex, 0));
smoothScrolly(mContentWidth * mChildIndex - scrollx);
mTracker.clear();
mFirstTouch = true;
break;
}
mLastX = x;
mLastY = y;
return true;
}
private void smoothScrolly(int dx){
mScroller.startScroll(getScrollX(), getScrollY(), dx, 0, 500);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
5.2、內部攔截法
內部攔截法,主要基于設置標志位的思想。requestDisallowInterceptTouchEvent方法,將給view設置標志位,view無法攔截touch事件了。
如果父view在down事件時,被調用此方法,父view無法再攔截touch事件,所有touch事件均會直接讓子view處理。子view如果發現對某touch事件不關心,再重新調用上述方法,關閉此標志位,父view將重新處理touch事件。
父view的onInterceptTouchEvent方法:
public boolean onInterceptTouchEvent(MotionEvent ev) {
Util.log("group2 onInterceptTouchEvent action = " + ev.getAction());
//父view在down事件時,不攔截,子view處理down事件時將父view設置標志位,禁止父view攔截touch事件
//父view其它事件均返回為true,這是為了時刻準備著,如果子view不需要處理此事件,則父view獲得機會,將攔截此事件
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
return true;
}
return false;
}else {
return true;
}
}
子view的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent ev) {
Util.log("ListViewEx dispatchTouchEvent action = " + ev.getAction());
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mHorizontalEx2.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltax = x - mLastX;
int deltay = y - mLastY;
//當橫向移動距離大于縱向移動距離時,給父view解禁,讓父view處理此touch事件
//綜合來說,就是當子view對這種touch事件不關心時,就扔給父view處理
if (Math.abs(deltax) > Math.abs(deltay)) {
mHorizontalEx2.requestDisallowInterceptTouchEvent(false);
}
break;
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(ev);
}
注,所有代碼均已上傳至本人的github