注:此篇筆記只記錄重難點(diǎn)李滴,對于基礎(chǔ)和詳細(xì)內(nèi)容請自行學(xué)習(xí)《Android開發(fā)藝術(shù)探索》
View的基礎(chǔ)知識
- 什么是View
View是Android中所有控件的基類阿逃,View是一種界面層的控件的一種抽象悍及,它代表了一個控件凌节,在Android設(shè)計(jì)中施掏,ViewGroup也繼承了View谎倔,這就意味著View本身就可以是單個控件也可以是多個控件組成的一組控件鸟辅,通過這種關(guān)系就形成了View樹的結(jié)構(gòu)氛什。
- View的位置參數(shù)
view的位置主要由它的四個頂點(diǎn)來決定,分別對應(yīng)于View的四個屬性:top剔桨、left屉更、right、bottom洒缀,其中top是左上角縱坐標(biāo)瑰谜,left是左上角橫坐標(biāo),right是右下角橫坐標(biāo)树绩,bottom是右下角縱坐標(biāo)
View的寬高和坐標(biāo)的關(guān)系:
width = right - left;
height = bottom - top;
如何得到這四個參數(shù):
Left = getLeft();
Right = getRight();
Top = getTop();
Bottom = getBottom();
從Android 3.0開始萨脑,view增加了x、y饺饭、translationX渤早、translationY四個參數(shù),這幾個參數(shù)也是相對于父容器的坐標(biāo)瘫俊。x和y是左上角的坐標(biāo)鹊杖,而translationX和translationY是view左上角相對于父容器的偏移量,默認(rèn)值都是0扛芽。
x = left + translationX
y = top + translationY
- MotionEvent和TouchSlop
MotionEvent:
在手指觸摸屏幕后所產(chǎn)生的一系列事件中骂蓖,典型的時間類型有:
1、ACTION_DOWN-手指剛接觸屏幕
2川尖、ACTION_MOVE-手指在屏幕上移動
3登下、ACTION_UP-手機(jī)從屏幕上松開的一瞬間
正常情況下,一次手指觸摸屏幕的行為會觸發(fā)一系列點(diǎn)擊事件,考慮如下幾種情況:
1被芳、點(diǎn)擊屏幕后離開松開缰贝,事件序列為 DOWN -> UP
2、點(diǎn)擊屏幕滑動一會再松開畔濒,事件序列為DOWN->MOVE->...->UP
通過MotionEvent對象我們可以得到點(diǎn)擊事件發(fā)生的x和y坐標(biāo)剩晴,getX/getY返回的是相對于當(dāng)前View左上角的x和y坐標(biāo),getRawX和getRawY是相對于手機(jī)屏幕左上角的x和y坐標(biāo)篓冲。
TouchSlop:
TouchSlope是系統(tǒng)所能識別出的可以被認(rèn)為是滑動的最小距離李破,獲取方式是ViewConfiguration.get(getContext()).getScaledTouchSlope()宠哄。
- VelocityTracker壹将、GestureDetector和Scroller
1、 VelocityTracker:用于追蹤手指在滑動過程中的速度毛嫉,包括水平和垂直方向上的速度诽俯。
VelocityTracker的使用方式:
//初始化
VelocityTracker mVelocityTracker = VelocityTracker.obtain();
//在onTouchEvent方法中
mVelocityTracker.addMovement(event);
//獲取速度
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
//重置和回收
mVelocityTracker.clear(); //一般在MotionEvent.ACTION_UP的時候調(diào)用
mVelocityTracker.recycle(); //一般在onDetachedFromWindow中調(diào)用
速度的計(jì)算公式:
速度 = (終點(diǎn)位置 - 起點(diǎn)位置) / 時間段
速度可能為負(fù)值,例如當(dāng)手指從屏幕右邊往左邊滑動的時候承粤。此外暴区,速度是單位時間內(nèi)移動的像素?cái)?shù),單位時間不一定是1秒鐘辛臊,可以使用方法computeCurrentVelocity(xxx)指定單位時間是多少仙粱,單位是ms。例如通過computeCurrentVelocity(1000)來獲取速度彻舰,手指在1s中滑動了100個像素伐割,那么速度是100,即100(像素/1000ms)刃唤。如果computeCurrentVelocity(100)來獲取速度隔心,在100ms內(nèi)手指只是滑動了10個像素,那么速度是10尚胞,即10(像素/100ms)硬霍。
當(dāng)不需要的時候,需要調(diào)用clear方法來重置并回收內(nèi)存
velocityTracker.clear();
velocityTracker.recycler();
?
2笼裳、GestureDetector
手勢檢測唯卖,用于輔助檢測用戶的點(diǎn)擊、滑動躬柬、長按拜轨、雙擊等行為。
在日常開發(fā)中楔脯,比較常用的有:onSingleTapUp(單擊)撩轰、onFling(快速滑動)、onScroll(拖動)、onLongPress(長按)堪嫂、onDoubleTap(雙擊)偎箫,建議:如果只是監(jiān)聽滑動相關(guān)的事件在onTouchEvent中實(shí)現(xiàn);如果要監(jiān)聽雙擊這種行為的話皆串,那么就使用GestureDetector淹办。
3、Scroller
彈性滑動對象恶复,用于實(shí)現(xiàn)View的彈性滑動怜森。Scroller本身無法讓View彈性滑動,它需要和View的computeScroll方法配合使用才能共同完成這個功能谤牡。
View的滑動
通過三種方式可以實(shí)現(xiàn)View的滑動
- 第一種是通過View本身提供的scrollTo/scrollBy方法來實(shí)現(xiàn)滑動
- 第二種是通過動畫給View施加平移效果來實(shí)現(xiàn)滑動
- 通過改變View的LayoutParams使得View重新布局從而實(shí)現(xiàn)滑動
1副硅、使用scrollTo/scrollBy
scrollTo和scrollBy方法只能改變view內(nèi)容的位置而不能改變view在布局中的位置。 scrollBy是基于當(dāng)前位置的相對滑動翅萤,而scrollTo是基于所傳參數(shù)的絕對滑動恐疲。通過View的getScrollX和getScrollY方法可以得到滑動的距離。
2套么、使用動畫
使用動畫來移動view主要是操作view的translationX和translationY屬性培己,既可以使用傳統(tǒng)的view動畫,也可以使用屬性動畫胚泌,使用后者需要考慮兼容性問題省咨,如果要兼容Android3.0一下版本系統(tǒng)的話推薦使用nineoldandroids。使用動畫還存在一個交互問題:在android3.0以前的系統(tǒng)上玷室,view動畫和屬性動畫零蓉,新位置均無法觸發(fā)點(diǎn)擊事件,同時阵苇,老位置仍然可以觸發(fā)單擊事件壁公。從3.0開始,屬性動畫的單擊事件觸發(fā)位置為移動后的位置绅项,view動畫仍然在原位置紊册。
3、改變布局參數(shù)
通過改變LayoutParams的方式去實(shí)現(xiàn)View的滑動是一種靈活的方法快耿。
4囊陡、各種滑動方式的對比
- scrollTo/scrollBy:操作簡單,適合對View內(nèi)容的滑動
- 動畫:操作簡單掀亥,主要適用于沒有交互的View和實(shí)現(xiàn)復(fù)雜的動畫效果
- 改變布局參數(shù):操作稍微復(fù)雜撞反,適用于有交互的View
動畫兼容庫nineoldandroids中的ViewHelper類提供了很多的get/set方法來為屬性動畫服務(wù),例如setTranslationX和setTranslationY方法搪花,這些方法是沒有版本要求的遏片。
彈性滑動
1挽牢、使用Scroller
Scroller的工作原理:Scroller本身并不能實(shí)現(xiàn)view的滑動归苍,它需要配合view的computeScroll方法才能完成彈性滑動的效果铣口,它不斷地讓view重繪镜粤,而每一次重繪距滑動起始時間會有一個時間間隔,通過這個時間間隔Scroller就可以得出view的當(dāng)前的滑動位置髓需,知道了滑動位置就可以通過scrollTo方法來完成view的滑動许师。就這樣,view的每一次重繪都會導(dǎo)致view進(jìn)行小幅度的滑動僚匆,而多次的小幅度滑動就組成了彈性滑動微渠,這就是Scroller的工作原理。
2咧擂、通過動畫
采用這種方法除了能完成彈性滑動以外逞盆,還可以實(shí)現(xiàn)其他動畫效果,我們完全可以在onAnimationUpdate方法中加上我們想要的其他操作屋确。
3纳击、使用延時策略
使用延時策略來實(shí)現(xiàn)彈性滑動续扔,它的核心思想是通過發(fā)送一系列延時消息從而達(dá)到一種漸進(jìn)式的效果攻臀,具體來說可以使用Handler的sendEmptyMessageDelayed(xxx)或view的postDelayed方法,也可以使用線程的sleep方法纱昧。
View的事件分發(fā)機(jī)制
1刨啸、事件分發(fā)機(jī)制的三個重要方法
- public boolean dispatchTouchEvent(MotionEvent ev)
用來進(jìn)行事件的分發(fā)。如果事件能夠傳遞給當(dāng)前的View识脆,那么此方法一定會被調(diào)用设联,返回結(jié)果受當(dāng)前View的onTouchEvent和下級View的dispatchTouchEvent方法的影響,表示是否消耗當(dāng)前事件灼捂。
- public boolean onInterceptTouchEvent(MotionEvent event)
在上述方法內(nèi)部調(diào)用离例,用來判斷是否攔截某個事件,如果當(dāng)前View攔截了某個事件悉稠,那么在同一個事件序列當(dāng)中宫蛆,此方法不會被再次調(diào)用,返回結(jié)果表示是否攔截當(dāng)前事件的猛。
- public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中調(diào)用耀盗,用來處理點(diǎn)擊事件,返回結(jié)果表示是否消耗當(dāng)前的事件卦尊,如果不消耗叛拷,則在同一個事件序列中,當(dāng)前View無法再次接受到事件岂却。
這三個方法的關(guān)系可以用如下偽代碼表示:
public boolean dispatchTouchEvent(MotionEvent event)
{
boolean consume = false;
if(onInterceptTouchEvent(ev))
{
consume = onTouchEvent(ev);
}
else
{
consume = child.dispatchTouchEvent(ev);
}
}
我們可以大致了解點(diǎn)擊事件的傳遞規(guī)則:對于一個根ViewGroup來說忿薇,點(diǎn)擊事件產(chǎn)生后裙椭,首先會傳遞給它,這時它的dispatchTouchEvent會被調(diào)用署浩,如果這個ViewGroup的onInterceptTouchEvent方法返回true骇陈,就表示它要攔截當(dāng)前事件,接著事件就會交給這個ViewGroup處理瑰抵,即它的onTouchEvent方法就會被調(diào)用你雌;如果這個ViewGroup的onInterceptTouchEvent方法返回false,就表示它不攔截當(dāng)前事件二汛,這時當(dāng)前事件就會繼續(xù)傳遞給它的子元素婿崭,接著子元素的dispatchTouchEvent方法就會被調(diào)用,如此反復(fù)直到事件被最終處理肴颊。
OnTouchListener的優(yōu)先級比onTouchEvent要高
如果給一個view設(shè)置了OnTouchListener氓栈,那么OnTouchListener中的onTouch方法會被回調(diào)。這時事件如何處理還要看onTouch的返回值婿着,如果onTouch返回false授瘦,那么當(dāng)前view的onTouchEvent方法會被調(diào)用;如果onTouch返回true竟宋,那么onTouchEvent方法將不會被調(diào)用提完。
在onTouchEvent方法中,如果當(dāng)前view設(shè)置了OnClickListener丘侠,那么它的onClick方法會被調(diào)用徒欣,所以O(shè)nClickListener的優(yōu)先級最低。
當(dāng)點(diǎn)擊一個事件產(chǎn)生后蜗字,它的傳遞過程遵循如順序打肝,Activity->Window->View。如果一個View的onTouchEvent方法返回false挪捕,那么它的父容器的onTouchEvent方法將會被調(diào)用粗梭,依次類推,如果所有的元素都不處理這個事件级零,那么這個事件將會最終傳遞給Activity處理(調(diào)用Activity的onTouchEvent方法)
關(guān)于事件傳遞的機(jī)制断医,給出一些結(jié)論:
- 同一個事件序列是以down事件開始,中間含有數(shù)量不定的move事件妄讯,最終以up事件結(jié)束孩锡。
- 正常情況下,一個事件序列只能被一個View攔截且消耗亥贸。一旦一個元素?cái)r截了某次事件躬窜,那么同一個事件序列內(nèi)的所有事件都會直接交給它處理,因此同一個事件序列中的事件不能分別由兩個View同時處理炕置,但是通過特殊手段可以做到荣挨,比如一個View將本該自己處理的事件通過onTouchEvent強(qiáng)行傳遞給其他View處理男韧。
- 某個View一旦開始處理事件,如果它不消耗ACTION_DOWN事件默垄,那么同一事件序列的其他事情都不會再交給它來處理此虑,并且事件將重新交給它的父容器去處理(調(diào)用父容器的onTouchEvent方法);如果它消耗ACTION_DOWN事件口锭,但是不消耗其他類型事件朦前,那么這個點(diǎn)擊事件會消失,父容器的onTouchEvent方法不會被調(diào)用鹃操,當(dāng)前view依然可以收到后續(xù)的事件韭寸,但是這些事件最后都會傳遞給Activity處理。
- ViewGroup默認(rèn)不攔截任何事件荆隘。Android源碼中ViewGroup的onInterceptTouchEvent方法默認(rèn)返回false恩伺,View沒有onInterceptTouchEvent方法,一旦有點(diǎn)擊事件傳遞給它椰拒,那么它的onTouchEvent方法就會調(diào)用晶渠。
- View的onTouchEvent默認(rèn)都會消耗事件(返回true),除非它是不可點(diǎn)擊的(clickable和longClickable同時為false)燃观。View的longClickable屬性默認(rèn)都為false褒脯,clickable要分情況,比如Button的clickable屬性默認(rèn)為true仪壮,而TextView的clickable屬性默認(rèn)為false憨颠。
- View的enable屬性不影響onTouchEvent的默認(rèn)返回值,哪怕一個View是disable狀態(tài)的积锅,只要它的clickable或者longClickable有一個為true,那么它的onTouchEvent就返回true养盗。
- 事件傳遞過程總是先傳遞給父元素缚陷,然后再由父元素分發(fā)給子view,通過requestDisallowInterceptTouchEvent方法可以在子元素中干預(yù)父元素的事件分發(fā)過程往核,但是ACTION_DOWN事件除外箫爷,即當(dāng)面對ACTION_DOWN事件時,ViewGroup總是會調(diào)用自己的onInterceptTouchEvent方法來詢問自己是否要攔截事件聂儒。
View的滑動沖突
1虎锚、常見的滑動沖突場景
- 外部滑動方向與內(nèi)部滑動方向不一致,比如ViewPager中包含ListView
- 外部滑動方向與內(nèi)部滑動方向一致
- 上面兩種情況的嵌套
2衩婚、滑動沖突的處理規(guī)則
可以根據(jù)滑動距離和水平方向形成的夾角窜护;或者根絕水平和豎直方向滑動的距離差;或者兩個方向上的速度差等非春。
3柱徙、滑動沖突的解決方式
- 外部攔截法
點(diǎn)擊事件都經(jīng)過父容器的攔截處理缓屠,如果父容器需要此事件就攔截,如果不需要此事件就不攔截护侮,該方法需要重寫父容器的onInterceptTouchEvent方法敌完,再內(nèi)部做相應(yīng)的攔截即可,偽代碼如下:
- 首先羊初,ACTION_DOWN這個事件滨溉,父容器必須返回false,即不攔截ACTION_DOWN事件,因?yàn)橐坏└溉萜鲾r截了ACTION_DOWN,那么后續(xù)的ACTION_MOVE/ACTION_UP都會直接交給父容器處理长赞;
- 其次业踏,ACTION_MOVE,根據(jù)需求來決定是否要攔截;
- 最后,ACTION_UP事件,這里必須要返回false,在這里沒有多大意義
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方法才能正常工作乐设。
- 父元素需要默認(rèn)攔截除ACTION_DOWN以外的事件,這樣子元素調(diào)用parent.requestDisallowInterceptTouchEvent(false)方法時讼庇,父元素才能繼續(xù)攔截需要的事件。
- ACTION_DOWN事件不受requestDisallowInterceptTouchEvent方法影響,所以一旦父元素?cái)r截ACTION_DOWN事件,那么所有元素都無法傳遞到子元素去近尚。
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);
}