Android自定義View 水波氣泡

wizardev發表於2018-06-16

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

效果圖預覽

  最後完成的效果圖如下

Android自定義View 水波氣泡

不想看文章的話,可以點選這裡,直接獲取原始碼。

實現方式

步驟拆解

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

  可以將文章開始的時候的效果圖拆分成以下幾部分:

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

拆解之後,就可以按照拆解的步驟來一步步實現了。

畫白色背景

  這裡畫白色背景有以下兩種方式:

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

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

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

Android自定義View 水波氣泡

本文采用的是第二種方式來實現的,具體程式碼如下

//此處程式碼是下部尖角的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);
複製程式碼

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

Android自定義View 水波氣泡

我們會發現氣泡內部的顏色是漸變色,那漸變色是怎麼設定的呢?其實自定義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;
    }
複製程式碼

上面的程式碼畫出的水波如下圖

Android自定義View 水波氣泡

到這裡已經畫出了水波,但現在水波還是靜止的,要讓水波不停的移動,就要新增屬性動畫,新增動畫的程式碼如下

 /**
     * 不斷的更新偏移量,並且迴圈。
     */
    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);

    }
複製程式碼

好了,現在水波已經可以移動了,看下效果

Android自定義View 水波氣泡

what!怎麼成這個樣子了呀,明顯不是我想要的效果呀,肯定是哪裡出錯了,經過我仔細的推敲,總結了出現上面問題的原因,原因如下圖

Android自定義View 水波氣泡

出現上面問題的原因就是因為下面三句程式碼

 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”首發

歡迎關注我的公眾號
掃碼關注公眾號,回覆“獲取資料”有驚喜

相關文章