Android自定義View 水波氣泡

前言:公司在作的一個項目,要求在地圖上以水波氣泡的形式來顯示站點,而且氣泡要有水波的動態效果。好吧!既然有這樣的需求,那就手擼一款水波氣泡吧!java

效果圖預覽

  最後完成的效果圖以下git

不想看文章的話,能夠點擊這裏,直接獲取源碼。github

實現方式

步驟拆解

  在須要自定義view的時候,我首先要作的就是將最後要實現的效果來進行拆分,拆分紅許多小的步驟,而後一步步的來實現,最終達到想要的效果。canvas

  能夠將文章開始的時候的效果圖拆分紅如下幾部分:markdown

  1. 畫出氣泡後面的白色背景。
  2. 畫內部的紫色氣泡。
  3. 用貝塞爾曲線讓內部的紫色氣泡動起來。

拆解以後,就能夠按照拆解的步驟來一步步實現了。ide

畫白色背景

  這裏畫白色背景有如下兩種方式:oop

  1. path直接描述一個白色背景的形狀。
  2. path描述一個三角形,而後在畫出一個圓形,即成最終的白色背景了。

第一種方式以下圖的左圖,用path直接描述出了白色背景,這種方式能夠用path.addArc()來畫上部弧形,而後用path.moveTo()path.lineTo()方法描述出下部分的尖角。post

第二種實現的方式以下圖的右圖,直接畫出一個圓,再用path.moveTo()path.lineTo()方法來描述出下部分的尖角。動畫

本文采用的是第二種方式來實現的,具體代碼以下ui

//此處代碼是下部尖角的path
mBackgroundPath.moveTo(mResultWidth / 2 - mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2);
        mBackgroundPath.lineTo(mResultWidth / 2, mResultWidth / 2 + mOutRadius + mOutRadius / 4);
        mBackgroundPath.lineTo(mResultWidth / 2 + mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2);

 //畫外部背景
        canvas.drawPath(mBackgroundPath, mBackgroundPaint);
        canvas.drawCircle(mResultWidth / 2, mResultWidth / 2, mOutRadius, mBackgroundPaint);
複製代碼

畫內部的氣泡

  內部的氣泡的形狀其實就是縮小的外部背景,具體的代碼以下

//內部氣泡的尖角 
mBubblesPath.moveTo(mResultWidth / 2 - mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2 - dp2px(getContext(), 5));
        mBubblesPath.lineTo(mResultWidth / 2, mResultWidth / 2 + mOutRadius + mOutRadius / 4 - dp2px(getContext(), 5));
        mBubblesPath.lineTo(mResultWidth / 2 + mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2 - dp2px(getContext(), 5));
//畫圓
 mBubblesPath.addCircle(mResultWidth / 2, mResultWidth / 2, mInnerRadius, Path.Direction.CCW);
複製代碼

到這裏已經將氣泡的基本形狀畫出來了,見下圖

咱們會發現氣泡內部的顏色是漸變色,那漸變色是怎麼設置的呢?其實自定義view就是將想要的效果經過畫筆畫在畫布上,實現顏色的漸變確定就是經過設置畫筆的屬性來實現的了,設置漸變色的代碼以下

//設置漸變色
        Shader shader = new LinearGradient(mResultWidth / 2, mResultWidth / 2 - mInnerRadius, mResultWidth / 2, mResultWidth / 2 + mInnerRadius, Color.parseColor("#9592FB"),
                Color.parseColor("#3831D4"), Shader.TileMode.CLAMP);
        mBubblesPaint.setShader(shader);
複製代碼

LinearGradient(float x0, float y0, float x1, float y1, @ColorInt int color0, @ColorInt int color1, @NonNull TileMode tile)

x0 y0 x1 y1:漸變的兩個端點的位置  color0 color1 是端點的顏色  tile:端點範圍以外的着色規則,類型是 TileModeTileMode 一共有 3 個值可選: CLAMPMIRROR和 REPEAT。通常用 CLAMP就能夠了。

讓內部氣泡動起來

  氣泡內部的動畫是水波的形式,這裏畫水波用的是二階貝塞爾曲線,關於Android中貝塞爾曲線的知識能夠參考這裏。實現氣泡內部水波效果的代碼以下

/** * 核心代碼,計算path * * @return */
    private Path getPath() {
        int itemWidth = waveWidth / 2;//半個波長
        Path mPath = new Path();
        mPath.moveTo(-itemWidth * 3, baseLine);//起始座標
        Log.d(TAG, "getPath: " + baseLine);

        //核心的代碼就是這裏
        for (int i = -3; i < 2; i++) {
            int startX = i * itemWidth;
            mPath.quadTo(
                    startX + itemWidth / 2 + offset,//控制點的X,(起始點X + itemWidth/2 + offset)
                    getWaveHeight(i),//控制點的Y
                    startX + itemWidth + offset,//結束點的X
                    baseLine//結束點的Y
            );//只須要處理完半個波長,剩下的有for循環自已就添加了。
        }
        Log.d(TAG, "getPath: ");
        //下面這三句話是行程封閉的效果,不明白能夠將下面3句代碼註釋看下效果的變化
        mPath.lineTo(width, height);
        mPath.lineTo(0, height);
        mPath.close();
        return mPath;
    }

//奇數峯值是正的,偶數峯值是負數
    private float getWaveHeight(int num) {
        if (num % 2 == 0) {
            return baseLine + waveHeight;
        }
        return baseLine - waveHeight;
    }
複製代碼

上面的代碼畫出的水波以下圖

到這裏已經畫出了水波,但如今水波仍是靜止的,要讓水波不停的移動,就要添加屬性動畫,添加動畫的代碼以下

/** * 不斷的更新偏移量,而且循環。 */
    public void updateXControl() {
        //設置一個波長的偏移
        ValueAnimator mAnimator = ValueAnimator.ofFloat(0, waveWidth);
        mAnimator.setInterpolator(new LinearInterpolator());
        mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float animatorValue = (float) animation.getAnimatedValue();
                offset = animatorValue;//不斷的設置偏移量,並重畫
                postInvalidate();
            }
        });
        mAnimator.setDuration(1800);
        mAnimator.setRepeatCount(ValueAnimator.INFINITE);
        mAnimator.start();

    }
複製代碼

修改一下onDraw中的代碼,以下

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mBubblesPath.reset();

        //設置漸變色
        Shader shader = new LinearGradient(mResultWidth / 2, mResultWidth / 2 - mInnerRadius, mResultWidth / 2, mResultWidth / 2 + mInnerRadius, Color.parseColor("#9592FB"),
                Color.parseColor("#3831D4"), Shader.TileMode.CLAMP);
        mBubblesPaint.setShader(shader);

        //此處代碼是下部尖角的path
        mBackgroundPath.moveTo(mResultWidth / 2 - mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2);
        mBackgroundPath.lineTo(mResultWidth / 2, mResultWidth / 2 + mOutRadius + mOutRadius / 4);
        mBackgroundPath.lineTo(mResultWidth / 2 + mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2);


        //內部氣泡的尖角
        mBubblesPath.moveTo(mResultWidth / 2 - mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2 - dp2px(getContext(), 5));
        mBubblesPath.lineTo(mResultWidth / 2, mResultWidth / 2 + mOutRadius + mOutRadius / 4 - dp2px(getContext(), 5));
        mBubblesPath.lineTo(mResultWidth / 2 + mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2 - dp2px(getContext(), 5));
        //畫外部背景
        canvas.drawPath(mBackgroundPath, mBackgroundPaint);
        canvas.drawCircle(mResultWidth / 2, mResultWidth / 2, mOutRadius, mBackgroundPaint);
        Log.d(TAG, "cx: " + mResultWidth / 2);
        //畫水波
        mBubblesPath.addCircle(mResultWidth / 2, mResultWidth / 2, mInnerRadius, Path.Direction.CCW);

        canvas.drawPath(getPath(), mBubblesPaint);

    }
複製代碼

好了,如今水波已經能夠移動了,看下效果

what!怎麼成這個樣子了呀,明顯不是我想要的效果呀,確定是哪裏出錯了,通過我仔細的推敲,總結了出現上面問題的緣由,緣由以下圖

出現上面問題的緣由就是由於下面三句代碼

mPath.lineTo(width, height);
 mPath.lineTo(0, height);
 mPath.close();
複製代碼

知道是這三句代碼的緣由,那應該怎麼修改呢?這三句代碼好像不能動,否則就會出現波浪畫的不完整的狀況,額.....,那應該修改哪裏呢?靈光一閃,不是能夠裁剪畫布嘛,只要將畫布裁剪成想要的形狀,而後在畫波浪不久完美了。再修改onDraw方法,修改後的代碼以下

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mBubblesPath.reset();

        //設置漸變色
        Shader shader = new LinearGradient(mResultWidth / 2, mResultWidth / 2 - mInnerRadius, mResultWidth / 2, mResultWidth / 2 + mInnerRadius, Color.parseColor("#9592FB"),
                Color.parseColor("#3831D4"), Shader.TileMode.CLAMP);
        mBubblesPaint.setShader(shader);

        //此處代碼是下部尖角的path
        mBackgroundPath.moveTo(mResultWidth / 2 - mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2);
        mBackgroundPath.lineTo(mResultWidth / 2, mResultWidth / 2 + mOutRadius + mOutRadius / 4);
        mBackgroundPath.lineTo(mResultWidth / 2 + mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2);


        //內部氣泡的尖角
        mBubblesPath.moveTo(mResultWidth / 2 - mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2 - dp2px(getContext(), 5));
        mBubblesPath.lineTo(mResultWidth / 2, mResultWidth / 2 + mOutRadius + mOutRadius / 4 - dp2px(getContext(), 5));
        mBubblesPath.lineTo(mResultWidth / 2 + mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2 - dp2px(getContext(), 5));
        //畫外部背景
        canvas.drawPath(mBackgroundPath, mBackgroundPaint);
        canvas.drawCircle(mResultWidth / 2, mResultWidth / 2, mOutRadius, mBackgroundPaint);
        Log.d(TAG, "cx: " + mResultWidth / 2);
        //切割畫布,畫水波
        canvas.save();
        mBubblesPath.addCircle(mResultWidth / 2, mResultWidth / 2, mInnerRadius, Path.Direction.CCW);
        //將畫布裁剪成內部氣泡的樣子
        canvas.clipPath(mBubblesPath);

        canvas.drawPath(getPath(), mBubblesPaint);
        canvas.restore();

    }
複製代碼

到這裏已經實現了文章開始時的效果了,文章也該結束了。

結束語

  本文主要是講解怎樣實現水波氣泡,並無講到View的測量,貼出的也只是繪製氣泡的代碼,完整的代碼能夠點擊這裏獲取。

  雖然已經擼出了這個效果,但最後項目中並無用這種動態的氣泡,由於氣泡多的時候是在是卡……。最後,喜歡此demo,就隨手給個star吧!

ps: 歷史文章中有乾貨哦!

本文已由公衆號「AndroidShared」首發

歡迎關注個人公衆號
掃碼關注公衆號,回覆「獲取資料」有驚喜
相關文章
相關標籤/搜索