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