##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_UP`、`ACTION_DOWN`、`ACTION_MOVE`等。正常情況下,一次手指觸屏會觸發一系列點擊事件,主要有下面兩種典型情況:
1.點擊屏幕后離開,事件序列是`ACTION_DOWN`?->?`ACTION_UP`;
2.點擊屏幕后滑動一會再離開,事件序列是`ACTION_DOWN`?->?`ACTION_MOVE`?->?`ACTION_MOVE`?-> … ->?`ACTION_UP`;
通過MotionEvent可以得到點擊事件發生的x和y坐標,其中`getX`和`getY`是相對于當前view左上角的x和y坐標,`getRawX`和`getRawY`是相對于手機屏幕左上角的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分析](http://hujiaweibujidao.github.io/blog/2015/11/26/Android-Heros-Reading-Notes-2/)
## 3.2 View的滑動
(1)常見的實現view的滑動的方式有三種:
第一種是通過view本身提供的scrollTo和scrollBy方法:操作簡單,適合對view內容的滑動;
第二種是通過動畫給view施加平移效果來實現滑動:操作簡單,適用于沒有交互的view和實現復雜的動畫效果;
第三種是通過改變view的LayoutParams使得view重新布局從而實現滑動:操作稍微復雜,適用于有交互的view。
以上三種方法的詳情可以參考閱讀[《Android群英傳》讀書筆記 (2)](http://hujiaweibujidao.github.io/blog/2015/11/26/Android-Heros-Reading-Notes-2/)中的內容,此處不再細述。
(2)scrollTo和scrollBy方法只能改變view內容的位置而不能改變view在布局中的位置。?scrollBy是基于當前位置的相對滑動,而scrollTo是基于所傳參數的絕對滑動。通過View的`getScrollX`和`getScrollY`方法可以得到滑動的距離。
(3)使用動畫來移動view主要是操作view的translationX和translationY屬性,既可以使用傳統的view動畫,也可以使用屬性動畫,使用后者需要考慮兼容性問題,如果要兼容Android 3.0以下版本系統的話推薦使用[nineoldandroids](http://nineoldandroids.com/)。
使用動畫還存在一個交互問題:在android3.0以前的系統上,view動畫和屬性動畫,新位置均無法觸發點擊事件,同時,老位置仍然可以觸發單擊事件。從3.0開始,屬性動畫的單擊事件觸發位置為移動后的位置,view動畫仍然在原位置。
(4)動畫兼容庫nineoldandroids中的`ViewHelper`類提供了很多的get/set方法來為屬性動畫服務,例如`setTranslationX`和`setTranslationY`方法,這些方法是沒有版本要求的。
## 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),除非它是不可點擊的(`clickable`和`longClickable`都為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);
}
~~~
書中對這兩種攔截法寫了兩個例子,感興趣閱讀源碼看下,[外部攔截法使用示例鏈接](https://github.com/singwhatiwanna/android-art-res/blob/master/Chapter_3/src/com/ryg/chapter_3/ui/HorizontalScrollViewEx.java)和[內部攔截法使用示例鏈接](https://github.com/singwhatiwanna/android-art-res/blob/master/Chapter_3/src/com/ryg/chapter_3/ui/ListViewEx.java)。
- 前言
- 讀書筆記(1)第1章 Activity的生命周期和啟動模式
- 讀書筆記(2)第2章 IPC機制
- 讀書筆記(3)第3章 View的事件體系
- 讀書筆記(4)第4章 View的工作原理
- 讀書筆記(5)第5章 理解RemoteViews
- 讀書筆記(6)第6章 Android的Drawable
- 讀書筆記(7)第7章 Android動畫深入分析
- 讀書筆記(8)第8章 理解Window和WindowManager
- 讀書筆記(9)第9章 四大組件的工作過程
- 讀書筆記(10)第10章 Android的消息機制
- 讀書筆記(11)第11章 Android的線程和線程池
- 讀書筆記(12)第12章 Bitmap的加載和Cache
- 讀書筆記(13)第13章 綜合技術