View的基礎(chǔ)知識(shí)
- 什么是View
View是Android中所有控件的基類,View是一種界面層的控件的一種抽象,它代表了一個(gè)控件,在Android設(shè)計(jì)中,ViewGroup也繼承了View,這就意味著View本身就可以是單個(gè)控件也可以是多個(gè)控件組成的一組控件,通過(guò)這種關(guān)系就形成了View樹(shù)的結(jié)構(gòu)。
- View的位置參數(shù)
view的位置主要由它的四個(gè)頂點(diǎn)來(lái)決定,分別對(duì)應(yīng)于View的四個(gè)屬性:top、left、right、bottom,其中top是左上角縱坐標(biāo),left是左上角橫坐標(biāo),right是右下角橫坐標(biāo),bottom是右下角縱坐標(biāo)
從Android 3.0開(kāi)始,view增加了x、y、translationX、translationY四個(gè)參數(shù),這幾個(gè)參數(shù)也是相對(duì)于父容器的坐標(biāo)。x和y是左上角的坐標(biāo),而translationX和translationY是view左上角相對(duì)于父容器的偏移量,默認(rèn)值都是0。
x = left + translationX
y = top + translationY
View在平移的過(guò)程中,top和left的值不會(huì)發(fā)生改變(表示原始左上角的位置信息),發(fā)生改變的是x、y、translationX和translationY。
- MotionEvent和TouchSlop
MotionEvent:
在手指觸摸屏幕后所產(chǎn)生的一系列事件中,典型的時(shí)間類型有:
1、ACTION_DOWN-手指剛接觸屏幕
2、ACTION_MOVE-手指在屏幕上移動(dòng)
3、ACTION_UP-手機(jī)從屏幕上松開(kāi)的一瞬間
正常情況下,一次手指觸摸屏幕的行為會(huì)觸發(fā)一系列點(diǎn)擊事件,考慮如下幾種情況:
1、點(diǎn)擊屏幕后離開(kāi)松開(kāi),事件序列為 DOWN -> UP
2、點(diǎn)擊屏幕滑動(dòng)一會(huì)再松開(kāi),事件序列為DOWN->MOVE->…->UP
通過(guò)MotionEvent對(duì)象我們可以得到點(diǎn)擊事件發(fā)生的x和y坐標(biāo),getX/getY返回的是相對(duì)于當(dāng)前View左上角的x和y坐標(biāo),getRawX和getRawY是相對(duì)于手機(jī)屏幕左上角的x和y坐標(biāo)。
TouchSlop:
TouchSlope是系統(tǒng)所能識(shí)別出的可以被認(rèn)為是滑動(dòng)的最小距離,獲取方式是ViewConfiguration.get(getContext()).getScaledTouchSlope()。
- VelocityTracker、GestureDetector和Scroller
1、 VelocityTracker:用于追蹤手指在滑動(dòng)過(guò)程中的速度,包括水平和垂直方向上的速度。
VelocityTracker的使用方式:
//初始化
VelocityTracker mVelocityTracker = VelocityTracker.obtain();
//在onTouchEvent方法中
mVelocityTracker.addMovement(event);
//獲取速度
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
//重置和回收
mVelocityTracker.clear(); //一般在MotionEvent.ACTION_UP的時(shí)候調(diào)用
mVelocityTracker.recycle(); //一般在onDetachedFromWindow中調(diào)用
速度的計(jì)算公式:速度 = (終點(diǎn)位置 - 起點(diǎn)位置) / 時(shí)間段
速度可能為負(fù)值,例如當(dāng)手指從屏幕右邊往左邊滑動(dòng)的時(shí)候。此外,速度是單位時(shí)間內(nèi)移動(dòng)的像素?cái)?shù),單位時(shí)間不一定是1秒鐘,可以使用方法computeCurrentVelocity(xxx)指定單位時(shí)間是多少,單位是ms。例如通過(guò)computeCurrentVelocity(1000)來(lái)獲取速度,手指在1s中滑動(dòng)了100個(gè)像素,那么速度是100,即100(像素/1000ms)。如果computeCurrentVelocity(100)來(lái)獲取速度,在100ms內(nèi)手指只是滑動(dòng)了10個(gè)像素,那么速度是10,即10(像素/100ms)。
當(dāng)不需要的時(shí)候,需要調(diào)用clear方法來(lái)重置并回收內(nèi)存
velocityTracker.clear();
velocityTracker.recycler();
2、GestureDetector
手勢(shì)檢測(cè),用于輔助檢測(cè)用戶的點(diǎn)擊、滑動(dòng)、長(zhǎng)按、雙擊等行為。
在日常開(kāi)發(fā)中,比較常用的有:onSingleTapUp(單擊)、onFling(快速滑動(dòng))、onScroll(拖動(dòng))、onLongPress(長(zhǎng)按)、onDoubleTap(雙擊),建議:如果只是監(jiān)聽(tīng)滑動(dòng)相關(guān)的事件在onTouchEvent中實(shí)現(xiàn);如果要監(jiān)聽(tīng)雙擊這種行為的話,那么就使用GestureDetector。
3、Scroller
彈性滑動(dòng)對(duì)象,用于實(shí)現(xiàn)View的彈性滑動(dòng)。Scroller本身無(wú)法讓View彈性滑動(dòng),它需要和View的computeScroll方法配合使用才能共同完成這個(gè)功能。
View的滑動(dòng)
通過(guò)三種方式可以實(shí)現(xiàn)View的滑動(dòng)
- 通過(guò)View本身提供的scrollTo/scrollBy方法來(lái)實(shí)現(xiàn)滑動(dòng)
- 動(dòng)畫(huà)給View施加平移效果來(lái)實(shí)現(xiàn)滑動(dòng)
- 通過(guò)改變View的LayoutParams使得View重新布局從而實(shí)現(xiàn)滑動(dòng)
1、使用scrollTo/scrollBy
scrollTo和scrollBy方法只能改變view內(nèi)容的位置而不能改變view在布局中的位置。 scrollBy是基于當(dāng)前位置的相對(duì)滑動(dòng),而scrollTo是基于所傳參數(shù)的絕對(duì)滑動(dòng)。通過(guò)View的getScrollX和getScrollY方法可以得到滑動(dòng)的距離。
2、使用動(dòng)畫(huà)
使用動(dòng)畫(huà)來(lái)移動(dòng)view主要是操作view的translationX和translationY屬性,既可以使用傳統(tǒng)的view動(dòng)畫(huà),也可以使用屬性動(dòng)畫(huà),使用后者需要考慮兼容性問(wèn)題,如果要兼容Android3.0一下版本系統(tǒng)的話推薦使用nineoldandroids。使用動(dòng)畫(huà)還存在一個(gè)交互問(wèn)題:在android3.0以前的系統(tǒng)上,view動(dòng)畫(huà)和屬性動(dòng)畫(huà),新位置均無(wú)法觸發(fā)點(diǎn)擊事件,同時(shí),老位置仍然可以觸發(fā)單擊事件。從3.0開(kāi)始,屬性動(dòng)畫(huà)的單擊事件觸發(fā)位置為移動(dòng)后的位置,view動(dòng)畫(huà)仍然在原位置。
3、改變布局參數(shù)
通過(guò)改變LayoutParams的方式去實(shí)現(xiàn)View的滑動(dòng)是一種靈活的方法。
4、各種滑動(dòng)方式的對(duì)比
- scrollTo/scrollBy:操作簡(jiǎn)單,適合對(duì)View內(nèi)容的滑動(dòng)
- 動(dòng)畫(huà):操作簡(jiǎn)單,主要適用于沒(méi)有交互的View和實(shí)現(xiàn)復(fù)雜的動(dòng)畫(huà)效果
- 改變布局參數(shù):操作稍微復(fù)雜,適用于有交互的View
動(dòng)畫(huà)兼容庫(kù)nineoldandroids中的ViewHelper類提供了很多的get/set方法來(lái)為屬性動(dòng)畫(huà)服務(wù),例如setTranslationX和setTranslationY方法,這些方法是沒(méi)有版本要求的。
彈性滑動(dòng)
1、使用Scroller
Scroller的工作原理:Scroller本身并不能實(shí)現(xiàn)view的滑動(dòng),它需要配合view的computeScroll方法才能完成彈性滑動(dòng)的效果,它不斷地讓view重繪,而每一次重繪距滑動(dòng)起始時(shí)間會(huì)有一個(gè)時(shí)間間隔,通過(guò)這個(gè)時(shí)間間隔Scroller就可以得出view的當(dāng)前的滑動(dòng)位置,知道了滑動(dòng)位置就可以通過(guò)scrollTo方法來(lái)完成view的滑動(dòng)。就這樣,view的每一次重繪都會(huì)導(dǎo)致view進(jìn)行小幅度的滑動(dòng),而多次的小幅度滑動(dòng)就組成了彈性滑動(dòng),這就是Scroller的工作原理。
2、通過(guò)動(dòng)畫(huà)
采用這種方法除了能完成彈性滑動(dòng)以外,還可以實(shí)現(xiàn)其他動(dòng)畫(huà)效果,我們完全可以在onAnimationUpdate方法中加上我們想要的其他操作。
3、使用延時(shí)策略
使用延時(shí)策略來(lái)實(shí)現(xiàn)彈性滑動(dòng),它的核心思想是通過(guò)發(fā)送一系列延時(shí)消息從而達(dá)到一種漸進(jìn)式的效果,具體來(lái)說(shuō)可以使用Handler的sendEmptyMessageDelayed(xxx)或view的postDelayed方法,也可以使用線程的sleep方法。
View的事件分發(fā)機(jī)制
1、事件分發(fā)機(jī)制的三個(gè)重要方法
public boolean dispatchTouchEvent(MotionEvent ev)
用來(lái)進(jìn)行事件的分發(fā)。如果事件能夠傳遞給當(dāng)前的View,那么此方法一定會(huì)被調(diào)用,返回結(jié)果受當(dāng)前View的onTouchEvent和下級(jí)View的dispatchTouchEvent方法的影響,表示是否消耗當(dāng)前事件。
public boolean onInterceptTouchEvent(MotionEvent event)
在上述方法內(nèi)部調(diào)用,用來(lái)判斷是否攔截某個(gè)事件,如果當(dāng)前View攔截了某個(gè)事件,那么在同一個(gè)事件序列當(dāng)中,此方法不會(huì)被再次調(diào)用,返回結(jié)果表示是否攔截當(dāng)前事件。
public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中調(diào)用,用來(lái)處理點(diǎn)擊事件,返回結(jié)果表示是否消耗當(dāng)前的事件,如果不消耗,則在同一個(gè)事件序列中,當(dāng)前View無(wú)法再次接受到事件。
這三個(gè)方法的關(guān)系可以用如下偽代碼表示:
public boolean dispatchTouchEvent(MotionEvent event)
{
boolean consume = false;
if(onInterceptTouchEvent(ev))
{
consume = onTouchEvent(ev);
}
else
{
consume = child.dispatchTouchEvent(ev);
}
}
我們可以大致了解點(diǎn)擊事件的傳遞規(guī)則:對(duì)于一個(gè)根ViewGroup來(lái)說(shuō),點(diǎn)擊事件產(chǎn)生后,首先會(huì)傳遞給它,這時(shí)它的dispatchTouchEvent會(huì)被調(diào)用,如果這個(gè)ViewGroup的onInterceptTouchEvent方法返回true就表示它要攔截當(dāng)前事件,接著事件就會(huì)交給這個(gè)ViewGroup處理,即它的onTouchEvent方法就會(huì)被調(diào)用;如果這個(gè)ViewGroup的onInterceptTouchEvent方法返回false就表示它不攔截當(dāng)前事件,這時(shí)當(dāng)前事件就會(huì)繼續(xù)傳遞給它的子元素,接著子元素的dispatchTouchEvent方法就會(huì)被調(diào)用,如此反復(fù)直到事件被最終處理。
OnTouchListener的優(yōu)先級(jí)比onTouchEvent要高
如果給一個(gè)view設(shè)置了OnTouchListener,那么OnTouchListener中的onTouch方法會(huì)被回調(diào)。這時(shí)事件如何處理還要看onTouch的返回值,如果返回false,那么當(dāng)前view的onTouchEvent方法會(huì)被調(diào)用;如果返回true,那么onTouchEvent方法將不會(huì)被調(diào)用。
在onTouchEvent方法中,如果當(dāng)前view設(shè)置了OnClickListener,那么它的onClick方法會(huì)被調(diào)用,所以O(shè)nClickListener的優(yōu)先級(jí)最低。
當(dāng)點(diǎn)擊一個(gè)事件產(chǎn)生后,它的傳遞過(guò)程遵循如順序,Activity->Window->View
如果一個(gè)View的onTouchEvent方法返回false,那么它的父容器的onTouchEvent方法將會(huì)被調(diào)用,依次類推,如果所有的元素都不處理這個(gè)事件,那么這個(gè)事件將會(huì)最終傳遞給Activity處理(調(diào)用Activity的onTouchEvent方法)
關(guān)于事件傳遞的機(jī)制,給出一些結(jié)論:
同一個(gè)事件序列是以down事件開(kāi)始,中間含有數(shù)量不定的move事件,最終以u(píng)p事件結(jié)束
正常情況下,一個(gè)事件序列只能被一個(gè)View攔截且消耗。一旦一個(gè)元素?cái)r截了某次事件,那么同一個(gè)事件序列內(nèi)的所有事件都會(huì)直接交給它處理,因此同一個(gè)事件序列中的事件不能分別由兩個(gè)View同時(shí)處理,但是通過(guò)特殊手段可以做到,比如一個(gè)View將本該自己處理的事件通過(guò)onTouchEvent強(qiáng)行傳遞給其他View處理
某個(gè)View一旦開(kāi)始處理事件,如果它不消耗ACTION_DOWN事件,那么同一事件序列的其他事情都不會(huì)再交給它來(lái)處理,并且事件將重新交給它的父容器去處理(調(diào)用父容器的onTouchEvent方法);如果它消耗ACTION_DOWN事件,但是不消耗其他類型事件,那么這個(gè)點(diǎn)擊事件會(huì)消失,父容器的onTouchEvent方法不會(huì)被調(diào)用,當(dāng)前view依然可以收到后續(xù)的事件,但是這些事件最后都會(huì)傳遞給Activity處理。
ViewGroup默認(rèn)不攔截任何事件。Android源碼中ViewGroup的onInterceptTouchEvent方法默認(rèn)返回false,View沒(méi)有onInterceptTouchEvent方法,一旦有點(diǎn)擊事件傳遞給它,那么它的onTouchEvent方法就會(huì)調(diào)用。
View的onTouchEvent默認(rèn)都會(huì)消耗事件(返回true),除非它是不可點(diǎn)擊的(clickable和longClickable同時(shí)為false)。View的longClickable屬性默認(rèn)都為false,clickable要分情況,比如Button的clickable屬性默認(rèn)為true,而TextView的clickable屬性默認(rèn)為false。
View的enable屬性不影響onTouchEvent的默認(rèn)返回值,哪怕一個(gè)View是disable狀態(tài)的,只要它的clickable或者longClickable有一個(gè)為true,那么它的onTouchEvent就返回true
事件傳遞過(guò)程總是先傳遞給父元素,然后再由父元素分發(fā)給子view,通過(guò)requestDisallowInterceptTouchEvent方法可以在子元素中干預(yù)父元素的事件分發(fā)過(guò)程,但是ACTION_DOWN事件除外,即當(dāng)面對(duì)ACTION_DOWN事件時(shí),ViewGroup總是會(huì)調(diào)用自己的onInterceptTouchEvent方法來(lái)詢問(wèn)自己是否要攔截事件。
View的滑動(dòng)沖突
1、常見(jiàn)的滑動(dòng)沖突場(chǎng)景
外部滑動(dòng)方向與內(nèi)部滑動(dòng)方向不一致,比如ViewPager中包含ListView
外部滑動(dòng)方向與內(nèi)部滑動(dòng)方向一致
上面兩種情況的嵌套
2、滑動(dòng)沖突的處理規(guī)則
可以根據(jù)滑動(dòng)距離和水平方向形成的夾角;或者根絕水平和豎直方向滑動(dòng)的距離差;或者兩個(gè)方向上的速度差等。
3、滑動(dòng)沖突的解決方式
- 外部攔截法
點(diǎn)擊事件都經(jīng)過(guò)父容器的攔截處理,如果父容器需要此事件就攔截,如果不需要此事件就不攔截,該方法需要重寫(xiě)父容器的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: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (父容器需要攔截當(dāng)前點(diǎn)擊事件的條件,例如: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;
}
- 內(nèi)部攔截法
父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要此事件就直接消耗掉,否則就由父容器進(jìn)行處理,這種方法和Android中的事件分發(fā)機(jī)制不一樣,需要配合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 (當(dāng)前view需要攔截當(dāng)前點(diǎn)擊事件的條件,例如: 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);
}