Android開發藝術探索 第三章

《Android開發藝術探索》讀書筆記 (3) 第3章 View的事件體系

本節和《Android羣英傳》中的第五章Scroll分析有關係,建議先閱讀該章的總結

第3章 View的事件體系

3.1 View基本知識

(1)view的層次結構:ViewGroup也是View;
(2)view的位置參數:top、left、right、bottom,分別對應View的左上角和右下角相對於父容器的橫縱座標值。
從Android 3.0開始,view增加了x、y、translationX、translationY四個參數,這幾個參數也是相對於父容器的座標。x和y是左上角的座標,而translationX和translationY是view左上角相對於父容器的偏移量,默認值都是0。
x = left + translationX
y = top + translationY
(3)MotionEvent是指手指接觸屏幕後所產生的一系列事件,主要有ACTION_UPACTION_DOWNACTION_MOVE等。正常情況下,一次手指觸屏會觸發一系列點擊事件,主要有下面兩種典型情況:
1.點擊屏幕後離開,事件序列是ACTION_DOWN -> ACTION_UP
2.點擊屏幕後滑動一會再離開,事件序列是ACTION_DOWN -> ACTION_MOVE -> ACTION_MOVE -> … -> ACTION_UP
通過MotionEvent可以得到點擊事件發生的x和y座標,其中getXgetY是相對於當前view左上角的x和y座標,getRawXgetRawY是相對於手機屏幕左上角的x和y座標。
(4)TouchSlope是系統所能識別出的可以被認爲是滑動的最小距離,獲取方式是ViewConfiguration.get(getContext()).getScaledTouchSlope()
(5)VelocityTracker用於追蹤手指在滑動過程中的速度,包括水平和垂直方向上的速度。
速度計算公式: 速度 = (終點位置 - 起點位置) / 時間段
速度可能爲負值,例如當手指從屏幕右邊往左邊滑動的時候。此外,速度是單位時間內移動的像素數,單位時間不一定是1秒鐘,可以使用方法computeCurrentVelocity(xxx)指定單位時間是多少,單位是ms。例如通過computeCurrentVelocity(1000)來獲取速度,手指在1s中滑動了100個像素,那麼速度是100,即100(像素/1000ms)。如果computeCurrentVelocity(100)來獲取速度,在100ms內手指只是滑動了10個像素,那麼速度是10,即10(像素/100ms)。

VelocityTracker的使用方式:

//初始化
VelocityTracker mVelocityTracker = VelocityTracker.obtain();
//在onTouchEvent方法中
mVelocityTracker.addMovement(event);
//獲取速度
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
//重置和回收
mVelocityTracker.clear(); //一般在MotionEvent.ACTION_UP的時候調用
mVelocityTracker.recycle(); //一般在onDetachedFromWindow中調用

(6)GestureDetector用於輔助檢測用戶的單擊、滑動、長按、雙擊等行爲。GestureDetector的使用比較簡單,主要也是輔助檢測常見的觸屏事件。作者建議:如果只是監聽滑動相關的事件在onTouchEvent中實現;如果要監聽雙擊這種行爲的話,那麼就使用GestureDetector。
(7)Scroller分析:詳細內容可以參見《Android羣英傳》讀書筆記 (2) 第五章 Scroll分析

3.2 View的滑動

(1)常見的實現view的滑動的方式有三種:
第一種是通過view本身提供的scrollTo和scrollBy方法:操作簡單,適合對view內容的滑動;
第二種是通過動畫給view施加平移效果來實現滑動:操作簡單,適用於沒有交互的view和實現複雜的動畫效果;
第三種是通過改變view的LayoutParams使得view重新佈局從而實現滑動:操作稍微複雜,適用於有交互的view。
以上三種方法的詳情可以參考閱讀《Android羣英傳》讀書筆記 (2)中的內容,此處不再細述。
(2)scrollTo和scrollBy方法只能改變view內容的位置而不能改變view在佈局中的位置。 scrollBy是基於當前位置的相對滑動,而scrollTo是基於所傳參數的絕對滑動。通過View的getScrollXgetScrollY方法可以得到滑動的距離。
(3)使用動畫來移動view主要是操作view的translationX和translationY屬性,既可以使用傳統的view動畫,也可以使用屬性動畫,使用後者需要考慮兼容性問題,如果要兼容Android 3.0以下版本系統的話推薦使用nineoldandroids
使用動畫還存在一個交互問題:在android3.0以前的系統上,view動畫和屬性動畫,新位置均無法觸發點擊事件,同時,老位置仍然可以觸發單擊事件。從3.0開始,屬性動畫的單擊事件觸發位置爲移動後的位置,view動畫仍然在原位置。
(4)動畫兼容庫nineoldandroids中的ViewHelper類提供了很多的get/set方法來爲屬性動畫服務,例如setTranslationXsetTranslationY方法,這些方法是沒有版本要求的。

3.3 彈性滑動

(1)Scroller的工作原理:Scroller本身並不能實現view的滑動,它需要配合view的computeScroll方法才能完成彈性滑動的效果,它不斷地讓view重繪,而每一次重繪距滑動起始時間會有一個時間間隔,通過這個時間間隔Scroller就可以得出view的當前的滑動位置,知道了滑動位置就可以通過scrollTo方法來完成view的滑動。就這樣,view的每一次重繪都會導致view進行小幅度的滑動,而多次的小幅度滑動就組成了彈性滑動,這就是Scroller的工作原理。
(2)使用延時策略來實現彈性滑動,它的核心思想是通過發送一系列延時消息從而達到一種漸進式的效果,具體來說可以使用Handler的sendEmptyMessageDelayed(xxx)或view的postDelayed方法,也可以使用線程的sleep方法。

3.4 view的事件分發機制

(1)事件分發過程的三個重要方法
public boolean dispatchTouchEvent(MotionEvent ev)
用來進行事件的分發。如果事件能夠傳遞給當前view,那麼此方法一定會被調用,返回結果受當前view的onTouchEvent和下級view的dispatchTouchEvent方法的影響,表示是否消耗當前事件。

public boolean onInterceptTouchEvent(MotionEvent event)
dispatchTouchEvent方法內部調用,用來判斷是否攔截某個事件,如果當前view攔截了某個事件,那麼在同一個事件序列當中,此方法不會再被調用,返回結果表示是否攔截當前事件。
若返回值爲True事件會傳遞到自己的onTouchEvent();
若返回值爲False傳遞到子view的dispatchTouchEvent()。

public boolean onTouchEvent(MotionEvent event)
dispatchTouchEvent方法內部調用,用來處理點擊事件,返回結果表示是否消耗當前事件,如果不消耗,則在同一個事件序列中,當前view無法再次接收到事件。
若返回值爲True,事件由自己處理,後續事件序列讓其處理;
若返回值爲False,自己不消耗事件,向上返回讓其他的父容器的onTouchEvent接受處理。

三個方法的關係可以用下面的僞代碼表示:

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

(2)OnTouchListener的優先級比onTouchEvent要高
如果給一個view設置了OnTouchListener,那麼OnTouchListener中的onTouch方法會被回調。這時事件如何處理還要看onTouch的返回值,如果返回false,那麼當前view的onTouchEvent方法會被調用;如果返回true,那麼onTouchEvent方法將不會被調用。
在onTouchEvent方法中,如果當前view設置了OnClickListener,那麼它的onClick方法會被調用,所以OnClickListener的優先級最低。
(3)當一個點擊事件發生之後,傳遞過程遵循如下順序:Activity -> Window -> View。
如果一個view的onTouchEvent方法返回false,那麼它的父容器的onTouchEvent方法將會被調用,依此類推,如果所有的元素都不處理這個事件,那麼這個事件將會最終傳遞給Activity處理(調用Activity的onTouchEvent方法)。
(4)正常情況下,一個事件序列只能被一個view攔截並消耗,因爲一旦某個元素攔截了某個事件,那麼同一個事件序列內的所有事件都會直接交給它處理,並且該元素的onInterceptTouchEvent方法不會再被調用了。
(5)某個view一旦開始處理事件,如果它不消耗ACTION_DOWN事件,那麼同一事件序列的其他事件都不會再交給它來處理,並且事件將重新交給它的父容器去處理(調用父容器的onTouchEvent方法);如果它消耗ACTION_DOWN事件,但是不消耗其他類型事件,那麼這個點擊事件會消失,父容器的onTouchEvent方法不會被調用,當前view依然可以收到後續的事件,但是這些事件最後都會傳遞給Activity處理。
(6)ViewGroup默認不攔截任何事件,因爲它的onInterceptTouchEvent方法默認返回false。view沒有onInterceptTouchEvent方法,一旦有點擊事件傳遞給它,那麼它的onTouchEvent方法就會被調用。
(7)View的onTouchEvent默認都會消耗事件(返回true),除非它是不可點擊的(clickablelongClickable都爲false)。view的longClickable默認是false的,clickable則不一定,Button默認是true,而TextView默認是false。
(8)View的enable屬性不影響onTouchEvent的默認返回值。哪怕一個view是disable狀態,只要它的clickable或者longClickable有一個是true,那麼它的onTouchEvent就會返回true。
(9)事件傳遞過程總是先傳遞給父元素,然後再由父元素分發給子view,通過requestDisallowInterceptTouchEvent方法可以在子元素中干預父元素的事件分發過程,但是ACTION_DOWN事件除外,即當面對ACTION_DOWN事件時,ViewGroup總是會調用自己的onInterceptTouchEvent方法來詢問自己是否要攔截事件。
ViewGroup的dispatchTouchEvent方法中有一個標誌位FLAG_DISALLOW_INTERCEPT,這個標誌位就是通過子view調用requestDisallowInterceptTouchEvent方法來設置的,一旦設置爲true,那麼ViewGroup不會攔截該事件。
(10)以上結論均可以在書中的源碼解析部分得到解釋。Window的實現類爲PhoneWindow,獲取Activity的contentView的方法

((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0);

3.5 view的滑動衝突

(1)常見的滑動衝突的場景:
1.外部滑動方向和內部滑動方向不一致,例如viewpager中包含listview;
2.外部滑動方向和內部滑動方向一致,例如viewpager的單頁中存在可以滑動的bannerview;
3.上面兩種情況的嵌套,例如viewpager的單個頁面中包含了bannerview和listview。
(2)滑動衝突處理規則
可以根據滑動距離和水平方向形成的夾角;或者根絕水平和豎直方向滑動的距離差;或者兩個方向上的速度差等
(3)解決方式
1.外部攔截法:點擊事件都先經過父容器的攔截處理,如果父容器需要此事件就攔截,如果不需要就不攔截。該方法需要重寫父容器的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: {
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;
}

2.內部攔截法:父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要此事件就直接消耗掉,否則就交給父容器來處理。這種方法和Android中的事件分發機制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。

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 = y - mLastY;
if (當前view需要攔截當前點擊事件的條件,例如:Math.abs(deltaX) > Math.abs(deltaY)) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}

mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}

書中對這兩種攔截法寫了兩個例子,感興趣閱讀源碼看下,外部攔截法使用示例鏈接內部攔截法使用示例鏈接

OK,本章結束,謝謝閱讀。


發佈了60 篇原創文章 · 獲贊 6 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章