1. 程式人生 > >Android自定義View——從零開始實現水波浪進度框

Android自定義View——從零開始實現水波浪進度框

前言:相信同行們都知道,我們程式設計師有一種痛,叫做別人的程式碼。讀懂別人的程式碼很重要的一點就是要抓住作者的思路,有了思路才能將過程推匯出來,否則腦闊會疼。為己為人,本系列教程部落格,我都會將自己實現的思路寫下來,帶大家一步步從零開始實現我們想要的效果。因為最近在網上看了很多前輩們實現的 水波浪進度框,一時手癢,所以任性地決定這系列的第二篇部落格的主角就是它了

本篇只著重於思路和實現步驟,裡面用到的一些知識原理不會非常細地拿來講,如果有不清楚的api或方法可以在網上搜下相應的資料,肯定有大神講得非常清楚的,我這就不獻醜了。本著認真負責的精神我會把相關知識的博文連結也貼出來(其實就是懶不想寫那麼多哈哈),大家可以自行傳送。為了照顧第一次閱讀系列部落格的小夥伴,本篇會出現一些在之前

系列部落格就講過的內容,看過的童鞋自行跳過該段即可

國際慣例,先來效果展示

目錄
  • 繪製一段波浪(二階貝塞爾曲線)
  • 繪製填充物
  • 測量及自適應View的寬高
  • 讓波浪隨進度上升
  • 實現波浪平移效果
  • 繪製圓形進度框背景
  • 自定義attr屬性
  • 擴充套件一:實現隨進度變化的文字效果
  • 擴充套件二:實現波浪高度隨進度上升而下降的效果
  • 擴充套件三:實現雙波浪效果

繪製一段波浪(二階貝塞爾曲線)

既然我們實現的是水波浪進度條,那我們就先從波浪效果入手吧。波浪是上下起伏的,也就意味著我們繪製的波浪應該是一條上下波動的曲線。查閱資料發現二階貝塞爾曲線

足以滿足我們的需求,我們可以通過控制其控制點的座標系y值實現曲線的上下波動。Android中提供了繪製貝塞爾曲線的API及方法,下面我們就試著繪製一條上下波動的二階貝塞爾曲線(有關貝塞爾曲線以及Path方面的知識已經有許多大大講得非常清楚了,這裡貼出他們的部落格連結,就不詳細展開了)

public class WaveProgressView extends View {
    private Paint wavePaint;//繪製波浪畫筆
    private Path wavePath;//繪製波浪Path

    private float waveWidth;//波浪寬度
    private
float waveHeight;//波浪高度 public WaveProgressView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(context,attrs); } private void init(Context context,AttributeSet attrs){ waveWidth = DpOrPxUtils.dip2px(context,15); waveHeight = DpOrPxUtils.dip2px(context,20); wavePath = new Path(); wavePaint = new Paint(); wavePaint.setColor(Color.GREEN); wavePaint.setAntiAlias(true);//設定抗鋸齒 } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawPath(getWavePath(),wavePaint); } private Path getWavePath(){ wavePath.reset(); wavePath.moveTo(0,waveHeight);//起始點移動至(0,waveHeight),注意座標系y軸是向下的 for (int i=0;i<5;i++){ wavePath.rQuadTo(waveWidth/2, waveHeight, waveWidth, 0); wavePath.rQuadTo(waveWidth/2, -waveHeight, waveWidth, 0); } return wavePath; } }

其中用到了dp和px相互轉換的工具類(相關知識有興趣的可以自己上網搜下),這裡也將相關程式碼貼出來

public class DpOrPxUtils {
    public static int dip2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }
    public static int px2dip(Context context, float pxValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (pxValue / scale + 0.5f);
    }
}

介面佈局:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <com.anlia.progressbar.WaveProgressView
            android:id="@+id/wave_progress"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:layout_marginLeft="20dp"/>
    </LinearLayout>
</RelativeLayout>

在Activity中進行註冊

waveProgressView = (WaveProgressView) findViewById(R.id.wave_progress);

效果如圖

繪製填充物

根據我們的需求,我們要模擬出進度框中水位隨著進度的增加而不斷上升的效果。我們將水看作是一種填充物,然後將填充物劃分成最上層的波浪曲線區域以及下層的矩形區域。我們可以利用path.lineTo()path.close()方法將波浪曲線和矩形組裝封閉起來,最終效果如圖

path繪製的順序如下圖所示(初始點為p0,p3至p0段繪製波浪曲線)

實現程式碼如下,修改我們的WaveProgressView

public class WaveProgressView extends View {
    //省略部分程式碼...
    private int waveNum;//波浪組的數量(一次起伏為一組)
    private int defaultSize;//自定義View預設的寬高
    private int maxHeight;//為了看到波浪效果,給定一個比填充物稍高的高度

    private void init(Context context,AttributeSet attrs){
        //省略部分程式碼...
        waveWidth = DpOrPxUtils.dip2px(context,20);
        waveHeight = DpOrPxUtils.dip2px(context,10);
        defaultSize = DpOrPxUtils.dip2px(context,200);
        maxHeight = DpOrPxUtils.dip2px(context,250);
        waveNum =(int) Math.ceil(Double.parseDouble(String.valueOf(defaultSize / waveWidth / 2)));//波浪的數量需要進一取整,所以使用Math.ceil函式
    }

    private Path getWavePath(){
        wavePath.reset();

        //移動到右上方,也就是p0點
        wavePath.moveTo(defaultSize, maxHeight - defaultSize);
        //移動到右下方,也就是p1點
        wavePath.lineTo(defaultSize, defaultSize);
        //移動到左下邊,也就是p2點
        wavePath.lineTo(0, defaultSize);
        //移動到左上方,也就是p3點
        wavePath.lineTo(0, maxHeight - defaultSize);

        //從p3開始向p0方向繪製波浪曲線
        for (int i=0;i<waveNum;i++){
            wavePath.rQuadTo(waveWidth/2, waveHeight, waveWidth, 0);
            wavePath.rQuadTo(waveWidth/2, -waveHeight, waveWidth, 0);
        }

        //將path封閉起來
        wavePath.close();
        return wavePath;
    }
}

測量及自適應View的寬高

在上面的程式碼中,View的寬高是由path區域的大小決定的,直接寫死在了init()方法中,而我們的實際需求是View的寬高可以由我們在外部進行設定。根據需求,進度框是一個圓形,我們需要將View的寬高強制相等,因此我們重寫View的onMeasure()方法

public class WaveProgressView extends View {
    //省略部分程式碼...
    private int viewSize;//重新測量後View實際的寬高

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int height = measureSize(defaultSize, heightMeasureSpec);
        int width = measureSize(defaultSize, widthMeasureSpec);
        int min = Math.min(width, height);// 獲取View最短邊的長度
        setMeasuredDimension(min, min);// 強制改View為以最短邊為長度的正方形
        viewSize = min;
        waveNum =(int) Math.ceil(Double.parseDouble(String.valueOf(viewSize / waveWidth / 2)));
    }

    private int measureSize(int defaultSize,int measureSpec) {
        int result = defaultSize;
        int specMode = View.MeasureSpec.getMode(measureSpec);
        int specSize = View.MeasureSpec.getSize(measureSpec);

        if (specMode == View.MeasureSpec.EXACTLY) {
            result = specSize;
        } else if (specMode == View.MeasureSpec.AT_MOST) {
            result = Math.min(result, specSize);
        }
        return result;
    }
}

讓波浪隨進度上升

波浪隨進度上升,實際上就是填充物的高度(p0p1,p3p2的長度)隨進度值的增加而增加。修改我們的WaveProgressView,並新增動畫效果

public class WaveProgressView extends View {
    //省略部分程式碼...
    private WaveProgressAnim waveProgressAnim;
    private float percent;//進度條佔比
    private float progressNum;//可以更新的進度條數值
    private float maxNum;//進度條最大值

    private void init(Context context,AttributeSet attrs){
        //省略部分程式碼...
        percent = 0;
        progressNum = 0;
        maxNum = 100;
        waveProgressAnim = new WaveProgressAnim();
    }

    private Path getWavePath(){
        wavePath.reset();
        //移動到右上方,也就是p0點
        wavePath.moveTo(viewSize, (1-percent)*viewSize);//讓p0p1的長度隨percent的增加而增加(注意這裡y軸方向預設是向下的)
        //移動到右下方,也就是p1點
        wavePath.lineTo(viewSize, viewSize);
        //移動到左下邊,也就是p2點
        wavePath.lineTo(0, viewSize);
        //移動到左上方,也就是p3點
        wavePath.lineTo(0, (1-percent)*viewSize);//讓p3p2的長度隨percent的增加而增加(注意這裡y軸方向預設是向下的)
        //從p3開始向p0方向繪製波浪曲線
        for (int i=0;i<waveNum;i++){
            wavePath.rQuadTo(waveWidth/2, waveHeight, waveWidth, 0);
            wavePath.rQuadTo(waveWidth/2, -waveHeight, waveWidth, 0);
        }
        //將path封閉起來
        wavePath.close();
        return wavePath;
    }

    public class WaveProgressAnim extends Animation {
        public WaveProgressAnim(){}
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            super.applyTransformation(interpolatedTime, t);
            percent = interpolatedTime * progressNum / maxNum;
            postInvalidate();
        }
    }

    /**
     * 設定進度條數值
     * @param progressNum 進度條數值
     * @param time 動畫持續時間
     */
    public void setProgressNum(float progressNum, int time) {
        this.progressNum = progressNum;

        percent = 0;
        waveProgressAnim.setDuration(time);
        this.startAnimation(waveProgressAnim);
    }
}

在Activity中呼叫setProgressNum()方法

waveProgressView.setProgressNum(80,3000);

效果如圖

實現波浪平移效果

上一小節我們實現的波浪上升的動畫,這一節中我們要為波浪新增一個迴圈向左平移的效果

讓波浪向左平移,我們將其可以理解為繪製波浪曲線的起點不斷向左移動,而迴圈則是當起點移動一段距離後又回到原來的位置重新向左移動。通過之前的分析我們知道波浪曲線的繪製起點是p3,因此整個波浪的平移效果我們只需要通過修改p3的位置即可實現

但僅僅是這樣還不夠,我們之前整段波浪曲線的寬度和View(正方形目標區域)的寬度是相等的,如果我們僅僅只是讓p3向左平移,會出現曲線不能鋪滿目標區域的情況,曲線與p0則會以預設的直線進行連線。有2D橫向遊戲開發經驗的小夥伴對於這種橫向背景迴圈的效果會很熟悉,一般的處理手段是將至少兩個相同的背景圖片拼接起來,當角色從第一個背景圖片最左端出發,向右移動了第一個背景圖片寬度的距離時,將角色重新放回到第一個背景圖片的最左端,這樣就能實現背景圖片迴圈的效果。參考這種手段,對於我們波浪迴圈平移來說,p3就相當於角色,波浪曲線相當於背景圖片,p3點平移的最大距離為原來一整段曲線的寬度(目標區域的寬度),整段曲線的寬度也變成原來的兩倍(至少兩倍)。為了讓大家更清楚地瞭解整個過程,我修改了View寬度的測量邏輯給大家看下效果(波浪到達最大高度後高度不再改變,僅進行平移迴圈)

然後下面是我們實際要實現的效果

實現程式碼如下,修改我們的WaveProgressView

public class WaveProgressView extends View {
    //省略部分程式碼...
    private float waveMovingDistance;//波浪平移的距離

    private void init(Context context,AttributeSet attrs){
        //省略部分程式碼...
        waveMovingDistance = 0;
    }

    private Path getWavePath(){
        //省略部分程式碼...
        //移動到左上方,也就是p3點(x軸預設方向是向右的,我們要向左平移,因此設為負值)
        //wavePath.lineTo(0, (1-percent)*viewSize);
        wavePath.lineTo(-waveMovingDistance, (1-percent)*viewSize);
        //從p3開始向p0方向繪製波浪曲線(曲線寬度為原來的兩倍也就是波浪數量*2)
        for (int i=0;i<waveNum*2;i++){
            wavePath.rQuadTo(waveWidth/2, waveHeight, waveWidth, 0);
            wavePath.rQuadTo(waveWidth/2, -waveHeight, waveWidth, 0);
        }
    }

    public class WaveProgressAnim extends Animation {
        //省略部分程式碼...
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            super.applyTransformation(interpolatedTime, t);
            //波浪高度到達最大值後就不需要迴圈了,只需讓波浪曲線平移迴圈即可
            if(percent < progressNum / maxNum){
                percent = interpolatedTime * progressNum / maxNum;
            }
            waveMovingDistance = interpolatedTime * waveNum * waveWidth * 2;
            postInvalidate();
        }
    }

    /**
     * 設定進度條數值
     * @param progressNum 進度條數值
     * @param time 動畫持續時間
     */
    public void setProgressNum(float progressNum, int time) {
        //省略部分程式碼...
        waveAnim.setRepeatCount(Animation.INFINITE);//讓動畫無限迴圈
        waveAnim.setInterpolator(new LinearInterpolator());//讓動畫勻速播放,不然會出現波浪平移停頓的現象
    }
}

如果需要讓波浪到達最高處後平移的速度改變,給動畫設定監聽即可

waveProgressAnim.setAnimationListener(new Animation.AnimationListener() {
    @Override
    public void onAnimationStart(Animation animation) {}

    @Override
    public void onAnimationEnd(Animation animation) {}

    @Override
    public void onAnimationRepeat(Animation animation) {
        if(percent == progressNum / maxNum){
            waveProgressAnim.setDuration(8000);
        }
    }
});

繪製圓形進度框背景

終於要開始繪製進度框了,之所以要將進度框放到後面來講,不僅是因為這部分比較簡單,而且按照這樣一個順序去思考設計對於初學者來說會更加友好,畢竟是從零開始的教程嘛(所以給個讚唄๑乛◡乛๑)。好了,一番自誇之後我們進入正題,按照需求,我們不僅要繪製圓形進度框作為背景,還需要取進度框和波浪填充物的交集部分繪製到進度框中,這裡用到了PorterDuffXfermode方面的知識(有不瞭解的童鞋可以通過上面的部落格連結傳送過去看看),我們繼續修改WaveProgressView,只需要加多幾行程式碼就可以了

public class WaveProgressView extends View {
    //省略部分程式碼...
    private Paint circlePaint;//圓形進度框畫筆

    private Bitmap bitmap;//快取bitmap
    private Canvas bitmapCanvas;

    private void init(Context context,AttributeSet attrs){
        //省略部分程式碼...
        wavePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));//根據繪製順序的不同選擇相應的模式即可

        circlePaint = new Paint();
        circlePaint.setColor(Color.GRAY);
        circlePaint.setAntiAlias(true);//設定抗鋸齒
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //這裡用到了快取技術
        bitmap = Bitmap.createBitmap(viewSize, viewSize, Bitmap.Config.ARGB_8888);
        bitmapCanvas = new Canvas(bitmap);
        bitmapCanvas.drawCircle(viewSize/2, viewSize/2, viewSize/2, circlePaint);
        bitmapCanvas.drawPath(getWavePath(),wavePaint);

        canvas.drawBitmap(bitmap, 0, 0, null);
    }
}

效果如圖

同樣的,如果想要用其他圖片作為背景進度框,也可以按照這樣的思路進行擴充套件,這留給小夥伴們自己去研究,就不展開說啦(如果用不規則圖片作為背景時記得要重新測量View的大小

自定義attr屬性

我們的View中有許多屬性需要在佈局檔案中進行設定,這需要我們自己進行自定義,實現過程如下

首先在res\values資料夾中新增attr.xml,為WaveProgressView自定義屬性

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--注意這裡的name要和自定義View的名稱一致,不然在xml佈局中無法引用-->
    <declare-styleable name="WaveProgressView">
        <attr name="wave_color" format="color"></attr>
        <attr name="bg_color" format="color"></attr>

        <attr name="wave_width" format="dimension"></attr>
        <attr name="wave_height" format="dimension"></attr>
    </declare-styleable>
</resources>

修改WaveProgressView,為自定義屬性賦值

public class WaveProgressView extends View {
    //省略部分程式碼...
    private int waveColor;//波浪顏色
    private int bgColor;//背景進度框顏色

    private void init(Context context,AttributeSet attrs){
        //省略部分程式碼...
        TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.WaveProgressView);
        waveWidth = typedArray.getDimension(R.styleable.WaveProgressView_wave_width,DpOrPxUtils.dip2px(context,25));
        waveHeight = typedArray.getDimension(R.styleable.WaveProgressView_wave_height,DpOrPxUtils.dip2px(context,5));
        waveColor = typedArray.getColor(R.styleable.WaveProgressView_wave_color,Color.GREEN);
        bgColor = typedArray.getColor(R.styleable.WaveProgressView_bg_color,Color.GRAY);
        typedArray.recycle();

        wavePaint.setColor(waveColor);

        circlePaint.setColor(bgColor);
    }
}

在佈局檔案中設定自定義屬性試試效果

<!--省略部分程式碼-->
<RelativeLayout 
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <com.anlia.progressbar.CircleBarView
            app:start_angle="135"
            app:sweep_angle="270"
            app:progress_color="@color/red"
            app:bg_color="@color/gray_light"
            app:bar_width="20dp"/>
    </LinearLayout>
</RelativeLayout>

效果如圖

到這裡我們的水波浪進度框的基礎框架已經搭建完畢,下面是在這基礎上進行擴充套件

擴充套件一:實現隨進度變化的文字效果

根據需求,我們需要顯示可以隨進度變化的文字,網上許多實現的方法都是在自定義View中實現相應的文書處理邏輯,然後使用canvas.drawText()方法去繪製文字。我個人覺得這樣寫比較麻煩且可擴充套件性不高,下面提供另外一種思路供大家參考

我的做法是將條形進度條和文字顯示區分開來,文字顯示的元件直接在佈局檔案用TextView就可以了,將TextView傳入WaveProgressView,然後在WaveProgressView提供介面編寫文書處理的邏輯即可。這樣實現的好處在於後期我們要是想改變文字的字型、樣式、位置等等都不需要再在WaveProgressView中傷筋動骨地去改,實現了文字與進度框控制元件解耦

具體實現如下,修改我們的WaveProgressView

public class WaveProgressView extends View {
    //省略部分程式碼...
    private TextView textView;
    private OnAnimationListener onAnimationListener;

    public class WaveProgressAnim extends Animation {
        //省略部分程式碼...
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            super.applyTransformation(interpolatedTime, t);
            if(percent < progressNum / maxNum){
                if(textView !=null && onAnimationListener!=null){
                    textView.setText(onAnimationListener.howToChangeText(interpolatedTime, progressNum,maxNum));
                }
            }
        }
    }

    /**
     * 設定顯示文字的TextView
     * @param textView
     */
    public void setTextView(TextView textView) {
        this.textView = textView;
    }

    public interface OnAnimationListener {
        /**
         * 如何處理要顯示的文字內容
         * @param interpolatedTime 從0漸變成1,到1時結束動畫
         * @param updateNum 進度條數值
         * @param maxNum 進度條最大值
         * @return
         */
        String howToChangeText(float interpolatedTime, float updateNum, float maxNum);
    }

    public void setOnAnimationListener(OnAnimationListener onAnimationListener) {
        this.onAnimationListener = onAnimationListener;
    }
}

然後在Activity中呼叫介面

textProgress = (TextView) findViewById(R.id.text_progress);
waveProgressView.setTextView(textProgress);
waveProgressView.setOnAnimationListener(new WaveProgressView.OnAnimationListener() {
    @Override
    public String howToChangeText(float interpolatedTime, float updateNum, float maxNum) {
        DecimalFormat decimalFormat=new DecimalFormat("0.00");
        String s = decimalFormat.format(interpolatedTime * updateNum / maxNum * 100)+"%";
        return s;
    }
});
waveProgressView.setProgressNum(80,1500);

佈局檔案也相應修改

<RelativeLayout
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:layout_gravity="center_horizontal"
    android:layout_marginTop="10dp">
    <com.anlia.progressbar.WaveProgressView
        android:id="@+id/wave_progress"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:wave_height="8dp"
        app:wave_width="40dp"
        app:wave_color="@color/blue_light"
        app:wave_bg_color="@color/gray_light"/>
    <TextView
        android:id="@+id/text_progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true"
        android:textColor="@color/textColorPrimary"
        android:textSize="13dp"
        android:textStyle="bold"/>
</RelativeLayout>

來看下效果

擴充套件二:實現波浪高度隨進度上升而下降的效果

如果已經理解之前所講的波浪繪製以及介面擴充套件的原理,相信實現起來是非常簡單的,這裡就不詳細解釋了,大家看程式碼即可

public class WaveProgressView extends View {
    //省略部分程式碼...
    private Path getWavePath(){
        //省略部分程式碼...
        float changeWaveHeight = waveHeight;
        if(onAnimationListener!=null){
            changeWaveHeight =
                    onAnimationListener.howToChangeWaveHeight(percent,waveHeight) == 0 && percent < 1
                    ?waveHeight
                    :onAnimationListener.howToChangeWaveHeight(percent,waveHeight);
        }

        //從p3開始向p0方向繪製波浪曲線
        for (int i=0;i<waveNum*2;i++){
            wavePath.rQuadTo(waveWidth/2, changeWaveHeight, waveWidth, 0);
            wavePath.rQuadTo(waveWidth/2, -changeWaveHeight, waveWidth, 0);
        }
    }

    public interface OnAnimationListener {
        //省略部分程式碼...
        /**
         * 如何處理波浪高度
         * @param percent 進度佔比
         * @param waveHeight 波浪高度
         * @return
         */
        float howToChangeWaveHeight(float percent, float waveHeight);
    }
}

然後在Activity中呼叫介面

waveProgressView.setOnAnimationListener(new WaveProgressView.OnAnimationListener() {
    //省略部分程式碼...
    @Override
    public float howToChangeWaveHeight(float percent, float waveHeight) {
        return (1-percent)*waveHeight;
    }
});

效果如圖

擴充套件三:實現雙波浪效果

我們繪製第二層波浪要與第一層波浪平移的方向相反,只需要改一下path的繪製順序就可以了。即初始點變為p3p0p3段繪製波浪曲線,則繪製順序如下圖(哈哈又是這張圖,重複利用)所示

最後將相應的path繪製到我們的快取區即可(注意繪製的先後順序),實現程式碼如下

public class WaveProgressView extends View {
    //省略部分程式碼...
    private int secondWaveColor;//第二層波浪顏色
    private boolean isDrawSecondWave;//是否繪製第二層波浪

    private void init(Context context,AttributeSet attrs){
        //省略部分程式碼...
        secondWaveColor = typedArray.getColor(R.styleable.WaveProgressView_second_wave_color,getResources().getColor(R.color.light));

        secondWavePaint = new Paint();
        secondWavePaint.setColor(secondWaveColor);
        secondWavePaint.setAntiAlias(true);//設定抗鋸齒
        //因為要覆蓋在第一層波浪上,且要讓半透明生效,所以選SRC_ATOP模式
        secondWavePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));

        isDrawSecondWave = false;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        bitmap = Bitmap.createBitmap(viewSize, viewSize, Bitmap.Config.ARGB_8888);
        bitmapCanvas = new Canvas(bitmap);

        bitmapCanvas.drawCircle(viewSize/2, viewSize/2, viewSize/2, circlePaint);
        bitmapCanvas.drawPath(getWavePath(),wavePaint);
        if(isDrawSecondWave){
            bitmapCanvas.drawPath(getSecondWavePath(),secondWavePaint);
        }
        canvas.drawBitmap(bitmap, 0, 0, null);
    }

    private Path getSecondWavePath(){
        float changeWaveHeight = waveHeight;
        if(onAnimationListener!=null){
            changeWaveHeight =
                    onAnimationListener.howToChangeWaveHeight(percent,waveHeight) == 0 && percent < 1
                            ?waveHeight
                            :onAnimationListener.howToChangeWaveHeight(percent,waveHeight);
        }

        wavePath.reset();
        //移動到左上方,也就是p3點
        wavePath.moveTo(0, (1-percent)*viewSize);
        //移動到左下方,也就是p2點
        wavePath.lineTo(0, viewSize);
        //移動到右下方,也就是p1點
        wavePath.lineTo(viewSize, viewSize);
        //移動到右上方,也就是p0點
        wavePath.lineTo(viewSize + waveMovingDistance, (1-percent)*viewSize);

        //從p0開始向p3方向繪製波浪曲線(注意繪製二階貝塞爾曲線控制點和終點x座標的正負值)
        for (int i=0;i<waveNum*2;i++){
            wavePath.rQuadTo(-waveWidth/2, changeWaveHeight, -waveWidth, 0);
            wavePath.rQuadTo(-waveWidth/2, -changeWaveHeight, -waveWidth, 0);
        }

        //將path封閉起來
        wavePath.close();
        return wavePath;
    }

    /**
     * 是否繪製第二層波浪
     * @param isDrawSecondWave
     */
    public void setDrawSecondWave(boolean isDrawSecondWave) {
        this.isDrawSecondWave = isDrawSecondWave;
    }
}

在Activity中設定isDrawSecondWave為true

waveProgressView.setDrawSecondWave(true);

效果如圖

至此本篇從零開始實現的教程就告一段落了,如果大家看了感覺還不錯麻煩點個贊,你們的支援是我最大的動力~要是小夥伴們想要擴充套件一些新的功能,也可以在評論區給我留言,我有空會把新功能的實現教程更新上去