Android自定義View——從零開始實(shí)現(xiàn)水波浪進(jìn)度框

版權(quán)聲明:本文為博主原創(chuàng)文章,未經(jīng)博主允許不得轉(zhuǎn)載。
系列教程:Android開發(fā)之從零開始系列
源碼:github.com/AnliaLee/Progressbar,歡迎star

大家要是看到有錯(cuò)誤的地方或者有啥好的建議,歡迎留言評(píng)論


前言:相信同行們都知道,我們程序員有一種痛,叫做別人的代碼。讀懂別人的代碼很重要的一點(diǎn)就是要抓住作者的思路,有了思路才能將過程推導(dǎo)出來,否則腦闊會(huì)疼。為己為人,本系列教程博客,我都會(huì)將自己實(shí)現(xiàn)的思路寫下來,帶大家一步步從零開始實(shí)現(xiàn)我們想要的效果。因?yàn)樽罱诰W(wǎng)上看了很多前輩們實(shí)現(xiàn)的 水波浪進(jìn)度框,一時(shí)手癢,所以任性地決定這系列的第二篇博客的主角就是它了

本篇只著重于思路和實(shí)現(xiàn)步驟,里面用到的一些知識(shí)原理不會(huì)非常細(xì)地拿來講,如果有不清楚的api或方法可以在網(wǎng)上搜下相應(yīng)的資料,肯定有大神講得非常清楚的,我這就不獻(xiàn)丑了。本著認(rèn)真負(fù)責(zé)的精神我會(huì)把相關(guān)知識(shí)的博文鏈接也貼出來(其實(shí)就是懶不想寫那么多哈哈),大家可以自行傳送。為了照顧第一次閱讀系列博客的小伙伴,本篇會(huì)出現(xiàn)一些在之前系列博客就講過的內(nèi)容,看過的童鞋自行跳過該段即可

國(guó)際慣例,先來效果展示

目錄
  • 繪制一段波浪(二階貝塞爾曲線)
  • 繪制填充物
  • 測(cè)量及自適應(yīng)View的寬高
  • 讓波浪隨進(jìn)度上升
  • 實(shí)現(xiàn)波浪平移效果
  • 繪制圓形進(jìn)度框背景
  • 自定義attr屬性
  • 擴(kuò)展一:實(shí)現(xiàn)隨進(jìn)度變化的文字效果
  • 擴(kuò)展二:實(shí)現(xiàn)波浪高度隨進(jìn)度上升而下降的效果
  • 擴(kuò)展三:實(shí)現(xiàn)雙波浪效果

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

相關(guān)博文鏈接

【Android - 自定義View】之自定義View淺析
Android 自定義View (一)
Android-貝塞爾曲線
安卓自定義View進(jìn)階:Path基本操作

既然我們實(shí)現(xiàn)的是水波浪進(jìn)度條,那我們就先從波浪效果入手吧。波浪是上下起伏的,也就意味著我們繪制的波浪應(yīng)該是一條上下波動(dòng)的曲線。查閱資料發(fā)現(xiàn)二階貝塞爾曲線足以滿足我們的需求,我們可以通過控制其控制點(diǎn)的坐標(biāo)系y值實(shí)現(xiàn)曲線的上下波動(dòng)。Android中提供了繪制貝塞爾曲線的API及方法,下面我們就試著繪制一條上下波動(dòng)的二階貝塞爾曲線(有關(guān)貝塞爾曲線以及Path方面的知識(shí)已經(jīng)有許多大大講得非常清楚了,這里貼出他們的博客鏈接,就不詳細(xì)展開了)

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);//設(shè)置抗鋸齒
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(getWavePath(),wavePaint);

    }

    private Path getWavePath(){
        wavePath.reset();
        wavePath.moveTo(0,waveHeight);//起始點(diǎn)移動(dòng)至(0,waveHeight),注意坐標(biāo)系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相互轉(zhuǎn)換的工具類(相關(guān)知識(shí)有興趣的可以自己上網(wǎng)搜下),這里也將相關(guān)代碼貼出來

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中進(jìn)行注冊(cè)

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

效果如圖


繪制填充物

相關(guān)博文鏈接

Math取整方法

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

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

實(shí)現(xiàn)代碼如下,修改我們的WaveProgressView

public class WaveProgressView extends View {
    //省略部分代碼...
    private int waveNum;//波浪組的數(shù)量(一次起伏為一組)
    private int defaultSize;//自定義View默認(rèn)的寬高
    private int maxHeight;//為了看到波浪效果,給定一個(gè)比填充物稍高的高度

    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)));//波浪的數(shù)量需要進(jìn)一取整,所以使用Math.ceil函數(shù)
    }

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

        //移動(dòng)到右上方,也就是p0點(diǎn)
        wavePath.moveTo(defaultSize, maxHeight - defaultSize);
        //移動(dòng)到右下方,也就是p1點(diǎn)
        wavePath.lineTo(defaultSize, defaultSize);
        //移動(dòng)到左下邊,也就是p2點(diǎn)
        wavePath.lineTo(0, defaultSize);
        //移動(dòng)到左上方,也就是p3點(diǎn)
        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;
    }
}

測(cè)量及自適應(yīng)View的寬高

相關(guān)博文鏈接

淺談自定義View的寬高獲取
教你搞定Android自定義View

在上面的代碼中,View的寬高是由path區(qū)域的大小決定的,直接寫死在了init()方法中,而我們的實(shí)際需求是View的寬高可以由我們?cè)谕獠窟M(jìn)行設(shè)置。根據(jù)需求,進(jìn)度框是一個(gè)圓形,我們需要將View的寬高強(qiáng)制相等,因此我們重寫View的onMeasure()方法

public class WaveProgressView extends View {
    //省略部分代碼...
    private int viewSize;//重新測(cè)量后View實(shí)際的寬高

    @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最短邊的長(zhǎng)度
        setMeasuredDimension(min, min);// 強(qiáng)制改View為以最短邊為長(zhǎng)度的正方形
        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;
    }
}

讓波浪隨進(jìn)度上升

波浪隨進(jìn)度上升,實(shí)際上就是填充物的高度(p0p1,p3p2的長(zhǎng)度)隨進(jìn)度值的增加而增加。修改我們的WaveProgressView,并添加動(dòng)畫效果

public class WaveProgressView extends View {
    //省略部分代碼...
    private WaveProgressAnim waveProgressAnim;
    private float percent;//進(jìn)度條占比
    private float progressNum;//可以更新的進(jìn)度條數(shù)值
    private float maxNum;//進(jìn)度條最大值

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

    private Path getWavePath(){
        wavePath.reset();
        //移動(dòng)到右上方,也就是p0點(diǎn)
        wavePath.moveTo(viewSize, (1-percent)*viewSize);//讓p0p1的長(zhǎng)度隨percent的增加而增加(注意這里y軸方向默認(rèn)是向下的)
        //移動(dòng)到右下方,也就是p1點(diǎn)
        wavePath.lineTo(viewSize, viewSize);
        //移動(dòng)到左下邊,也就是p2點(diǎn)
        wavePath.lineTo(0, viewSize);
        //移動(dòng)到左上方,也就是p3點(diǎn)
        wavePath.lineTo(0, (1-percent)*viewSize);//讓p3p2的長(zhǎng)度隨percent的增加而增加(注意這里y軸方向默認(rèn)是向下的)
        //從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();
        }
    }

    /**
     * 設(shè)置進(jìn)度條數(shù)值
     * @param progressNum 進(jìn)度條數(shù)值
     * @param time 動(dòng)畫持續(xù)時(shí)間
     */
    public void setProgressNum(float progressNum, int time) {
        this.progressNum = progressNum;

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

在Activity中調(diào)用setProgressNum()方法

waveProgressView.setProgressNum(80,3000);

效果如圖


實(shí)現(xiàn)波浪平移效果

上一小節(jié)我們實(shí)現(xiàn)的波浪上升的動(dòng)畫,這一節(jié)中我們要為波浪添加一個(gè)循環(huán)向左平移的效果

讓波浪向左平移,我們將其可以理解為繪制波浪曲線的起點(diǎn)不斷向左移動(dòng),而循環(huán)則是當(dāng)起點(diǎn)移動(dòng)一段距離后又回到原來的位置重新向左移動(dòng)。通過之前的分析我們知道波浪曲線的繪制起點(diǎn)是p3,因此整個(gè)波浪的平移效果我們只需要通過修改p3的位置即可實(shí)現(xiàn)

但僅僅是這樣還不夠,我們之前整段波浪曲線的寬度和View(正方形目標(biāo)區(qū)域)的寬度是相等的,如果我們僅僅只是讓p3向左平移,會(huì)出現(xiàn)曲線不能鋪滿目標(biāo)區(qū)域的情況,曲線與p0則會(huì)以默認(rèn)的直線進(jìn)行連接。有2D橫向游戲開發(fā)經(jīng)驗(yàn)的小伙伴對(duì)于這種橫向背景循環(huán)的效果會(huì)很熟悉,一般的處理手段是將至少兩個(gè)相同的背景圖片拼接起來,當(dāng)角色從第一個(gè)背景圖片最左端出發(fā),向右移動(dòng)了第一個(gè)背景圖片寬度的距離時(shí),將角色重新放回到第一個(gè)背景圖片的最左端,這樣就能實(shí)現(xiàn)背景圖片循環(huán)的效果。參考這種手段,對(duì)于我們波浪循環(huán)平移來說,p3就相當(dāng)于角色,波浪曲線相當(dāng)于背景圖片,p3點(diǎn)平移的最大距離為原來一整段曲線的寬度(目標(biāo)區(qū)域的寬度),整段曲線的寬度也變成原來的兩倍(至少兩倍)。為了讓大家更清楚地了解整個(gè)過程,我修改了View寬度的測(cè)量邏輯給大家看下效果(波浪到達(dá)最大高度后高度不再改變,僅進(jìn)行平移循環(huán))

然后下面是我們實(shí)際要實(shí)現(xiàn)的效果

實(shí)現(xiàn)代碼如下,修改我們的WaveProgressView

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

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

    private Path getWavePath(){
        //省略部分代碼...
        //移動(dòng)到左上方,也就是p3點(diǎn)(x軸默認(rèn)方向是向右的,我們要向左平移,因此設(shè)為負(fù)值)
        //wavePath.lineTo(0, (1-percent)*viewSize);
        wavePath.lineTo(-waveMovingDistance, (1-percent)*viewSize);
        //從p3開始向p0方向繪制波浪曲線(曲線寬度為原來的兩倍也就是波浪數(shù)量*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);
            //波浪高度到達(dá)最大值后就不需要循環(huán)了,只需讓波浪曲線平移循環(huán)即可
            if(percent < progressNum / maxNum){
                percent = interpolatedTime * progressNum / maxNum;
            }
            waveMovingDistance = interpolatedTime * waveNum * waveWidth * 2;
            postInvalidate();
        }
    }

    /**
     * 設(shè)置進(jìn)度條數(shù)值
     * @param progressNum 進(jìn)度條數(shù)值
     * @param time 動(dòng)畫持續(xù)時(shí)間
     */
    public void setProgressNum(float progressNum, int time) {
        //省略部分代碼...
        waveAnim.setRepeatCount(Animation.INFINITE);//讓動(dòng)畫無限循環(huán)
        waveAnim.setInterpolator(new LinearInterpolator());//讓動(dòng)畫勻速播放,不然會(huì)出現(xiàn)波浪平移停頓的現(xiàn)象
    }
}

如果需要讓波浪到達(dá)最高處后平移的速度改變,給動(dòng)畫設(shè)置監(jiān)聽即可

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

繪制圓形進(jìn)度框背景

相關(guān)博文鏈接

android 自定義view 緩存技術(shù)
Android中Canvas繪圖之PorterDuffXfermode使用及工作原理詳解
Android 自定義View學(xué)習(xí)(五)——Paint 關(guān)于PorterDuffXfermode學(xué)習(xí)

終于要開始繪制進(jìn)度框了,之所以要將進(jìn)度框放到后面來講,不僅是因?yàn)檫@部分比較簡(jiǎn)單,而且按照這樣一個(gè)順序去思考設(shè)計(jì)對(duì)于初學(xué)者來說會(huì)更加友好,畢竟是從零開始的教程嘛(所以給個(gè)贊唄?乛?乛?)。好了,一番自夸之后我們進(jìn)入正題,按照需求,我們不僅要繪制圓形進(jìn)度框作為背景,還需要取進(jìn)度框和波浪填充物的交集部分繪制到進(jìn)度框中,這里用到了PorterDuffXfermode方面的知識(shí)(有不了解的童鞋可以通過上面的博客鏈接傳送過去看看),我們繼續(xù)修改WaveProgressView,只需要加多幾行代碼就可以了

public class WaveProgressView extends View {
    //省略部分代碼...
    private Paint circlePaint;//圓形進(jìn)度框畫筆

    private Bitmap bitmap;//緩存bitmap
    private Canvas bitmapCanvas;

    private void init(Context context,AttributeSet attrs){
        //省略部分代碼...
        wavePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));//根據(jù)繪制順序的不同選擇相應(yīng)的模式即可
        
        circlePaint = new Paint();
        circlePaint.setColor(Color.GRAY);
        circlePaint.setAntiAlias(true);//設(shè)置抗鋸齒
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //這里用到了緩存技術(shù)
        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);
    }
}

效果如圖

同樣的,如果想要用其他圖片作為背景進(jìn)度框,也可以按照這樣的思路進(jìn)行擴(kuò)展,這留給小伙伴們自己去研究,就不展開說啦(如果用不規(guī)則圖片作為背景時(shí)記得要重新測(cè)量View的大小


自定義attr屬性

相關(guān)博文鏈接

Android自定義View(二、深入解析自定義屬性)
解析:TypedArray 為什么需要調(diào)用recycle()

我們的View中有許多屬性需要在布局文件中進(jìn)行設(shè)置,這需要我們自己進(jìn)行自定義,實(shí)現(xiàn)過程如下

首先在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;//背景進(jìn)度框顏色

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

在布局文件中設(shè)置自定義屬性試試效果

<!--省略部分代碼-->
<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>

效果如圖

到這里我們的水波浪進(jìn)度框的基礎(chǔ)框架已經(jīng)搭建完畢,下面是在這基礎(chǔ)上進(jìn)行擴(kuò)展


擴(kuò)展一:實(shí)現(xiàn)隨進(jìn)度變化的文字效果

根據(jù)需求,我們需要顯示可以隨進(jìn)度變化的文字,網(wǎng)上許多實(shí)現(xiàn)的方法都是在自定義View中實(shí)現(xiàn)相應(yīng)的文字處理邏輯,然后使用canvas.drawText()方法去繪制文字。我個(gè)人覺得這樣寫比較麻煩且可擴(kuò)展性不高,下面提供另外一種思路供大家參考

我的做法是將條形進(jìn)度條和文字顯示區(qū)分開來,文字顯示的組件直接在布局文件用TextView就可以了,將TextView傳入WaveProgressView,然后在WaveProgressView提供接口編寫文字處理的邏輯即可。這樣實(shí)現(xiàn)的好處在于后期我們要是想改變文字的字體、樣式、位置等等都不需要再在WaveProgressView中傷筋動(dòng)骨地去改,實(shí)現(xiàn)了文字與進(jìn)度框控件解耦

具體實(shí)現(xiàn)如下,修改我們的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));
                }
            }
        }
    }

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

    public interface OnAnimationListener {
        /**
         * 如何處理要顯示的文字內(nèi)容
         * @param interpolatedTime 從0漸變成1,到1時(shí)結(jié)束動(dòng)畫
         * @param updateNum 進(jìn)度條數(shù)值
         * @param maxNum 進(jìn)度條最大值
         * @return
         */
        String howToChangeText(float interpolatedTime, float updateNum, float maxNum);
    }

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

然后在Activity中調(diào)用接口

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

布局文件也相應(yīng)修改

<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>

來看下效果


擴(kuò)展二:實(shí)現(xiàn)波浪高度隨進(jìn)度上升而下降的效果

如果已經(jīng)理解之前所講的波浪繪制以及接口擴(kuò)展的原理,相信實(shí)現(xiàn)起來是非常簡(jiǎn)單的,這里就不詳細(xì)解釋了,大家看代碼即可

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 進(jìn)度占比
         * @param waveHeight 波浪高度
         * @return
         */
        float howToChangeWaveHeight(float percent, float waveHeight);
    }
}

然后在Activity中調(diào)用接口

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

效果如圖


擴(kuò)展三:實(shí)現(xiàn)雙波浪效果

我們繪制第二層波浪要與第一層波浪平移的方向相反,只需要改一下path的繪制順序就可以了。即初始點(diǎn)變?yōu)?strong>p3,p0p3段繪制波浪曲線,則繪制順序如下圖(哈哈又是這張圖,重復(fù)利用)所示

最后將相應(yīng)的path繪制到我們的緩存區(qū)即可(注意繪制的先后順序),實(shí)現(xiàn)代碼如下

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);//設(shè)置抗鋸齒
        //因?yàn)橐采w在第一層波浪上,且要讓半透明生效,所以選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();
        //移動(dòng)到左上方,也就是p3點(diǎn)
        wavePath.moveTo(0, (1-percent)*viewSize);
        //移動(dòng)到左下方,也就是p2點(diǎn)
        wavePath.lineTo(0, viewSize);
        //移動(dòng)到右下方,也就是p1點(diǎn)
        wavePath.lineTo(viewSize, viewSize);
        //移動(dòng)到右上方,也就是p0點(diǎn)
        wavePath.lineTo(viewSize + waveMovingDistance, (1-percent)*viewSize);

        //從p0開始向p3方向繪制波浪曲線(注意繪制二階貝塞爾曲線控制點(diǎn)和終點(diǎn)x坐標(biāo)的正負(fù)值)
        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中設(shè)置isDrawSecondWave為true

waveProgressView.setDrawSecondWave(true);

效果如圖

至此本篇從零開始實(shí)現(xiàn)的教程就告一段落了,如果大家看了感覺還不錯(cuò)麻煩點(diǎn)個(gè)贊,你們的支持是我最大的動(dòng)力~要是小伙伴們想要擴(kuò)展一些新的功能,也可以在評(píng)論區(qū)給我留言,我有空會(huì)把新功能的實(shí)現(xiàn)教程更新上去


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,619評(píng)論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,155評(píng)論 3 425
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,635評(píng)論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,539評(píng)論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,255評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,646評(píng)論 1 326
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,655評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,838評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,399評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,146評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,338評(píng)論 1 372
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,893評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,565評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,983評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,257評(píng)論 1 292
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,059評(píng)論 3 397
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,296評(píng)論 2 376

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,701評(píng)論 25 708
  • 每次聽到某大牛談?wù)撟远xView,頓時(shí)敬佩之心,如滔滔江水連綿不絕,心想我什么時(shí)候能有如此境界,好了,心動(dòng)不如行動(dòng)...
    Code4Android閱讀 2,179評(píng)論 6 25
  • 內(nèi)容抽屜菜單ListViewWebViewSwitchButton按鈕點(diǎn)贊按鈕進(jìn)度條TabLayout圖標(biāo)下拉刷新...
    皇小弟閱讀 46,859評(píng)論 22 665
  • 一直以來,將最美的情愫注入情愛,原以為,我會(huì)是一個(gè)不能缺少愛情的女子,心思細(xì)膩的等待攜手看花之人。 愛上的其實(shí)只是...
    梅園遺珠閱讀 460評(píng)論 0 4
  • 首先來看一下百度百科對(duì)特異功能的定義: 特異功能是未經(jīng)科學(xué)證明的想象中的人類潛能,當(dāng)今科學(xué)界并未找到確鑿證據(jù)。這種...
    周口生活網(wǎng)閱讀 462評(píng)論 0 0