博主參加了2014 CSDN博客之星評選,幫我投一票吧。 前言前段時(shí)間android L(android 5.0)出來了,界面上做了一些改動(dòng),主要是添加了若干動(dòng)畫和一些新的控件,相信大家對view的點(diǎn)擊效果-水波紋很有印象吧,點(diǎn)擊一個(gè)view,然后一個(gè)水波紋就會(huì)從點(diǎn)擊處擴(kuò)散開來,本文就來分析這種效果的實(shí)現(xiàn)。首先,先說下L上的實(shí)現(xiàn),這種波紋效果,L上提供了一種動(dòng)畫,叫做Reveal效果,其底層是通過拿到view的canvas然后不斷刷新view來完成的,這種效果需要view的支持,而在低版本上沒有view的支持,因此,Reveal效果沒法直接在低版本運(yùn)行。但是,我們了解其效果、其原理后,還是可以通過模擬的方式去實(shí)現(xiàn)這種效果,平心而論,寫出一個(gè)具有波紋效果的自定義view不難,或者說很簡單,但是,view的子類很多,如果要一一去實(shí)現(xiàn)button、edit等控件,這樣比較繁瑣,于是,我們想是否有更簡單的方式呢?其實(shí)是有的,我們可以寫一個(gè)自定義的layout,然后讓layout中所有可點(diǎn)擊的元素都具有波紋效果,這樣做,就大大簡化了整個(gè)過程。接下來本文就會(huì)分析這個(gè)layout的實(shí)現(xiàn),在此之前,我們先看下效果。
實(shí)現(xiàn)思想首先我們自定義一個(gè)layout,這里我們選取LinearLayout,至于原因,文章下面會(huì)進(jìn)行分析。當(dāng)用戶點(diǎn)擊一個(gè)可點(diǎn)擊的元素時(shí),比如button,我們需要得到用戶點(diǎn)擊的元素的信息,包含:用戶點(diǎn)擊了哪個(gè)元素、用戶點(diǎn)擊的那個(gè)元素的寬、高、位置信息等。得到了button的信息后,我就可以確定水波紋的范圍,然后通過layout進(jìn)行重繪去繪制水波紋,這樣水波紋效果就實(shí)現(xiàn)了,當(dāng)然,這只是大概步驟,中間還是有一些細(xì)節(jié)需要處理的。 layout的選取既然我們打算實(shí)現(xiàn)一個(gè)自定義layout,那我們要選取那個(gè)layout呢,LinearLayout、RelativeLayout、FrameLayout?我這里選用LinearLayout。為什么呢?也許有人會(huì)問,不應(yīng)該用RelativeLayout嗎?因?yàn)镽elativeLayout比較強(qiáng)大,可以實(shí)現(xiàn)復(fù)雜的布局,但LinearLayout和FrameLayout就不行。沒錯(cuò),RelativeLayout是強(qiáng)大,但是考慮到水波效果是通過頻繁刷新layout來實(shí)現(xiàn)的,由于頻繁重繪,因此,我們要考慮性能問題,RelativeLayout的性能是最差的(因?yàn)樽龅氖虑槎啵?,因?yàn)椋瑸榱诵阅?,我們選擇LinearLayout,至于FrameLayout,它功能太簡單了,不太適合使用。當(dāng)實(shí)現(xiàn)復(fù)雜布局的時(shí)候,我們可以在具有波紋效果的元素外部包裹LinearLayout,這樣重繪的時(shí)候不至于有過重的任務(wù)。 根據(jù)上面的分析,我們定義如下的layout: public class RevealLayout extends LinearLayout implements Runnable 實(shí)現(xiàn)過程實(shí)現(xiàn)過程主要是如下幾個(gè)問題的解決: 1. 如何得知用戶點(diǎn)擊了哪個(gè)元素 2. 如何取得被點(diǎn)擊元素的信息 3. 如何通過layout進(jìn)行重繪繪制水波紋 4. 如果延遲up事件的分發(fā) 下面一一進(jìn)行分析 如何得知用戶點(diǎn)擊了哪個(gè)元素這個(gè)問題好弄,為了得知用戶點(diǎn)擊了哪個(gè)元素(這個(gè)元素一般來說要是可點(diǎn)擊的,否則是無意義的),我們要提前攔截所有的點(diǎn)擊事件,于是,我們應(yīng)該重寫layout中的dispatchTouchEvent方法,注意,這里不推薦用onInterceptTouchEvent,因?yàn)閛nInterceptTouchEvent不是一直會(huì)被回調(diào)的,具體原因請參看我之前寫的view系統(tǒng)解析系列。然后當(dāng)用戶點(diǎn)擊的時(shí)候,會(huì)有一系列的down、move、up事件,我們要在down的時(shí)候來確定事件落在哪個(gè)元素上,down的元素就是用戶點(diǎn)擊的元素,當(dāng)然為了嚴(yán)謹(jǐn),我們還要判斷up的時(shí)候是否也落在同一個(gè)元素上面,因?yàn)?,系統(tǒng)click事件的判斷規(guī)則就是:down和up同時(shí)落在同一個(gè)可點(diǎn)擊的元素上。 @Override public boolean dispatchTouchEvent(MotionEvent event) { int x = (int) event.getRawX(); int y = (int) event.getRawY(); int action = event.getAction(); if (action == MotionEvent.ACTION_DOWN) { View touchTarget = getTouchTarget(this, x, y); if (touchTarget.isClickable() && touchTarget.isEnabled()) { mTouchTarget = touchTarget; initParametersForChild(event, touchTarget); postInvalidateDelayed(INVALIDATE_DURATION); } } else if (action == MotionEvent.ACTION_UP) { mIsPressed = false; postInvalidateDelayed(INVALIDATE_DURATION); mDispatchUpTouchEventRunnable.event = event; postDelayed(mDispatchUpTouchEventRunnable, 400); return true; } else if (action == MotionEvent.ACTION_CANCEL) { mIsPressed = false; postInvalidateDelayed(INVALIDATE_DURATION); } return super.dispatchTouchEvent(event); } 通過上述代碼,我們可以知道,當(dāng)down的時(shí)候,我們?nèi)〕鳇c(diǎn)擊事件的屏幕坐標(biāo),然后去遍歷view樹找到用戶所點(diǎn)擊的那個(gè)view,代碼如下,就是判斷事件的坐標(biāo)是否落在view的范圍內(nèi),這個(gè)不再多說了,比較好理解。需要注意的是,事件的坐標(biāo)我們不能用getX和getY,而要用getRawX和getRawY,二者的區(qū)別是:前者是相對于被點(diǎn)擊view的坐標(biāo),后者是相對于屏幕的坐標(biāo),而我們的目標(biāo)view具體位于layout的哪一層我們無法知道,所以,必須用屏幕的絕對坐標(biāo)來進(jìn)行計(jì)算。而有了事件的坐標(biāo),再根據(jù)view在屏幕中的絕對坐標(biāo),只要判斷事件的xy是否落在view的上下左右四個(gè)角之內(nèi),就可以知道事件是否落在view上,從而取出用戶所點(diǎn)擊的那個(gè)view。 private View getTouchTarget(View view, int x, int y) { View target = null; ArrayList<View> TouchableViews = view.getTouchables(); for (View child : TouchableViews) { if (isTouchPointInView(child, x, y)) { target = child; break; } } return target; } private boolean isTouchPointInView(View view, int x, int y) { int[] location = new int[2]; view.getLocationOnScreen(location); int left = location[0]; int top = location[1]; int right = left + view.getMeasuredWidth(); int bottom = top + view.getMeasuredHeight(); if (view.isClickable() && y >= top && y <= bottom && x >= left && x <= right) { return true; } return false; } 如何取得被點(diǎn)擊元素的信息這個(gè)比較簡單,被點(diǎn)擊元素的信息有:寬、高、left、top、right、bottom,獲取它們的代碼如下: int[] location = new int[2]; mTouchTarget.getLocationOnScreen(location); int left = location[0] - mLocationInScreen[0]; int top = location[1] - mLocationInScreen[1]; int right = left + mTouchTarget.getMeasuredWidth(); int bottom = top + mTouchTarget.getMeasuredHeight(); 說明:mTouchTarget指的是用戶點(diǎn)擊的那個(gè)view 如何通過layout進(jìn)行重繪繪制水波紋這個(gè)會(huì)水波紋比較簡單,只要用drawCircle繪制一個(gè)半透明的圓環(huán)即可,這里主要說下繪制時(shí)機(jī)。一般來說,我們會(huì)選擇在onDraw中去進(jìn)行繪制,這是沒錯(cuò)的,但是對于L中的效果不太適合,查看view的繪制過程,我們會(huì)明白,view的繪制大致遵循如下流程:先繪制背景,再繪制自己(onDraw),接著繪制子元素(dispatchDraw),最后繪制一些裝飾等比如滾動(dòng)條(onDrawScrollBars),因此,如果我們在onDraw中繪制波紋,那么由于子元素的繪制在onDraw之后,就會(huì)導(dǎo)致子元素蓋住我們所繪制的圓環(huán),這樣,圓環(huán)就有可能看不全了,因?yàn)?,把我繪制的時(shí)機(jī)很重要。根據(jù)view的繪制流程,我們選擇dispatchDraw比較合適,當(dāng)所有的子元素都繪制完成后,再進(jìn)行波紋的繪制。讀到這里,大家會(huì)更加明白,為什么我們要選擇LinearLayout以及為什么不建議view的嵌套層級太深,因?yàn)槿绻鹶iew本身比較重或者嵌套層級太深,就會(huì)導(dǎo)致dispatchDraw執(zhí)行的耗時(shí)增加,這樣水波的繪制就會(huì)收到些許影響。因此,性能的平滑在代碼中也很重要,也是需要考慮的。同時(shí),為了不讓繪制的圓環(huán)超出被點(diǎn)擊元素的范圍,我們需要對canvas進(jìn)行clip。為了有波紋效果,我們需要頻繁地進(jìn)行l(wèi)ayout重繪,并且在重繪的過程中改變圓環(huán)的半徑,這樣一個(gè)動(dòng)態(tài)的水波紋就出來了。仍然,我來性能的考慮,我們選擇用postInvalidateDelayed(long delayMilliseconds, int left, int top, int right, int bottom)來進(jìn)行view的部分重繪,因?yàn)?,其他區(qū)域是不需要重繪的,僅僅是被點(diǎn)擊的元素所在的區(qū)域需要重繪。為什么要采用Delayed這個(gè)方法,原因是我們不能一直進(jìn)行刷新,必須有一點(diǎn)點(diǎn)時(shí)間間隔,這樣做的好處是:避免view的重繪搶占過多時(shí)間片從而造成潛在的間接棧溢出,因?yàn)閕nvalidate會(huì)直接導(dǎo)致draw的調(diào)用。 具體代碼如下: protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); if (!mShouldDoAnimation || mTargetWidth <= 0 || mTouchTarget == null) { return; } if (mRevealRadius > mMinBetweenWidthAndHeight / 2) { mRevealRadius += mRevealRadiusGap * 4; } else { mRevealRadius += mRevealRadiusGap; } int[] location = new int[2]; mTouchTarget.getLocationOnScreen(location); int left = location[0] - mLocationInScreen[0]; int top = location[1] - mLocationInScreen[1]; int right = left + mTouchTarget.getMeasuredWidth(); int bottom = top + mTouchTarget.getMeasuredHeight(); canvas.save(); canvas.clipRect(left, top, right, bottom); canvas.drawCircle(mCenterX, mCenterY, mRevealRadius, mPaint); canvas.restore(); if (mRevealRadius <= mMaxRevealRadius) { postInvalidateDelayed(INVALIDATE_DURATION, left, top, right, bottom); } else if (!mIsPressed) { mShouldDoAnimation = false; postInvalidateDelayed(INVALIDATE_DURATION, left, top, right, bottom); } } 到此為止,這個(gè)layout我們已經(jīng)實(shí)現(xiàn)了,但是細(xì)心的你,一定會(huì)發(fā)現(xiàn),還有什么不妥的地方。比如,你可以給button加一個(gè)點(diǎn)擊事件,當(dāng)button被點(diǎn)擊的時(shí)候起一個(gè)activity,很快你就會(huì)發(fā)現(xiàn)問題所在了:水波還沒播完呢,activity就起來了,導(dǎo)致水波效果大打折扣,而仔細(xì)觀察android L的效果,我們發(fā)現(xiàn),L中總是要等到水波效果播放完畢才會(huì)進(jìn)行下一步的行為。所以,最后一個(gè)待解決的問題也就出來了,請看下面的分析 如何延遲up事件的分發(fā)針對上面所說的問題,如果我們能夠延遲up時(shí)間的分發(fā),比如延遲400ms,這樣水波就有足夠的時(shí)間去播放完畢,然后再分發(fā)up事件,這樣就可以解決問題。最開始,我的確是這樣做的,先看如下的代碼: else if (action == MotionEvent.ACTION_UP) { mIsPressed = false; postInvalidateDelayed(INVALIDATE_DURATION); mDispatchUpTouchEventRunnable.event = event; postDelayed(mDispatchUpTouchEventRunnable, 400); return true; } 可以發(fā)現(xiàn),當(dāng)up的時(shí)候,我并沒有直接走系統(tǒng)的分發(fā)流程,只是強(qiáng)行消耗點(diǎn)up事件然后再延遲分發(fā),請看代碼: private class DispatchUpTouchEventRunnable implements Runnable { public MotionEvent event; @Override public void run() { if (mTouchTarget == null || !mTouchTarget.isEnabled()) { return; } if (isTouchPointInView(mTouchTarget, (int)event.getRawX(), (int)event.getRawY())) { mTouchTarget.dispatchTouchEvent(event); } } }; 到此為止,上述幾個(gè)問題都已經(jīng)分析完畢了,我們就可以輕易地實(shí)現(xiàn)水波紋的點(diǎn)擊效果了。 源碼下載本文中的demo源碼暫時(shí)未開放到互聯(lián)網(wǎng)上,請加群 215680213 ,在群共享中下載源碼。 |
|