Android中水波紋使用之自定義檢視實現

Anumbrella發表於2017-03-05

在前面一篇部落格中介紹了Android中水波紋的使用,如果忘記了可以去複習一下Android中水波紋使用,但是那種實現方法要在API21+以上才能使用。如果我們要相容API21以下的系統,則需要通過第三方的類庫,即通過自定義檢視來實現水波紋的效果。

自定義檢視,是Android中擴充套件控制元件常用的方法,這裡不做過多的介紹,如果不太會,可以去搜尋學習一下。這裡主要介紹如何實現水波紋效果,其次是對自定義檢視中常用的問題總結一下。

自定義檢視會涉及到建構函式,現在的自定義檢視建構函式有4個,最後一個一般是API21+,所以平時不常用。對這個不瞭解的可以去看看Android View 四個建構函式詳解

總結一下:
一般第一個建構函式是指在程式碼中建立例項化呼叫;第二個建構函式是通過XML方式建立控制元件檢視,提供AttributeSet屬性設定;第三個也是通過XML方式建立控制元件檢視,提供AttributeSet屬性設定,同時提供預設樣式值defStyleAttr;第四個一樣通過XML方式建立,除了提供跟第三個一樣的引數外,新增defStyleRes。

View類的後兩個建構函式都是與主題相關的。
它們的屬性賦值優先順序為:

XML直接定義 > XML中style引用 > defStyleAttr > defStyleRes > theme直接定義

瞭解了上面後,我們再來看看幾個自定義檢視中會常用到的函式:
onFinishInflate()
onAttachedToWindow()
onMeasure()
onSizeChanged ()
onLayout ()
onConfigurationChanged()
onDraw()
dispatchDraw ()
draw()
onTouchEvent()
onInterceptTouchEvent()

onFinishInflate()方法一般是在xml檔案載入完成後呼叫這個方法;
onAttachedToWindow()方法是將檢視依附到Window中;
onMeasure()方法是測量自定義檢視的大小 ;
onSizeChanged()方法是自定義檢視大小發生改變時呼叫;
onLayout()方法是將自定義檢視放置到父容器的具體某個位置中;
onConfigurationChanged()方法是在當手機螢幕從橫屏和豎屏相互轉化時呼叫;
onDraw() 、dispatchDraw ()、draw()這三個方法,則是根據具體的情況來呼叫的;

  1. 自定義一個view時,重寫onDraw()。
    呼叫view.invalidate(),會導致draw流程重新執行。
    view.postInvalidate(); //是在非UI執行緒上呼叫的

  2. 自定義一個ViewGroup,重寫onDraw()。
    onDraw可能不會被呼叫,原因是需要先設定一個背景(顏色或圖)。
    表示這個group有東西需要繪製了,才會觸發draw,之後是onDraw。
    因此,一般直接重寫dispatchDraw()來繪製viewGroup

  3. dispatchDraw會()對子檢視進行分發繪製操作。

總結一下:
View元件的繪製會呼叫draw(Canvas canvas)方法,draw過程中主要是先畫Drawable背景,對 drawable呼叫setBounds()然後是draw(Canvas c)方法。有點注意的是背景drawable的實際大小會影響view元件的大小,drawable的實際大小通過getIntrinsicWidth()和getIntrinsicHeight()獲取,當背景比較大時view元件大小等於背景drawable的大小。

畫完背景後,draw過程會呼叫onDraw(Canvas canvas)方法,然後就是dispatchDraw(Canvas canvas)方法, dispatchDraw()主要是分發給子元件進行繪製,我們通常定製元件的時候重寫的是onDraw()方法。值得注意的是ViewGroup容器元件的繪製,當它沒有背景時直接呼叫的是dispatchDraw()方法, 而繞過了draw()方法,當它有背景的時候就呼叫draw()方法,而draw()方法裡包含了dispatchDraw()方法的呼叫。因此要在ViewGroup上繪製東西的時候往往重寫的是dispatchDraw()方法而不是onDraw()方法,或者自定製一個Drawable,重寫它的draw(Canvas c)和 getIntrinsicWidth()。

該總結來自網路,感謝分享~~~

最後是onTouchEvent()和onInterceptTouchEvent()方法,這兩個方法也是我們經常會用到的。

onInterceptTouchEvent()方法定義在於ViewGroup中,預設返回值為false,表示不攔截TouchEvent()。onTouchEvent()方法定義在View中,當ViewGroup要呼叫onTouchEvent()時,呼叫super.onTouchEvent()方法。ViewGroup呼叫onTouchEvent()預設返回false,表示不消耗touch事件,View呼叫onTouchEvent()預設返回true,表示消耗了touch事件。

到這裡我們把自定義檢視會常遇到的方法都大致總結了一下。接下來再進行分析,因為有了這些方法,但我們還不知道它們呼叫的順序,只有清楚了這些後才能做出更好的自定義檢視。

首先建立自定義檢視如下,繼承View父類,然後列印出各種方法。

public class CustomView extends View {

    public CustomView(Context context) {
        super(context);
        Log.i("anumbrella","constructor1");
    }

    public CustomView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        Log.i("anumbrella","constructor2");
    }

    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        Log.i("anumbrella","constructor3");
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        Log.i("anumbrella","constructor3");
    }


    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        Log.i("anumbrella","onAttachedToWindow()");
    }

    @Override
    protected void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        Log.i("anumbrella","onConfigurationChanged()");
    }


    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        Log.i("anumbrella","dispatchDraw()");
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.i("anumbrella","onDraw()");
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
        Log.i("anumbrella","draw()");
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        Log.i("anumbrella","onLayout()");
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.i("anumbrella","onMeasure()");
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        Log.i("anumbrella", "onSizeChanged() " + " w = " + w + "  h = " + h + "  oldW = " + oldw + "  oldH = " + oldw);
    }
}

結果如下圖:
結果

我們可以看到自定義view執行的方法順序為:constructor2->onAttachedToWindow()->onMeasure()->onSizeChanged()->onLayout()->onMeasure()->onLayout()->onDraw()->dispatchDraw()->draw()。

現在我們將上面的程式碼改為繼承VIewGroup(如:RelativeLayout)的檢視控制元件,再新增onTouchEvent()和onInterceptTouchEvent()方法,再執行。
如下:
結果2

為啥沒有呼叫draw()方法?因為ViewGroup沒有背景顏色,這就跟上面總結的一樣的。我們加上背景顏色android:background=”@color/colorPrimary”。重新執行,結果可以看到呼叫了draw()方法:

結果3

然後我們在自定義的ViewGroup下面新增子檢視,比如TextView重新執行,點選時就會呼叫onTouchEvent()方法。

這裡只是對自定義檢視會用到的方法進行了簡單的介紹,更深入的瞭解在此不做過多介紹。

好了,接下來我們才要開始進入主題——自定義的水波紋實現效果。有了上面的知識,我相信對下面的程式碼理解就會容易多了。

先來看看效果:
gif

具體的程式碼如下,我們接下來一步一步介紹:

public class RippleView extends RelativeLayout {


    /**
     * 水波紋的顏色
     */
    private int rippleColor;


    /**
     * 水波紋擴散型別
     */
    private Integer rippleType;

    /**
     * 放大持續時間
     */
    private int zoomDuration;

    /**
     * 放大比例
     */
    private float zoomScale;

    /**
     * 放大動畫類
     */
    private ScaleAnimation scaleAnimation;


    /**
     * 檢視是否放大
     */
    private Boolean hasToZoom;

    /**
     * 是否從檢視中心開始動畫
     */
    private Boolean isCentered;


    /**
     * 幀速率
     */
    private int frameRate = 10;

    /**
     * 水波紋持續時間
     */
    private int rippleDuration = 400;


    /**
     * 水波紋透明度
     */
    private int rippleAlpha = 90;


    /**
     * canvas畫布執行Handler
     */
    private Handler canvasHandler;

    /**
     * 水波紋畫筆
     */
    private Paint paint;


    /**
     * 水波紋擴散內邊距
     */
    private int ripplePadding;


    /**
     * 手勢監聽類
     */
    private GestureDetector gestureDetector;


    /**
     * 水波紋動畫是否開始
     */
    private boolean animationRunning = false;


    /**
     * 時間統計
     */
    private int timer = 0;


    /**
     * 時間間隔
     */
    private int timerEmpty = 0;

    /**
     * 水波紋持續時間間隔
     */
    private int durationEmpty = -1;


    /**
     * 最大圓半徑
     */
    private float radiusMax = 0;


    /**
     * 水波紋圓的座標點
     */
    private float x = -1;
    private float y = -1;


    private Bitmap originBitmap;

    private OnRippleCompleteListener onCompletionListener;


    /**
     * 檢視的寬和高
     */
    private int WIDTH;

    private int HEIGHT;


    /**
     * 定義水波紋型別
     */
    public enum RippleType {
        SIMPLE(0),
        DOUBLE(1),
        RECTANGLE(2);

        int type;

        RippleType(int type) {
            this.type = type;
        }
    }


    /**
     * 水波紋更新波紋Runnable
     */
    private final Runnable runnable = new Runnable() {
        @Override
        public void run() {
            invalidate();
        }
    };


    /**
     * 定義回撥函式,當水波紋效果完成時呼叫
     */
    public interface OnRippleCompleteListener {
        void onComplete(RippleView rippleView);
    }


    public RippleView(Context context) {
        super(context);
    }

    public RippleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }


    public RippleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    /**
     * 初始化方法
     *
     * @param context
     * @param attrs
     */
    private void init(Context context, AttributeSet attrs) {
        if (isInEditMode()) {
            return;
        }

        final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RippleView);
        rippleColor = typedArray.getColor(R.styleable.RippleView_rv_color, getResources().getColor(R.color.rippelColor));
        rippleType = typedArray.getInt(R.styleable.RippleView_rv_type, 0);
        hasToZoom = typedArray.getBoolean(R.styleable.RippleView_rv_zoom, false);
        isCentered = typedArray.getBoolean(R.styleable.RippleView_rv_centered, false);
        rippleDuration = typedArray.getInteger(R.styleable.RippleView_rv_rippleDuration, rippleDuration);
        rippleAlpha = typedArray.getInteger(R.styleable.RippleView_rv_alpha, rippleAlpha);
        ripplePadding = typedArray.getDimensionPixelSize(R.styleable.RippleView_rv_ripplePadding, 0);
        canvasHandler = new Handler();
        zoomScale = typedArray.getFloat(R.styleable.RippleView_rv_zoomScale, 1.03f);
        zoomDuration = typedArray.getInt(R.styleable.RippleView_rv_zoomDuration, 200);
        typedArray.recycle();
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(rippleColor);
        paint.setAlpha(rippleAlpha);
        //使onDraw方法可以呼叫,以便被我們重寫
        this.setWillNotDraw(false);

        gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
            @Override
            public void onLongPress(MotionEvent event) {
                super.onLongPress(event);

            }

            @Override
            public boolean onSingleTapConfirmed(MotionEvent e) {
                return true;
            }

            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                return true;
            }
        });

        //開啟cache來繪製檢視
        this.setDrawingCacheEnabled(true);
        this.setClickable(true);
    }

    /**
     * 繪製水波紋
     *
     * @param canvas
     */
    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
        if (animationRunning) {
            canvas.save();
            if (rippleDuration <= timer * frameRate) {
                animationRunning = false;
                timer = 0;
                durationEmpty = -1;
                timerEmpty = 0;
                //android 23 會自動呼叫canvas.restore();
                if (Build.VERSION.SDK_INT != 23) {
                    canvas.restore();
                }
                invalidate();
                if (onCompletionListener != null) {
                    onCompletionListener.onComplete(this);
                }
                return;
            } else {
                canvasHandler.postDelayed(runnable, frameRate);
            }

            if (timer == 0) {
                canvas.save();
            }

            canvas.drawCircle(x, y, (radiusMax * (((float) timer * frameRate) / rippleDuration)), paint);
            paint.setColor(Color.parseColor("#ffff4444"));


            if (rippleType == 1 && originBitmap != null && (((float) timer * frameRate) / rippleDuration) > 0.4f) {
                if (durationEmpty == -1) {
                    durationEmpty = rippleDuration - timer * frameRate;
                }
                timerEmpty++;
                final Bitmap tmpBitmap = getCircleBitmap((int) ((radiusMax) * (((float) timerEmpty * frameRate) / (durationEmpty))));
                canvas.drawBitmap(tmpBitmap, 0, 0, paint);
                tmpBitmap.recycle();
            }
            paint.setColor(rippleColor);

            if (rippleType == 1) {
                if ((((float) timer * frameRate) / rippleDuration) > 0.6f) {
                    paint.setAlpha((int) (rippleAlpha - ((rippleAlpha) * (((float) timerEmpty * frameRate) / (durationEmpty)))));
                } else {
                    paint.setAlpha(rippleAlpha);
                }
            } else {
                paint.setAlpha((int) (rippleAlpha - ((rippleAlpha) * (((float) timer * frameRate) / rippleDuration))));
            }
            timer++;
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        WIDTH = w;
        HEIGHT = h;

        scaleAnimation = new ScaleAnimation(1.0f, zoomScale, 1.0f, zoomScale, w / 2, h / 2);
        scaleAnimation.setDuration(zoomDuration);
        scaleAnimation.setRepeatMode(Animation.REVERSE);
        scaleAnimation.setRepeatCount(1);
    }


    /**
     * 啟動水波紋動畫,通過MotionEvent事件
     *
     * @param event
     */
    public void animateRipple(MotionEvent event) {
        createAnimation(event.getX(), event.getY());
    }

    /**
     * 啟動水波紋動畫,通過x,y座標
     *
     * @param x
     * @param y
     */
    public void animateRipple(final float x, final float y) {
        createAnimation(x, y);
    }


    private void createAnimation(final float x, final float y) {
        if (this.isEnabled() && !animationRunning) {
            if (hasToZoom) {
                this.startAnimation(scaleAnimation);
            }

            radiusMax = Math.max(WIDTH, HEIGHT);

            if (rippleType != 2) {
                radiusMax /= 2;
            }

            radiusMax -= ripplePadding;

            if (isCentered || rippleType == 1) {
                this.x = getMeasuredWidth() / 2;
                this.y = getMeasuredHeight() / 2;
            } else {
                this.x = x;
                this.y = y;
            }

            animationRunning = true;

            if (rippleType == 1 && originBitmap == null) {
                originBitmap = getDrawingCache(true);
            }
            invalidate();
        }
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (gestureDetector.onTouchEvent(event)) {
            animateRipple(event);
            sendClickEvent(false);
        }
        return super.onTouchEvent(event);
    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        this.onTouchEvent(event);
        return super.onInterceptTouchEvent(event);
    }


    /**
     * 傳送一個點選事件,如果父檢視是ListView例項
     *
     * @param isLongClick
     */
    private void sendClickEvent(final Boolean isLongClick) {
        if (getParent() instanceof AdapterView) {
            final AdapterView adapterView = (AdapterView) getParent();
            final int position = adapterView.getPositionForView(this);
            final long id = adapterView.getItemIdAtPosition(position);
            if (isLongClick) {
                if (adapterView.getOnItemLongClickListener() != null) {
                    adapterView.getOnItemLongClickListener().onItemLongClick(adapterView, this, position, id);
                }
            } else {
                if (adapterView.getOnItemClickListener() != null) {
                    adapterView.getOnItemClickListener().onItemClick(adapterView, this, position, id);
                }
            }
        }
    }


    /**
     * 設定水波紋的顏色
     *
     * @param rippleColor
     */
    public void setRippleColor(int rippleColor) {
        this.rippleColor = getResources().getColor(rippleColor);
    }

    public int getRippleColor() {
        return rippleColor;
    }

    public RippleType getRippleType() {
        return RippleType.values()[rippleType];
    }


    /**
     * 設定水波紋動畫型別,預設為RippleType.SIMPLE
     *
     * @param rippleType
     */
    public void setRippleType(final RippleType rippleType) {
        this.rippleType = rippleType.ordinal();
    }

    public Boolean isCentered() {
        return isCentered;
    }

    /**
     * 設定水波紋動畫是否開始從父檢視中心開始,預設為false
     *
     * @param isCentered
     */
    public void setCentered(final Boolean isCentered) {
        this.isCentered = isCentered;
    }

    public int getRipplePadding() {
        return ripplePadding;
    }

    /**
     * 設定水波紋內邊距,預設為0dip
     *
     * @param ripplePadding
     */
    public void setRipplePadding(int ripplePadding) {
        this.ripplePadding = ripplePadding;
    }

    public Boolean isZooming() {
        return hasToZoom;
    }

    /**
     * 在水波紋結束後,是否有放大動畫,預設為false
     *
     * @param hasToZoom
     */
    public void setZooming(Boolean hasToZoom) {
        this.hasToZoom = hasToZoom;
    }

    public float getZoomScale() {
        return zoomScale;
    }

    /**
     * 設定放大動畫比例
     *
     * @param zoomScale
     */
    public void setZoomScale(float zoomScale) {
        this.zoomScale = zoomScale;
    }

    public int getZoomDuration() {
        return zoomDuration;
    }

    /**
     * 設定放大動畫持續時間,預設為200ms
     *
     * @param zoomDuration
     */
    public void setZoomDuration(int zoomDuration) {
        this.zoomDuration = zoomDuration;
    }

    public int getRippleDuration() {
        return rippleDuration;
    }

    /**
     * 設定水波紋動畫持續時間,預設為400ms
     *
     * @param rippleDuration
     */
    public void setRippleDuration(int rippleDuration) {
        this.rippleDuration = rippleDuration;
    }

    public int getFrameRate() {
        return frameRate;
    }

    /**
     * 設定水波紋動畫的幀速率,預設為10
     *
     * @param frameRate
     */
    public void setFrameRate(int frameRate) {
        this.frameRate = frameRate;
    }

    public int getRippleAlpha() {
        return rippleAlpha;
    }

    /**
     * 設定水波紋動畫的透明度,預設為90,取值為0到255之間
     *
     * @param rippleAlpha
     */
    public void setRippleAlpha(int rippleAlpha) {
        this.rippleAlpha = rippleAlpha;
    }


    public void setOnRippleCompleteListener(OnRippleCompleteListener listener) {
        this.onCompletionListener = listener;
    }


    /**
     * 繪製擴散背景範圍檢視bitmap
     *
     * @param radius
     * @return
     */
    private Bitmap getCircleBitmap(final int radius) {
        final Bitmap output = Bitmap.createBitmap(originBitmap.getWidth(), originBitmap.getHeight(), Bitmap.Config.ARGB_8888);
        final Canvas canvas = new Canvas(output);
        final Paint paint = new Paint();
        final Rect rect = new Rect((int) (x - radius), (int) (y - radius), (int) (x + radius), (int) (y + radius));

        paint.setAntiAlias(true);
        canvas.drawARGB(0, 0, 0, 0);
        canvas.drawCircle(x, y, radius, paint);
        //出來兩圖交叉情況
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        canvas.drawBitmap(originBitmap, rect, rect, paint);
        return output;
    }
}

定義RippleView類繼承RelativeLayout。為啥是RelativeLayout?覺得可能比LinearLayout耗效能,但是它的擴充套件性畢竟好很多。所以這點還是可以忽略的,其次通過建構函式,引入attr下的style屬性。

attr下的屬性為:

 <!--自定義水波紋樣式屬性-->
    <declare-styleable name="RippleView">
        <!--定義透明度-->
        <attr name="rv_alpha" format="integer" />
        <!--畫面frame速率(幀速率)-->
        <attr name="rv_framerate" format="integer" />
        <!--水波紋持續時間-->
        <attr name="rv_rippleDuration" format="integer" />
        <!--檢視放大持續時間-->
        <attr name="rv_zoomDuration" format="integer" />
        <!--擴散水波紋顏色-->
        <attr name="rv_color" format="color" />
        <!--是否從中心擴散-->
        <attr name="rv_centered" format="boolean" />
        <!--水波紋樣式-->
        <attr name="rv_type" format="enum">
            <enum name="simpleRipple" value="0" />
            <enum name="doubleRipple" value="1" />
            <enum name="rectangle" value="2" />
        </attr>
        <!--水波紋擴散內邊距-->
        <attr name="rv_ripplePadding" format="dimension" />
        <!--水波紋是否放大-->
        <attr name="rv_zoom" format="boolean" />
        <!--放大比例-->
        <attr name="rv_zoomScale" format="float" />

    </declare-styleable>

有了這些後,我們就在init()函式中獲取定義的屬性,如果沒有獲取到
XML中定義的屬性就設定為預設的值。然後呼叫onMeasure()來獲取背
景檢視的大小,再呼叫draw()方法去繪製。

在draw()方法中,一開始animationRunning是false,所以不會執行任何操作。

當我們點選檢視時,這個onTouchEvent()方法就會呼叫,獲取具體的x,y座標,然後對radiusMax、animationRunning變數進行設定。
這個時候animationRunning為ture。最後呼叫invalidate(),重新draw()開始繪製。

在draw()中判斷時間是否結束了,沒有結束就通過canvasHandler來不停更新檢視,呼叫draw()重新繪製檢視。同時每次timer都會進行增加1,然後我們就通過timer的改變來實現半徑大小的變化。每次繪製圓就形成了水波紋。

當要實現不同效果的水波紋時,即rippleType的值不一樣時。就可以通過繪製不同的背景效果,改變透明度來實現。

好了,結束了。這就是水波紋自定義檢視的大致實現,水波紋演示程式碼

相關文章