Android L中水波紋點擊效果的實現

http://blog.csdn.net/singwhatiwanna/article/details/42614953
前言

前段時間android L(android 5.0)出來了,界面上做了一些改動,主要是添加了若干動畫和一些新的控件,相信大家對view的點擊效果-水波紋很有印象吧,點擊一個view,然後一個水波紋就會從點擊處擴散開來,本文就來分析這種效果的實現。首先,先說下L上的實現,這種波紋效果,L上提供了一種動畫,叫做Reveal效果,其底層是通過拿到view的canvas然後不斷刷新view來完成的,這種效果需要view的支持,而在低版本上沒有view的支持,因此,Reveal效果沒法直接在低版本運行。但是,我們瞭解其效果、其原理後,還是可以通過模擬的方式去實現這種效果,平心而論,寫出一個具有波紋效果的自定義view不難,或者說很簡單,但是,view的子類很多,如果要一一去實現button、edit等控件,這樣比較繁瑣,於是,我們想是否有更簡單的方式呢?其實是有的,我們可以寫一個自定義的layout,然後讓layout中所有可點擊的元素都具有波紋效果,這樣做,就大大簡化了整個過程。接下來本文就會分析這個layout的實現,在此之前,我們先看下效果。


實現思想

首先我們自定義一個layout,這裏我們選取LinearLayout,至於原因,文章下面會進行分析。當用戶點擊一個可點擊的元素時,比如button,我們需要得到用戶點擊的元素的信息,包含:用戶點擊了哪個元素、用戶點擊的那個元素的寬、高、位置信息等。得到了button的信息後,我就可以確定水波紋的範圍,然後通過layout進行重繪去繪製水波紋,這樣水波紋效果就實現了,當然,這只是大概步驟,中間還是有一些細節需要處理的。

layout的選取

既然我們打算實現一個自定義layout,那我們要選取那個layout呢,LinearLayout、RelativeLayout、FrameLayout?我這裏選用LinearLayout。爲什麼呢?也許有人會問,不應該用RelativeLayout嗎?因爲RelativeLayout比較強大,可以實現複雜的佈局,但LinearLayout和FrameLayout就不行。沒錯,RelativeLayout是強大,但是考慮到水波效果是通過頻繁刷新layout來實現的,由於頻繁重繪,因此,我們要考慮性能問題,RelativeLayout的性能是最差的(因爲做的事情多),因爲,爲了性能,我們選擇LinearLayout,至於FrameLayout,它功能太簡單了,不太適合使用。當實現複雜佈局的時候,我們可以在具有波紋效果的元素外部包裹LinearLayout,這樣重繪的時候不至於有過重的任務。

根據上面的分析,我們定義如下的layout:

public class RevealLayout extends LinearLayout implements Runnable

實現過程

實現過程主要是如下幾個問題的解決:

1. 如何得知用戶點擊了哪個元素

2. 如何取得被點擊元素的信息

3. 如何通過layout進行重繪繪製水波紋

4. 如果延遲up事件的分發

下面一一進行分析

如何得知用戶點擊了哪個元素

這個問題好弄,爲了得知用戶點擊了哪個元素(這個元素一般來說要是可點擊的,否則是無意義的),我們要提前攔截所有的點擊事件,於是,我們應該重寫layout中的dispatchTouchEvent方法,注意,這裏不推薦用onInterceptTouchEvent,因爲onInterceptTouchEvent不是一直會被回調的,具體原因請參看我之前寫的view系統解析系列。然後當用戶點擊的時候,會有一系列的down、move、up事件,我們要在down的時候來確定事件落在哪個元素上,down的元素就是用戶點擊的元素,當然爲了嚴謹,我們還要判斷up的時候是否也落在同一個元素上面,因爲,系統click事件的判斷規則就是:down和up同時落在同一個可點擊的元素上。

[java] view plain copy
  1. @Override  
  2. public boolean dispatchTouchEvent(MotionEvent event) {  
  3.     int x = (int) event.getRawX();  
  4.     int y = (int) event.getRawY();  
  5.     int action = event.getAction();  
  6.     if (action == MotionEvent.ACTION_DOWN) {  
  7.         View touchTarget = getTouchTarget(this, x, y);  
  8.         if (touchTarget.isClickable() && touchTarget.isEnabled()) {  
  9.             mTouchTarget = touchTarget;  
  10.             initParametersForChild(event, touchTarget);  
  11.             postInvalidateDelayed(INVALIDATE_DURATION);  
  12.         }  
  13.     } else if (action == MotionEvent.ACTION_UP) {  
  14.         mIsPressed = false;  
  15.         postInvalidateDelayed(INVALIDATE_DURATION);  
  16.         mDispatchUpTouchEventRunnable.event = event;  
  17.         postDelayed(mDispatchUpTouchEventRunnable, 400);  
  18.         return true;  
  19.     } else if (action == MotionEvent.ACTION_CANCEL) {  
  20.         mIsPressed = false;  
  21.         postInvalidateDelayed(INVALIDATE_DURATION);  
  22.     }  
  23.   
  24.     return super.dispatchTouchEvent(event);  
  25. }  
通過上述代碼,我們可以知道,當down的時候,我們取出點擊事件的屏幕座標,然後去遍歷view樹找到用戶所點擊的那個view,代碼如下,就是判斷事件的座標是否落在view的範圍內,這個不再多說了,比較好理解。需要注意的是,事件的座標我們不能用getX和getY,而要用getRawX和getRawY,二者的區別是:前者是相對於被點擊view的座標,後者是相對於屏幕的座標,而我們的目標view具體位於layout的哪一層我們無法知道,所以,必須用屏幕的絕對座標來進行計算。而有了事件的座標,再根據view在屏幕中的絕對座標,只要判斷事件的xy是否落在view的上下左右四個角之內,就可以知道事件是否落在view上,從而取出用戶所點擊的那個view。

[html] view plain copy
  1. private View getTouchTarget(View view, int x, int y) {  
  2.     View target = null;  
  3.     ArrayList<View> TouchableViews = view.getTouchables();  
  4.     for (View child : TouchableViews) {  
  5.         if (isTouchPointInView(child, x, y)) {  
  6.             target = child;  
  7.             break;  
  8.         }  
  9.     }  
  10.   
  11.     return target;  
  12. }  
  13.   
  14. private boolean isTouchPointInView(View view, int x, int y) {  
  15.     int[] location = new int[2];  
  16.     view.getLocationOnScreen(location);  
  17.     int left = location[0];  
  18.     int top = location[1];  
  19.     int right = left + view.getMeasuredWidth();  
  20.     int bottom = top + view.getMeasuredHeight();  
  21.     if (view.isClickable() && y >= top && y <= bottom  
  22.             && x >= left && x <= right) {  
  23.         return true;  
  24.     }  
  25.     return false;  
  26. }  

如何取得被點擊元素的信息

這個比較簡單,被點擊元素的信息有:寬、高、left、top、right、bottom,獲取它們的代碼如下:

[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. int[] location = new int[2];  
  2. mTouchTarget.getLocationOnScreen(location);  
  3. int left = location[0] - mLocationInScreen[0];  
  4. int top = location[1] - mLocationInScreen[1];  
  5. int right = left + mTouchTarget.getMeasuredWidth();  
  6. int bottom = top + mTouchTarget.getMeasuredHeight();  
說明:mTouchTarget指的是用戶點擊的那個view

如何通過layout進行重繪繪製水波紋

這個會水波紋比較簡單,只要用drawCircle繪製一個半透明的圓環即可,這裏主要說下繪製時機。一般來說,我們會選擇在onDraw中去進行繪製,這是沒錯的,但是對於L中的效果不太適合,查看view的繪製過程,我們會明白,view的繪製大致遵循如下流程:先繪製背景,再繪製自己(onDraw),接着繪製子元素(dispatchDraw),最後繪製一些裝飾等比如滾動條(onDrawScrollBars),因此,如果我們在onDraw中繪製波紋,那麼由於子元素的繪製在onDraw之後,就會導致子元素蓋住我們所繪製的圓環,這樣,圓環就有可能看不全了,因爲,把我繪製的時機很重要。根據view的繪製流程,我們選擇dispatchDraw比較合適,當所有的子元素都繪製完成後,再進行波紋的繪製。讀到這裏,大家會更加明白,爲什麼我們要選擇LinearLayout以及爲什麼不建議view的嵌套層級太深,因爲如果view本身比較重或者嵌套層級太深,就會導致dispatchDraw執行的耗時增加,這樣水波的繪製就會收到些許影響。因此,性能的平滑在代碼中也很重要,也是需要考慮的。同時,爲了不讓繪製的圓環超出被點擊元素的範圍,我們需要對canvas進行clip。爲了有波紋效果,我們需要頻繁地進行layout重繪,並且在重繪的過程中改變圓環的半徑,這樣一個動態的水波紋就出來了。仍然,我來性能的考慮,我們選擇用postInvalidateDelayed(long delayMilliseconds, int left, int top, int right, int bottom)來進行view的部分重繪,因爲,其他區域是不需要重繪的,僅僅是被點擊的元素所在的區域需要重繪。爲什麼要採用Delayed這個方法,原因是我們不能一直進行刷新,必須有一點點時間間隔,這樣做的好處是:避免view的重繪搶佔過多時間片從而造成潛在的間接棧溢出,因爲invalidate會直接導致draw的調用。

具體代碼如下:

[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. protected void dispatchDraw(Canvas canvas) {  
  2.     super.dispatchDraw(canvas);  
  3.     if (!mShouldDoAnimation || mTargetWidth <= 0 || mTouchTarget == null) {  
  4.         return;  
  5.     }  
  6.   
  7.     if (mRevealRadius > mMinBetweenWidthAndHeight / 2) {  
  8.         mRevealRadius += mRevealRadiusGap * 4;  
  9.     } else {  
  10.         mRevealRadius += mRevealRadiusGap;  
  11.     }  
  12.     int[] location = new int[2];  
  13.     mTouchTarget.getLocationOnScreen(location);  
  14.     int left = location[0] - mLocationInScreen[0];  
  15.     int top = location[1] - mLocationInScreen[1];  
  16.     int right = left + mTouchTarget.getMeasuredWidth();  
  17.     int bottom = top + mTouchTarget.getMeasuredHeight();  
  18.   
  19.     canvas.save();  
  20.     canvas.clipRect(left, top, right, bottom);  
  21.     canvas.drawCircle(mCenterX, mCenterY, mRevealRadius, mPaint);  
  22.     canvas.restore();  
  23.   
  24.     if (mRevealRadius <= mMaxRevealRadius) {  
  25.         postInvalidateDelayed(INVALIDATE_DURATION, left, top, right, bottom);  
  26.     } else if (!mIsPressed) {  
  27.         mShouldDoAnimation = false;  
  28.         postInvalidateDelayed(INVALIDATE_DURATION, left, top, right, bottom);  
  29.     }  
  30. }  
到此爲止,這個layout我們已經實現了,但是細心的你,一定會發現,還有什麼不妥的地方。比如,你可以給button加一個點擊事件,當button被點擊的時候起一個activity,很快你就會發現問題所在了:水波還沒播完呢,activity就起來了,導致水波效果大打折扣,而仔細觀察android L的效果,我們發現,L中總是要等到水波效果播放完畢纔會進行下一步的行爲。所以,最後一個待解決的問題也就出來了,請看下面的分析

如何延遲up事件的分發

針對上面所說的問題,如果我們能夠延遲up時間的分發,比如延遲400ms,這樣水波就有足夠的時間去播放完畢,然後再分發up事件,這樣就可以解決問題。最開始,我的確是這樣做的,先看如下的代碼:

[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. else if (action == MotionEvent.ACTION_UP) {  
  2.            mIsPressed = false;  
  3.            postInvalidateDelayed(INVALIDATE_DURATION);  
  4.            mDispatchUpTouchEventRunnable.event = event;  
  5.            postDelayed(mDispatchUpTouchEventRunnable, 400);  
  6.            return true;  
  7.        }   
可以發現,當up的時候,我並沒有直接走系統的分發流程,只是強行消耗點up事件然後再延遲分發,請看代碼:

[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. private class DispatchUpTouchEventRunnable implements Runnable {  
  2.     public MotionEvent event;  
  3.   
  4.     @Override  
  5.     public void run() {  
  6.         if (mTouchTarget == null || !mTouchTarget.isEnabled()) {  
  7.             return;  
  8.         }  
  9.   
  10.         if (isTouchPointInView(mTouchTarget, (int)event.getRawX(), (int)event.getRawY())) {  
  11.             mTouchTarget.dispatchTouchEvent(event);  
  12.         }  
  13.     }  
  14. };  

到此爲止,上述幾個問題都已經分析完畢了,我們就可以輕易地實現水波紋的點擊效果了。

源碼下載

本文中的demo源碼暫時未開放到互聯網上,請加羣 215680213 ,在羣共享中下載源碼。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章