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

博主參加了2014 CSDN博客之星評選,幫我投一票吧。html

點擊給我投票
java


前言

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


實現思想

首先咱們自定義一個layout,這裏咱們選取LinearLayout,至於緣由,文章下面會進行分析。當用戶點擊一個可點擊的元素時,好比button,咱們須要獲得用戶點擊的元素的信息,包含:用戶點擊了哪一個元素、用戶點擊的那個元素的寬、高、位置信息等。獲得了button的信息後,我就能夠肯定水波紋的範圍,而後經過layout進行重繪去繪製水波紋,這樣水波紋效果就實現了,固然,這只是大概步驟,中間仍是有一些細節須要處理的。canvas

layout的選取

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

根據上面的分析,咱們定義以下的layout:佈局

public class RevealLayout extends LinearLayout implements Runnable
post

實現過程

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

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

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

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

4. 若是延遲up事件的分發

下面一一進行分析

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

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

@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);
    }
經過上述代碼,咱們能夠知道,當down的時候,咱們取出點擊事件的屏幕座標,而後去遍歷view樹找到用戶所點擊的那個view,代碼以下,就是判斷事件的座標是否落在view的範圍內,這個再也不多說了,比較好理解。須要注意的是,事件的座標咱們不能用getX和getY,而要用getRawX和getRawY,兩者的區別是:前者是相對於被點擊view的座標,後者是相對於屏幕的座標,而咱們的目標view具體位於layout的哪一層咱們沒法知道,因此,必須用屏幕的絕對座標來進行計算。而有了事件的座標,再根據view在屏幕中的絕對座標,只要判斷事件的xy是否落在view的上下左右四個角以內,就能夠知道事件是否落在view上,從而取出用戶所點擊的那個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;
    }

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

這個比較簡單,被點擊元素的信息有:寬、高、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指的是用戶點擊的那個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的調用。

具體代碼以下:

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);
        }
    }
到此爲止,這個layout咱們已經實現了,可是細心的你,必定會發現,還有什麼不妥的地方。好比,你能夠給button加一個點擊事件,當button被點擊的時候起一個activity,很快你就會發現問題所在了:水波還沒播完呢,activity就起來了,致使水波效果大打折扣,而仔細觀察android L的效果,咱們發現,L中老是要等到水波效果播放完畢纔會進行下一步的行爲。因此,最後一個待解決的問題也就出來了,請看下面的分析

如何延遲up事件的分發

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

else if (action == MotionEvent.ACTION_UP) {
            mIsPressed = false;
            postInvalidateDelayed(INVALIDATE_DURATION);
            mDispatchUpTouchEventRunnable.event = event;
            postDelayed(mDispatchUpTouchEventRunnable, 400);
            return true;
        }
能夠發現,當up的時候,我並無直接走系統的分發流程,只是強行消耗點up事件而後再延遲分發,請看代碼:

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);
            }
        }
    };

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

源碼下載

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

相關文章
相關標籤/搜索