我的“View的事件體系”知識點總結
文章總結自《android開發藝術探索》一書。
3.1 View基礎知識
1、View是Android中所有控制元件的基類,是一種介面層的控制元件的一種抽象,也代表了一種控制元件。
2、在Android中View呈現出樹的結構,ViewGroup繼承了View,這代表View本身可以是單個控制元件也可以是多個控制元件組成的控制元件組
3、View中的四個屬性:top、left、right、bottom,top是左上角縱座標,left是左上角橫座標,right是右下角橫座標,bottom是右下角縱座標;Android3.0開始增加了x、y、translationX、translationY
4、獲取屬性的方法:
- Left =getLeft(); 剩下幾個類比;
- getX/getY,返回的是控制元件或事件相對於當前View左上角的座標
- getRawX/getRawY,返回的是控制元件或事件相對於手機螢幕左上角的座標
5、 MotionEvent典型事件型別:
- ACTION_DOWN
- ACTION_MOVE
- ACTION_UP
6、 TouchSlop:滑動最小距離,即兩次滑動之間的距離小於這個值,系統預設不是滑動操作,獲取方式:ViewConfiguration.get(getContext()).getScaledTouchSlop()
7、 VelocityTracker,速度追蹤器,追蹤手指在滑動過程中的速度,使用方法:
//build
VelocityTracker vt = VelocityTracker.obtain();
VelocityTracker.addMovement(event);
//use
vt.computeCurrentVelocity(1000);
int Vx = (int) vt.getXVelocity();
int Vy = (int) vt.getYVelocity();
//recyle
vt.clear();
vt.recyle();
8、GestureDetector,手勢檢測器,輔助檢測單擊、滑動等事件,用法:
//build
GestureDetector mGestureDetector = new GestureDetector(this);
//implement Interface: OnGestureListener || OnDoubleTapListener
....
//code in Method: mView.onTouchEvent
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
9、Scroller,用於實現View的彈性滑動。View的srcollTo/scrollBy過程是瞬間的,Scroller本身無法滑動View,需要和View的computeScroll配合使用,用法:
Scroller mScroller = new Scroller(mContext);
private void smoothScrollTo(int destX,int destY){
int scrollX = getScrollX();
int delta = destX - scrollX;
//slide slowly to destX in 1000ms
mScroller.startScroll(scrollX,0,delta,0,1000);
invalidate();
}
@Override
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
startScroll方法只是做一些賦值計算的工作,實際上起作用的是下面的invalidate,該方法導致View重繪,View的draw方法中會去呼叫computeScroll方法,computeScroll去獲取Scroller當前的屬性,然後開始滑動,再通知重繪,如此反覆。
computeScrollOffset()方法是根據當前時間流逝的百分比來計算scrollX和scrollY改變的百分比並計算當前值,類似於屬性動畫中的插值器和估值器。返回值決定滑動是否結束。
3.2 View的滑動
三種方式實現View的滑動:
- View自身提供的scrollTo/scrollBy方法
- 動畫給View施加平移效果
- 改變View的LayoutParams使View重新佈局
1、scrollTo/scrollBy
scrollBy呼叫了scrollTo,前者是基於當前距離的相對滑動,後者是絕對滑動。其原始碼中的mScrollX總是等於View左邊緣和View內容左邊緣的水平距離,mScrollY上邊緣和內容上邊緣垂直距離,從左往右滑,mScrollX為負,從上往下滑,mScrollY為負,並且只能改變View內容的位置而不能改變View在佈局中的位置。
2、使用動畫
android3.0以上可以使用屬性動畫,基本沒缺點,3.0以下版本的View動畫也可以移動,但是不能改變物件的屬性,所以onClick等事件需要解決。
3、改變佈局引數
改變LayoutParams中的marginXXX引數的值或者在需要的地方放上空的View,需要時改變View的寬度高度用來“擠走”目標控制元件,例如:
MarginLayoutParams params = (MarginLayoutParams)mButton.getLayoutParams();
params.width += 100;
params.height += 100;
mButton.requestlayout();
4、三者比較
scrollTo/scrollBy唯一缺點:只能滑動內容,不能改變View本身。
使用動畫:3.0以上屬性動畫沒有明顯缺點,3.0以下View動畫不能改變View的屬性。動畫缺點:不適用於與使用者互動的控制元件。
改變佈局:操作複雜,沒有明顯缺點。
5、使用延時策略
傳送一系列的延時訊息,可以使用Handler或View的postDelayed方法,也可以使用執行緒的sleep方法。
3.4 View的事件分發機制
1、針對MotionEvent的事件分發,主要由三個方法完成:
- public boolean dispatchTouchEvent(MotionEvent ev),用來進行當前事件的分發,如果事件能傳遞給當前View,此方法一定會呼叫,返回結果受當前View的onTouchEvent和下級的dispatchTouchEvent方法影響,表示是否消耗該事件
- public boolean onInterceptTouchEvent(MotionEvent event),在上述方法內部呼叫,用來表示是否攔截該事件,如果View攔截了該事件,那麼同一事件序列的所有事件都交給該View處理,並且此方法不會再被呼叫
- public boolean onTouchEvent(MotionEvent event),在dispatchTouchEvent方法中呼叫,處理點選事件,返回結果表示是否消耗該事件。
關係虛擬碼:
public boolean dispatchTouchEvent(MotionEvent event){
boolean consume = false;
if(onInterceptTouchEvent){
consume = onTouchEvent(event);
}else{
consume = child.dispatchTouchEvent(event);
}
return consume;
}
2、點選事件的激發順序:onTouchListener—>onTouchEvent—>onClickListener
3、點選事件的傳遞順序: Activity—>Window—>View,頂層View拿到事件以後進行分發。分發時如果子View無法處理事件,會上拋事件,父View的onTouchEvent會被呼叫,如果所有元素都不處理這件事,就會拋給Activity,Activity的onTouchEvent會被呼叫。
4、同一事件序列:指的是從手指觸控式螢幕幕的那一刻,中間經歷了若干個move事件,到手指離開螢幕的那一刻,中間產生的所有事件。
5、正常情況下,同一事件序列的所有事件只能被一個View攔截並且消耗。
6、某個View一旦開始處理事件,如果onTouchEvent返回false,即不消耗事件,那麼同一事件序列中剩下的所有事件都不交給它處理,轉交給父View去處理。
7、ViewGroup預設不攔截任何事件,View沒有onInterceptTouchEvent方法,事件傳給它直接呼叫onTouchEvent。
8、View的onTouchEvent預設都會消耗事件,除非是不可點選的,View的enable屬性不影響onTouchEvent的預設返回值。
9.onClick事件發生的前提是當前View可點選,並且接收到了down和up事件。
10、可以通過requestDisallowInterceptTouchEvent方法在子元素中干預父元素的分發過程,但是ACTION_DOWN事件除外。
3.5 View的滑動衝突
1、常見的滑動衝突場景
- 外部滑動方向和內部滑動方向不一致,比如外層一個ScrollView,內層一個ListView
- 外部滑動方向和內部滑動方向一致,比如內外層同時能上下滑動或者左右滑動
- 上面兩種情況的巢狀
第一種的處理規則是:根據使用者不同的滑動方向,讓不同層的View攔截點選事件。具體來說根據滑動過程中兩個點的座標來判斷,比如說兩點座標的豎直距離差與水平距離差的大小比較,水平豎直方向的速度差等。
第二、三種的處理規則:根據業務邏輯,在View的攔截中加上相應的業務邏輯判斷是否攔截處理。
2、衝突的解決方式
外部攔截法
點選事件都先經過父容器的攔截處理,如果父容器需要就攔截,不需要就不攔截。需要重寫父容器的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:{
if(父容器需要當前點選事件){
intercepted = true;
}else{
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP:{
intercepted = false;
break;
}
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
ACTION_DOWN事件一定不能攔截,否則後續事件都交給父容器處理,如果父容器處理不好會上拋給上層父容器,不會傳遞到其子元素,另外ACTION_UP事件沒有多大意義,也要返回false。
內部攔截法
父容器不攔截任何事件,所有事件都傳遞給子元素,如果子元素需要此事件就消耗掉,否則就上拋給父容器進行處理。需要配合requestDisallowInterceptTouchEvent方法並重寫子元素的dispatchTouchEvent:
public boolean dispatchTouchEvent(MotionEvent event){
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:{
parent.requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE:{
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if(父類需要此類點選事件){
parent.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP:{
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
父元素預設攔截除了ACTION_DOWN以外的其他事件:
public boolean onInterceptTouchEvent(MotionEvent event){
int action = event.getAction();
if(action==MotionEvent.ACTION_DOWN){
return false;
}else{
return true;
}
}