Android開發藝術探討(View的事件體系)
MotionEvent 點擊事件
獲取點擊事件發生的x和y坐標
getX/getY返回的是相對于當前View左上角的x和y坐標
getRawX/getRawY返回的是相對于手機屏幕左上角的x和y坐標
TouchSlop
系統所能識別出的滑動最小距離 是一個常量
ViewConfiguration.get(getContext()).getScaledTouchSlop()獲取該常量
VelocityTracker
用于追蹤手指在滑動過程中的速度 是指一段時間內手指所劃過的像素數
GestureDetector
手勢檢測
Scroller
彈性滑動對象
Scroller本身不能實現彈性滑動,它需要和View的computeScroll方法配合使用
View的滑動
1.View原生方法scrollTo/scrollBy
當View左邊緣在View內容邊緣的右邊時,mScrollX為正值,反之為負值
當View上邊緣在View內容上邊緣下邊時,mScrollY為正值,反之為負值
從左往右滑動,mScrollX為負值,反之為正值
從上往下滑動,mScrollY為負值,反之為正值
可以通過getScrollX和getScrollY得到mScrollX和mScrollY
它只能滑動view的內容,并不能滑動view本身
2.動畫給View施加平移效果來實現滑動
主要操作View的translationX和translationY屬性
View動畫并不能改變View的位置參數
適用于沒有交互的View
3.改變布局參數LayoutParams使得View重新布局
彈性滑動
1.使用Scroller
startScroll方法下面的invalidate,會導致View重繪,在View的draw方法中會去調用computeScroll方法,實現這個computeScroll方法,View才能實現彈性滑動
2.動畫
3.使用延遲策略
通過發送一系列延時消息從而達到一種漸近式的效果,使用Handler或View的postDelayed,線程的sleep方法
View的事件分發機制
dispatchTouchEvent():如果事件能夠傳遞到當前view,那么此方法一定會被調用,返回結果受到當前view的onTouchEvent和下級view的dispatchTouchEvent方法的影響,表示是否消耗當前事件
onInterceptTouchEvent():在dispatchTouchEvent方法內部調用,判斷是否攔截某個事件,如果當前view攔截了某個事件,那么在同一個事件序列當中,此方法不會被再次調用
onTouchEvent():在dispatchTouchEvent方法中調用,用來處理點擊事件,返回結果表示是否消耗當前事件,如果不消耗,則在同一個事件序列中,當前view無法再次接收到事件。
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
對于一個根ViewGroup來說,點擊事件產生后,首先會傳遞給它,這時它的dispatchTouchEvent就會被調用,如果這個ViewGroup的onInterceptTouchEvent方法返回true就表示它要攔截當前事件,這個事件就會交給ViewGroup處理,即它的onTouchEvent方法會被調用,如果這個ViewGroup的onInterceptTouchEvent方法返回false就表示它不攔截當前事件,這時事件就會繼續傳遞給它的子元素,接著子元素的dispatchTouchEvent方法就會被調用,如此反復直到事件被最終處理
當一個View需要處理事件時,如果它設置了OnTouchListener,那么OnTouchListener中的onTouch方法會被回調,如果onTouch的返回值為false,則當前view的onTouchEvent方法會被調用,如果返回true,那么onTouchEvent方法將不會被調用。View的OnTouchListener優先級比onTouchEvent高
點擊事件的傳遞順序 : Activity-》Window-》View
如果一個View的onTouchEvent返回false,那么它的父容器的onTouchEvent將會被調用,以此類推,如果所有的元素都不處理這個事件,那么這個事件將會最終傳遞給Activity處理,即Activity的onTouchEvent方法會被調用
觸摸事件的傳遞順序
觸摸事件的傳遞順序是由Activity到ViewGroup,再由ViewGroup遞歸傳遞給它的子View。
ViewGroup通過onInterceptTouchEvent方法對事件進行攔截,如果該方法返回true,則事件不會繼續傳遞給子View,如果返回false或者super.onInterceptTouchEvent,則事件會繼續傳遞給子View
在子View中對事件進行消費后,ViewGroup將接收不到任何事件
1.Activity對點擊事件的分發過程
事件最先傳遞給當前activity,由activity的dispatchTouchEvent來進行事件派發,具體的工作由activity內部的window來完成,window將事件傳遞給decor view
window是個抽象類,實現類是PhoneWindow,PhoneWindow將事件直接傳遞給了DecorView,DecorView繼承了FrameLayout且是父View(一般是一個ViewGroup)
2.頂級View對點擊事件的分發過程
ViewGroup判斷是否攔截當前事件:
事件類型為ACTION_DOWN 或者
mFirstTouchTarget != null 即ViewGroup不攔截事件并將事件交由子元素處理
(當面對ACTION_DOWN事件時,ViewGroup總是會調用自己的onInterceptTouchEvent方法來詢問自己是否要攔截事件,對FLAG_DISALLOW_INTERCEPT進行重置,子View調用requestDisallowInterceptTouchEvent方法并不會影響ViewGroup對ACTION_DOWN
事件的處理)
當ViewGroup決定攔截事件后,那么后續的點擊事件將會默認交給它處理并且不會調用它的onInterceptTouchEvent方法,FLAG_DISALLOW_INTERCEPT這個標志的作用是讓ViewGroup不再攔截事件
||
ViewGroup不攔截事件,事件會向下分發由子View進行處理
判斷子元素是否能夠接收點擊事件?
子元素是否在播放動畫和點擊事件是否落在子元素的區域內
如果某個子元素滿足這兩個條件,那么事件就會傳遞給它來處理
View的滑動沖突
界面中只要內外兩層都能滑動,這個時候就會產生滑動沖突
場景一:外部滑動方向和內部滑動方向不一致
例如:ViewPager和Fragment,Fragment中是一個Listview
當用戶左右滑動的時候,需要讓外部的View攔截點擊事件,當用戶上下滑動的時候,需要讓內部View攔截點擊事件
場景二:外部滑動方向和內部滑動方向一致
當處于某種狀態是需要外部View響應用戶的滑動,而處于另外一種狀態時則需要內部View來響應View的滑動
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:{
if(父容器需要當前的點擊事件){
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP:{
intercepted = false;
break;
}
default:break;
}
mLastXIntercepted = x;
mLastYIntercepted = y;
return intercepted;
}
2.內部攔截法
父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要此事件就直接消耗,否則就交由父容器進行處理,需要配合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;
}
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
子元素調用parent.requestDisallowInterceptTouchEvent(false)方法時,父容器才能繼續攔截所需的事件
父容器不能攔截ACTION_DOWN事件,是因為ACTION_DOWN事件并不受FLAG_DISALLOW_INTERCEPT這個標記位的控制,父容器一旦攔截ACTION_DOWN事件,那么所有的事件都無法傳遞到子元素中去
父元素所做修改:
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}