Android 4.0規定的有效可觸摸的UI元素標準是48dp,這是一個用戶手指能準確并且舒適觸摸的區域。
日常開發中,如果我們想擴大一個View的點擊區域,往往通過給View設置padding即可實現,但是對于某些特殊的情況,如圖
因為布局對齊的關系,這個SeekBar不能有paddingTop,而這時又需要在上方增加可響應區域,就只能用TouchDelegate了。
這里提供一篇Android Developer上介紹 TouchDelegate 的文檔,TouchDelegate讓父視圖能夠將子視圖的可輕觸區域擴展到子視圖的邊界之外。當子視圖必須較小,同時又應該具有較大的輕觸區域時,此方法很有用。
TouchDelegate的使用方法很簡單,考慮以下這種情形
我們想擴大View2的點擊區域至View1內部的Bounds區域,代碼如下:
view1.post(new Runnable() {
@Override
public void run() {
Rect bounds = new Rect();
// 獲取View2占據的矩形區域在其父View(也就是View1)中的相對坐標
view2.getHitRect(bounds);
// 計算擴展后的j矩形區域Bounds相對于View1的坐標,left、top、right、bottom分別為View2在各個方向上的擴展范圍
bounds.left -= left;
bounds.top -= top;
bounds.right += right;
bounds.bottom += bottom;
// 創建TouchDelegate,delegateView為View2
TouchDelegate touchDelegate = new TouchDelegate(bounds, view2);
// 為View1設置TouchDelegate,原因可以參考View.java中mTouchDelegate的注釋
// The delegate to handle touch events that are physically in this view but should be handled by another view.
view1.setTouchDelegate(touchDelegate);
}
});
使用TouchDelegate的擴展點擊區域的原理,可以查看View.java源碼(基于API 27),前面使用了下面的代碼為View1設置了TouchDelegate
/**
* Sets the TouchDelegate for this View.
*/
public void setTouchDelegate(TouchDelegate delegate) {
mTouchDelegate = delegate;
}
當我們點擊View2內部的區域,仍然會觸發View2的onClick();而當我們點擊View2外且在Bounds內(亦在View1內)的區域,根據Android Touch事件的分發原理,最終一定會觸發View1的onTouchEvent()方法
public boolean onTouchEvent(MotionEvent event) {
......
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
......
}
因為我們為View1設置了TouchDelegate,所以會進入TouchDelegate的onTouchEvent(),如果這個方法返回了ture,View1的onTouchEvent()也會返回true并到此結束,對外宣稱View1消費了這個事件,但實際上并不會觸發View1的onClick();而如果這個方法返回了false,則會繼續執行后面的邏輯。TouchDelegate的onTouchEvent() 源碼如下:
/**
* Will forward touch events to the delegate view if the event is within the bounds
* specified in the constructor.
*
* @param event The touch event to forward
* @return True if the event was forwarded to the delegate, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event) {
// 這里是觸摸點相對于View1的坐標
int x = (int)event.getX();
int y = (int)event.getY();
// 是否將event發送給View2
boolean sendToDelegate = false;
// 事件是否發生在Bounds內
boolean hit = true;
// 作為返回值,標識View1是否消費了event(實際上可能傳遞給View2消費了)
boolean handled = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Rect bounds = mBounds;
// Down事件發生在Bounds內
if (bounds.contains(x, y)) {
// 存儲Down事件的處理策略(傳遞給View2)供后續的Move和Up參考
mDelegateTargeted = true;
sendToDelegate = true;
}
// !!! 下面被注釋的代碼為作者添加的優化代碼 !!!
// 只有加上下面的代碼才能保證在點擊Rounds區域觸發View2的onClick()后
// 再點擊View1仍會觸發View1的onClick()
// else {
// mDelegateTargeted = false;
// sendToDelegate = false;
// }
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_MOVE:
// 這里首先參考前面Down事件的處理策略
sendToDelegate = mDelegateTargeted;
if (sendToDelegate) {
Rect slopBounds = mSlopBounds;
// 再檢查事件是否發生在SlopBounds內,它是由Bounds向外擴展TouchSlop形成的
if (!slopBounds.contains(x, y)) {
// Down事件在發生在Bounds內,但Move和Up事件未發生在SlopBounds內,說明在此期間手指滑出了指定區域
hit = false;
}
}
break;
case MotionEvent.ACTION_CANCEL:
sendToDelegate = mDelegateTargeted;
mDelegateTargeted = false;
break;
}
// 這里使用的是局部變量來決定事件的處理策略
if (sendToDelegate) {
final View delegateView = mDelegateView;
if (hit) {
// Offset event coordinates to be inside the target view
event.setLocation(delegateView.getWidth() / 2, delegateView.getHeight() / 2);
} else {
// Offset event coordinates to be outside the target view (in case it does something like tracking pressed state)
int slop = mSlop;
event.setLocation(-(slop * 2), -(slop * 2));
}
// 將事件分發給View2處理
handled = delegateView.dispatchTouchEvent(event);
}
return handled;
}
然而,直接使用API 27的TouchDelegate會存在一種 bad case:當點擊過一次擴展區域Bounds(不包括View2內的部分),View1的點擊失效。這是因為當點擊過一次Bounds區域,mDelegateTargeted 會被置為true;當下一次點擊View1時,由于sendToDelegate為false,Down事件會交由View1處理;而由于mDelegateTargeted仍為true,后續的Move和Up事件還會交由View2處理,閱讀View.java的onTouchEvent()源碼可知,這種情況下View1的performClick()不會被調用,也就不會觸發View1的onClick()
好在Google工程師已經發現了這個問題,并在API 28進行了修復:
最后,本文通過對API 27的TouchDelegate進行擴展,也給出一種使用TouchDelegate擴展點擊區域的優化方案,該方案具有以下兩個優點:
- 解決了先點擊一次擴展區域Bounds(不包括View2內的部分),View1點擊失效的問題。
- 內部改用絕對坐標,View2的擴展區域Bounds不再局限于其直接父類View1內,即被代理的View可以是代理View的任意祖先View,使用者只需提供四個方向需要擴展的大小。