Android教你一步一步從學習貝塞爾曲線到實現波浪進度條

前言

你們好,我是深紅騎士,愛開玩笑,技術一渣渣,熱愛鑽研,這篇文章是今年的最後一篇了,首先祝你們在新的一年裏心想事成,諸事順利。今天來學習貝塞爾曲線,以前一直想學,惋惜沒時間。什麼是貝塞爾曲線呢?一開始我也是不懂的,當查了不少資料,如今仍是不夠了解,其推導公式仍是不能深刻了解。對發佈這曲線的法國工程師皮埃爾·貝塞爾由衷敬佩,貝塞爾曲線,又稱貝茲曲線或者貝濟埃曲線,是應用於二維圖形應用程序的數學曲線.1962年,皮埃爾·貝塞爾運用貝塞爾曲線爲汽車的主體進行設計,貝塞爾曲線最初由Paul de Casteljau於1959年運用de Casteljau算法開發,以穩定的述職方法求出貝塞爾曲線。其實貝塞爾曲線就在咱們平常生活中,如一些成熟的位圖軟件中:PhotoSHop,Flash5等。在前端開發中,貝塞爾曲線也是無處不在:前端2D或者3D圖形圖標庫都會使用貝塞爾曲線;它能夠用來繪製曲線,在svg和canvas中,原生提供的曲線繪製都是用貝塞爾曲線實現的;在css的transition-timing-function屬性,可使用貝塞爾曲線來描述過渡的緩動計算。css

前言貝塞爾演示圖
上面這個動畫是否是很炫,它就是用貝塞爾曲線來實現的。

貝塞爾曲線原理

貝塞爾曲線是用一系列點來控制曲線狀態的,我將這一系列點分爲三個點:起點終點控制點。經過改變這些點,貝塞爾曲線就會發生變化。前端

  • 起點:肯定曲線的起點
  • 終點:肯定曲線的終點
  • 控制點:肯定曲線的控制點

一階曲線原理

一階曲線就是一條直線,只有兩個點,就是起點終點,也就是最終效果就是一條線段。仍是直接上圖比較直觀:java

一階曲線效果圖
一階公式以下:

一階公式推導
那麼上面的公式是怎麼來的呢?爲了方便,我就在紙上寫了,字有點醜,見諒了:

一階公式詳細推導

二階曲線原理

二階曲線由兩個數據點(起始點和終點),一個控制點來描述曲線狀態,以下圖,下面A點是起始點,C是終點,B是控制點。android

二階曲線示意圖(一)
紅線AC是怎麼生成的呢?繼續上圖:

二階曲線示意圖(二)
簡單來看鏈接AB,BC兩條線段,在AB,BC分別取D,E兩點,鏈接DE。以下圖:

二階曲線示意圖(三)
D在AB線段上從A往B作一階曲線運動,E在BC線段上從B往C作一階曲線運動,而F在DE上作一階曲線運動,那麼F這點就是貝塞爾曲線上的一個點,動態圖以下:

二階曲線示意圖(四)
再簡單理解就是:二階貝塞爾曲線就是 起點終點不斷變化的一階貝塞爾曲線。二階公式以下:

二階公式
那麼上面這個公司怎麼推導出來的呢?一樣爲了方便,我就在紙上寫了:

二階公式推導

三階曲線原理

三階曲線其實就是由兩個數據點(起始點和終點),兩個控制點來描述曲線的狀態,以下圖,下面A是起始點,D是終點,B和C是控制點。git

三階曲線示意圖(一)
動態圖以下:

三階曲線示意圖(二)
能夠這麼理解,兩個數據點和控制點不斷變化的二階貝塞爾曲線,即拆分爲p0p1p2和p1p2p3兩個二階貝塞爾曲線。三階公式以下:

三階公式以下
那麼上面這個公式是怎麼推導出來的呢?直接上圖:

三階曲線推導圖
四階,五階的效果圖和推導公式就不上了,原理是同樣的。經過上面一階,二階,三階的推導能夠發現這樣一個規律:沒N階貝塞爾曲線均可以拆分爲兩個N-1階,和高數中二項式展開同樣,就是階數越高,控制點之間就會越近,繪製的曲線就會更加絲滑。通用公式以下:

n階曲線通用公式
把貝塞爾曲線原理弄懂了,下面就能夠用來作實際性的東西了。

Android中的貝塞爾曲線

在Android中,Path類中有四個方法與貝塞爾曲線相關的,也就是已經封裝了關於貝塞爾曲線的函數,開發者直接調用便可:github

//二階貝賽爾
    public void quadTo(float x1, float y1, float x2, float y2);
    public void rQuadTo(float dx1, float dy1, float dx2, float dy2);
    //三階貝賽爾
    public void cubicTo(float x1, float y1, float x2, float y2,float x3, float y3);
    public void rCubicTo(float x1, float y1, float x2, float y2,float x3, float y3);
複製代碼

上面的四個函數中,quadTo、rQuadTo是二階貝塞爾曲線,cubicTo、rCubicTo是三階貝塞爾曲線。由於三階貝塞爾曲線使用方法和二階貝塞爾曲線類似,用處也不多,就不細說了。下面就針對二階的貝塞爾曲線quadTo、rQuadTo爲詳細說明。算法

quadTo原理

先看看quadTo函數的定義:canvas

/** * Add a quadratic bezier from the last point, approaching control point * (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for * this contour, the first point is automatically set to (0,0). * * @param x1 The x-coordinate of the control point on a quadratic curve * @param y1 The y-coordinate of the control point on a quadratic curve * @param x2 The x-coordinate of the end point on a quadratic curve * @param y2 The y-coordinate of the end point on a quadratic curve */
    public void quadTo(float x1, float y1, float x2, float y2) {
        isSimplePath = false;
        nQuadTo(mNativePath, x1, y1, x2, y2);
    }
複製代碼

看上面的註釋能夠知道:(x1,y1)是控制點,(x2,y2)是終點座標,怎麼沒有起點的座標呢?做爲Android開發者都知道,一條線段的起始點都是經過Path.move(x,y)來指定的。若是連續調用quadTo函數,那麼前一個quadTo的終點就是下一個quadTo函數的起始點,若是初始化沒有調用Path.moveTo(x,y)來指定起始點,那麼控件視圖就會以左上角(0,0)爲起始點,仍是直接上例子描述。 下面實現繪製下面如下效果圖:緩存

效果圖一
下面先經過PhotoShop來模擬畫出上面這條軌跡的輔助控制點的位置:

photoShop演示圖
下面經過草圖分析肯定起始點,終點,控制點的位置, 注意,下面的分析圖位置不是很準備,只是爲了肯定控制點的位置。

開始路徑分析圖
先看p0-p1這條路徑,是以p0爲起始點,p2爲終點,p1爲控制點。起始的座標設置爲(200,400),終點的座標設置(400,400),控制點是在p0,p1的上方,所以縱座標y的值比兩點都要小,橫座標的位置是在p0和p2的中間。那麼p1座標設定爲(300,300);同理,在p2-p4的這條二階貝塞爾曲線上,控制點p3的座標位置應該是(500,500),由於p0-p2,p2-p4這兩條貝塞爾曲線是對稱的。

示例代碼

public class PathView extends View {


    //畫筆
    private Paint paint;
    //路徑
    private Path path;

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

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


    //重寫onDraw方法
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setStyle(Paint.Style.STROKE);
        //線條寬度
        paint.setStrokeWidth(10);
        paint.setColor(Color.RED);
        //設置起始點的位置爲(200,400)
        path.moveTo(200,400);
        //線條p0-p2控制點(300,300) 終點位置(400,400)
        path.quadTo(300,300,400,400);
        //線條p2-p4控制點(500,500) 終點位置(600,400)
        path.quadTo(500,500,600,400);
        canvas.drawPath(path, paint);

    }


    private void init() {
        paint = new Paint();
        path = new Path();
    }
}
複製代碼

佈局文件以下:app

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#000000">



    <Button
        android:id="@+id/btn_reset"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_marginTop="10dp"
        android:text="清空路徑"
        />

    <com.example.okhttpdemo.PathView
        android:id="@+id/path_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@id/btn_reset"
        android:background="#000000"/>


</android.support.constraint.ConstraintLayout>
複製代碼

效果圖以下圖所示:

線段一效果圖
下面把 path.moveTo(200,400);註釋,再看看效果:

去掉指定起始點的效果圖
經過上面的簡單例子,能夠得出如下兩點:

  • 當連續調用quadTo函數時,前一個quadTo函數的終點就是調用下一個quadTo函數的起始點。
  • 貝塞爾曲線的起點是經過Path.moveTo(x,y)來指定的,若是一開始沒有調用Path.move(x,y),則會取控件的左上角(0,0)做爲起點。

Path.lineTo和Path.quadTo的區別

下面來看看Path.lineTo和Path.quadTo的區別,Path.lineTo是鏈接直線,是鏈接上一個點到當前點的之間的直線,下面來實現繪製手指在屏幕上所走的路徑,也不難就在上面的基礎上增長onTouchEvent方法便可,代碼以下:

public class PathView extends View {


    //畫筆
    private Paint paint;
    //路徑
    private Path path;

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

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


    //重寫onDraw方法
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setStyle(Paint.Style.STROKE);
        //線條寬度
      // paint.setStrokeWidth(10);
        paint.setColor(Color.RED);
        canvas.drawPath(path, paint);

    }


    private void init() {
        paint = new Paint();
        path = new Path();
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                Log.d("ssd","觸發按下");
                path.moveTo(event.getX(), event.getY());
                return true;
            }
            case MotionEvent.ACTION_MOVE:
                Log.d("ssd","觸發移動");
                path.lineTo(event.getX(), event.getY());
                invalidate();
                break;
            default:
                break;

        }
        return super.onTouchEvent(event);
    }


    public void reset() {
        path.reset();
        invalidate();
    }



}
複製代碼

直接上效果圖:

手指軌跡Path.LineTo實現
當用戶點擊屏幕時,首先觸發的是 MotionEvent.DOWN這個條件,而後調用 path.move(event.getX(),event.getY()),當用戶移動手指時,就用 path.lineTo(event.getX,event.getY())將各個點鏈接起來,而後調用 invalidate從新繪製。這裏簡單說一下在 MotionEvent.ACTION_DOWN爲何要返回 return truereturn true表示當前的控件已經消費了按下事件,剩下的 ACTION_UPACTION_MOVE都會被執行;若是在 case MotionEvent.ACTION_DOWN下返回 return false,後續的 MOTION_MOVEMMOTION_UP 都不會被接收到,由於沒有消費 ACTION_DOWN,系統就會認爲 ACTION_DOWN沒有發生過,因此 ACTION_MOVEACTION_UP就不能捕獲,下面把圖放大仔細看:

放大的圖細節
把C放大後,很明顯看出,這個C字不是很平滑,像是不少一折一折的線段構成,出現這樣的緣由也很簡單分析出來,由於這個C字是由各個不一樣點之間連線構成的,之間就沒有平滑過渡,若是橫縱座標變化劇烈時,更加突出有摺痕效果。若是要解決這問題,這時候二階曲線的做用體現出來了。

線段轉折分析
上圖中,有三個黑點連成兩條直線,從兩個線段能夠看出,若是使用Path.lineTo的時候,是直接把觸摸點p0,p1,p2鏈接起來,那麼如今要實現這個三個點之間的流暢過渡,我想到的就是把這 兩條線的中點分別做爲起點和終點,把鏈接這兩條線段的點(p1)做爲 控制點,那這樣就能解決上面摺痕的問題,直接上效果圖:

解決摺痕點
可是上面會有這樣一個問題:當你繪製二階曲線的時候,結束的時候,最開始那段線段的前半部分也就是p0-p3,最後那段線段的後半部分也就是p4-p2不會繪製出來。其實這兩端距離能夠忽略不計,由於手指滑動的時候,所產生的點與點之間的距離是很小的,所以p0-p3,p4-p2的距離能夠忽略不算了。每一個圖形中,確定有不少點共同鏈接線段,而如今就是將兩個線段的中間作爲二階曲線的起點和終點,把線段與線段之間的轉折點作爲控制點,這樣來組成平滑的連線。理論上應該能夠,那就下面之間敲代碼:

@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                Log.d("ssd","觸發按下");
                path.moveTo(event.getX(), event.getY());
                //保存這個點的座標
                mBeforeX = event.getX();
                mBeforeY = event.getY();
                return true;
            }
            case MotionEvent.ACTION_MOVE:
                Log.d("ssd","觸發移動");
                //移動的時候繪製二階曲線
                //終點是線段的中點
                endX = (mBeforeX + event.getX()) / 2;
                endY = (mBeforeY + event.getY()) / 2;
                //繪製二階曲線
                path.quadTo(mBeforeX,mBeforeY,endX,endY);
                //而後更新前一點的座標
                mBeforeX = event.getX();
                mBeforeY = event.getY();
                invalidate();
                break;
            default:
                break;

        }
        return super.onTouchEvent(event);
    }

複製代碼

這裏簡單說明一下,在ACTION_DOWN的時候,先調用path.moveTo(event.getX(), event.getY());設置曲線的初始位置就是手指觸屏的位置,上面也解釋了若是不調用moveTo(event.getX(), event.getY())的話,那麼繪製點就會從控件的(0,0)開始。用mBeforeXmBeforeY記錄手指移動的前一個橫縱座標,而這個點是作控制點,最後返回return true爲了讓ACTION_MOVEACTION_UP向本控件傳遞。下面說說在ACTION_MOVE方法的邏輯處理,首先是肯定結束點,上面也說告終束點是線段的中間位置,因此用了兩條公式來endX = (mBeforeX + event.getX()) / 2;endY = (mBeforeY + event.getY()) / 2;求這個中間位置的橫縱座標,而控制點就是上個手指觸摸屏幕的位置,後面就是更新前一個手指座標。這裏注意一下,上面也說了當連續調用quardTo的時候,第一個起始點是Path.moveTo(x,y)來設置的,其餘部分,前面調用quadTo的終點是下一個quard的起點,這裏所說的起始點就是上一個線段的中間點。上面的邏輯用一句話表示:把各個線段的中間點做爲起始點和終點,把前一個手指位置做爲控制點,最終效果以下:

改良後的C圖
能夠看到經過 quadT實現的曲線會更順滑。

Path.rQuadTo原理

直接看這個函數的說明:

/** * Add a quadratic bezier from the last point, approaching control point * (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for * this contour, the first point is automatically set to (0,0). * * @param x1 The x-coordinate of the control point on a quadratic curve * @param y1 The y-coordinate of the control point on a quadratic curve * @param x2 The x-coordinate of the end point on a quadratic curve * @param y2 The y-coordinate of the end point on a quadratic curve */
    public void quadTo(float x1, float y1, float x2, float y2) {
        isSimplePath = false;
        nQuadTo(mNativePath, x1, y1, x2, y2);
    }
複製代碼
  • x1:控制點的X座標,表示相對於上一個終點X座標的位移值,能夠爲負值,正值表示相加,負值表示相減
  • x2:控制點的Y座標,表示相對於上一個終點Y座標的位移值,能夠爲負值,正值表示相加,負值表示相減
  • x2:終點的X座標,表示相對於上一個終點X座標的位移值,能夠爲負值,正值表示相加,負值表示相減
  • y2:終點的Y座標,表示相對一上一個終點Y座標的位移值,能夠爲負值,正值表示相加,負值表示相減 這麼說可能不理解,下面仍是直接舉例子: 若是上一個終點座標是(100,200),若是這時候調用rQuardTo(100,-100,200,200),獲得的控制點座標是(100 + 100,200 - 100 )就是(200,100),獲得的終點座標是(100 + 200,200 + 200)就是(300,400),下面兩段是相等的:
path.moveTo(100,200);
path.quadTo(200,100,300,400);
複製代碼
path.moveTo(100,200);
path.rQuadTo(100,-100,200,200);
複製代碼

在上面中,用quadTo實現了一個波浪線,下圖:

波浪線效果圖
下面是上面用 quadTo實現的代碼:

//重寫onDraw方法
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setStyle(Paint.Style.STROKE);
        //線條寬度
        paint.setStrokeWidth(10);
        paint.setColor(Color.RED);
        //設置起始點的位置爲(200,400)
        path.moveTo(200,400);
        //線條p0-p2控制點(300,300) 終點位置(400,400)
        path.quadTo(300,300,400,400);
        //線條p2-p4控制點(500,500) 終點位置(600,400)
        path.quadTo(500,500,600,400);
        canvas.drawPath(path, paint);

    }
複製代碼

下面就用rQuadTo來實現這個波浪線,先上分析圖:

開始路徑分析圖
代碼以下:

//重寫onDraw方法
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setStyle(Paint.Style.STROKE);
        //線條寬度
        paint.setStrokeWidth(10);
        paint.setColor(Color.RED);
        //設置起始點的位置爲(200,400)
        path.moveTo(200,400);
        //線條p0-p2控制點(300,300) 終點座標位置(400,400)
        path.rQuadTo(100,-100,200,0);
        //線條p2-p4控制點(500,500) 終點座標位置(600,400)
        path.rQuadTo(100,100,200,0);
        canvas.drawPath(path, paint);

    }
複製代碼

第一行:path.rQuadTo(100,-100,200,0);這個一行代碼是基於(200,400)這個點來計算曲線p0-p2的控制點和終點座標。

  • 控制點X座標 = 上一個終點的X座標 + 控制點X位移值 = 200 + 100 = 300;
  • 控制點Y座標 = 上一個終點的Y座標 + 控制點Y位移值 = 400 - 100 = 300;
  • 終點X座標 = 上一個終點的X座標 + 終點X的位移值 = 200 + 200 = 400;
  • 終點Y座標 = 上一個終點的Y座標 + 終點Y的位移值 = 400 + 0 = 400; 這句和path.quadTo(300,300,400,400)是等價的。

那麼第一條曲線就容易繪製出來了,而且第一條曲線的終點也知道了是(400,400),那麼第二句path.rQuadTo(100,100,200,0)是基於這個終點(400,400)來計算第二條曲線的控制點和終點。

  • 控制點X座標 = 上一個終點的X座標 + 控制點X位移值 = 400 + 100 = 500;
  • 控制點Y座標 = 上一個終點的Y座標 + 控制點Y位移值 = 400 + 100 = 500
  • 終點X座標 = 上一個終點的X座標 + 終點X的位移值 = 400 + 200 = 600;
  • 終點Y座標 = 上一個終點的Y座標 + 終點Y的位移值 = 400 + 0 = 400;

其實這句path.rQuadTo(100,100,200,0);是和path.quadTo(500,500,600,400);相等的,實際運行的效果圖也和用quadTo方法繪製的同樣,經過這個例子,能夠知道quadTo這個方法的參數都是實際結果的座標,而rQuadTo這個方法的參數是以上一個終點位置爲基準來作位移的。

實現封閉波浪

下面要實現如下效果:

滿屏波浪效果

實現靜態封閉波浪

對應代碼以下:

//重寫onDraw方法
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        path.reset();
        //設置填充繪製
        paint.setStyle(Paint.Style.FILL_AND_STROKE);
        //線條寬度
        //paint.setStrokeWidth(10);
        paint.setColor(Color.RED);
        int control = waveLength / 2;
        //首先肯定初始狀態的起點(-400,1200)
        path.moveTo(-waveLength,origY);
        //由於這個整個波浪的的寬度是View寬度加上左右各一個波長
        for(int i = -waveLength;i <= getWidth() + waveLength;i += waveLength){
            path.rQuadTo(control / 2,-70,control,0);
            path.rQuadTo(control / 2,70,control,0);
        }
        path.lineTo(getWidth(),getHeight());
        path.lineTo(0,getHeight());
        path.close();
        canvas.drawPath(path, paint);

    }
複製代碼

下面一行一行分析:

//首先肯定初始狀態的起點(-400,1200)
        path.moveTo(-waveLength,origY);
複製代碼

首先將Path起始位置向左移一個波長,爲了就是後面實現的位移動畫,而後利用循環來畫出屏幕所容下的全部波浪:

for(int i = -waveLength;i <= getWidth() + waveLength;i += waveLength){
            path.rQuadTo(control / 2,-70,control,0);
            path.rQuadTo(control / 2,70,control,0);
        }
複製代碼

這裏我簡單說一下,path.rQuadTo(control / 2,-70,control,0);循環裏的第一行畫的是一個波長的前半部分,下面把數值放進去就很容易理解了,由於waveLength是400,因此control = waveLength / 2就是200,而path.rQuadTo(control / 2,-70,control,0)就是path.rQuadTo(100,-70,200,0),而path.rQuadTo(control / 2,70,control,0)就是path.rQuadTo(100,70,200,0),上面說過rQuadTo的用法了,就再也不敘述,下面直接上分析圖,下面只是分析最左邊的第一個波浪起始點,控制點的座標,其他波浪只是經過循環繪製,就不分析了:

分析左邊第一個波浪
由於須要連續調用兩次 rQuadTo方法才能繪製出一個完整的波浪,因此上面分析須要肯定五個點的位置。這裏注意,上面圖左右有一條線段鏈接底部,造成封閉圖形,由於要填充內部,因此要封閉繪製 paint.setStyle(Paint.Style.FILL_AND_STROKE);。當波浪繪製完成時, path點會在A點,而後用 path.lineTo(getWidth(),getHeight());鏈接A,B點,再調用 path.lineTo(0,getHeight());鏈接B,C點,最後調用 path.close();鏈接初始點就是鏈接C和起始點,這樣滿橫屏的波浪就繪製完成了。

實現位移動畫的波浪

下面實現左右上下位移動畫,這就會有一點點進度條的感受,個人作法很簡單,由於一開始在View的左邊多畫了一個波浪,也就是說,將起始點向右邊移動,而且要移動一個波浪的長度就可讓波紋重合,而後不斷循環便可,簡單來說就是,動畫移動的距離是一個波浪的長度,當移動到最大的距離時設置不斷循環,就會從新繪製波浪的初始狀態。

/** * 動畫位移方法 */
    public void startAnim(){
        //建立動畫實例
        ValueAnimator moveAnimator = ValueAnimator.ofInt(0,waveLength);
        //動畫的時間
        moveAnimator.setDuration(2500);
        //設置動畫次數 INFINITE表示無限循環
        moveAnimator.setRepeatCount(ValueAnimator.INFINITE);
        //設置動畫插值
        moveAnimator.setInterpolator(new LinearInterpolator());
        //添加監聽
        moveAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                moveDistance = (int)animation.getAnimatedValue();
                invalidate();
            }
        });
        //啓動動畫
        moveAnimator.start();
    }
複製代碼

動畫的位移距離是一個波浪的長度,並將位移的距離保存到moveDistance中,而後開始的時候,在moveTo加上這個距離,就能夠了,完整代碼以下:

//重寫onDraw方法
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //把路線清除 從新繪製 必定要加上 否則是矩形
        path.reset();
        //設置填充繪製
        paint.setStyle(Paint.Style.FILL_AND_STROKE);
        //線條寬度
        //paint.setStrokeWidth(10);
        paint.setColor(Color.RED);
        int control = waveLength / 2;
        //首先肯定初始狀態的起點(-400,1200)
        path.moveTo(-waveLength + moveDistance,origY);
        for(int i = -waveLength;i <= getWidth() + waveLength;i += waveLength){
            path.rQuadTo(control / 2,-70,control,0);
            path.rQuadTo(control / 2,70,control,0);
        }
        path.lineTo(getWidth(),getHeight());
        path.lineTo(0,getHeight());
        path.close();
        canvas.drawPath(path, paint);

    }
複製代碼

效果以下:

橫向波浪
上面只是添加橫向移動,下面添加垂直的移動,我這邊爲了方便,垂直移動距離跟橫向距離同樣,很簡單,把初始縱座標一樣減去移動距離,由於是向上移動,因此是要減 path.moveTo(-waveLength + moveDistance,origY - moveDistance);,最後調用如下代碼:

pathView = findViewById(R.id.path_view);
        pathView.startAnim();
複製代碼

效果如上上上圖。通過上面,本身對貝塞爾曲線由初步的瞭解,下面就實現波浪形進度條。

實現波浪進度條

學到了上面的基本知識,那下面就實現一個小例子,就是圓形波浪進度條,最終效果在文章最底部,慣例下面就一步一步來實現。

繪製一段波浪

先繪製一段滿屏的波浪線,繪製原理就不詳細講了,直接上代碼:

/** * Describe : 實現圓形波浪進度條 * Created by Knight on 2019/2/1 * 點滴之行,看世界 **/
public class CircleWaveProgressView extends View {

    //繪製波浪畫筆
    private Paint wavePaint;
    //繪製波浪Path
    private Path wavePath;
    //波浪的寬度
    private float waveLength;
    //波浪的高度
    private float waveHeight;
    public CircleWaveProgressView(Context context) {
        this(context,null);
    }

    public CircleWaveProgressView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public CircleWaveProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }


    /** * 初始化一些畫筆路徑配置 * @param context */
    private void init(Context context){
        //設置波浪寬度
        waveLength = Density.dip2px(context,25);
        //設置波浪高度
        waveHeight = Density.dip2px(context,15);
        wavePath = new Path();
        wavePaint = new Paint();
        wavePaint.setColor(Color.parseColor("#ff7c9e"));
        //設置抗鋸齒
        wavePaint.setAntiAlias(true);
    }


    @Override
    protected void onDraw(Canvas canvas){
        super.onDraw(canvas);
        //繪製波浪線
        canvas.drawPath(paintWavePath(),wavePaint);
    }

    /** * 繪製波浪線 * * @return */
    private Path paintWavePath(){
        //要先清掉路線
        wavePath.reset();
        //起始點移至(0,waveHeight)
        wavePath.moveTo(0,waveHeight);
        for(int i = 0;i < getWidth() ;i += waveLength){
             wavePath.rQuadTo(waveLength / 2,waveHeight,waveLength,0);
             wavePath.rQuadTo(waveLength / 2,-waveHeight,waveLength,0);
        }
        return wavePath;
    }

}

複製代碼

xml佈局文件:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
>
    <com.example.progressbar.CircleWaveProgressView
        android:id="@+id/circle_progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        />

</android.support.constraint.ConstraintLayout>
複製代碼

實際效果以下:

波浪進度條(一)
下面繪製封閉波浪,效果分析圖以下:

繪製封閉靜態波浪

由於圓形進度框中的波浪是隨着進度的增長而不斷上升的,因此波浪是填充物,先繪製波浪,而後用path.lineTopath.close來鏈接封閉起來,構成一個填充圖形,分析以下圖:

波浪進度分析條二
繪製順序是p0-p1-p2-p3,代碼以下:

public class CircleWaveProgressView extends View {

    //繪製波浪畫筆
    private Paint wavePaint;
    //繪製波浪Path
    private Path wavePath;
    //波浪的寬度
    private float waveLength;
    //波浪的高度
    private float waveHeight;
    //波浪組的數量 一個波浪是一低一高
    private int waveNumber;
    //自定義View的波浪寬高
    private int waveDefaultSize;
    //自定義View的最大寬高 就是比波浪高一點
    private int waveMaxHeight;

    public CircleWaveProgressView(Context context) {
        this(context,null);
    }

    public CircleWaveProgressView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public CircleWaveProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }


    /** * 初始化一些畫筆路徑配置 * @param context */
    private void init(Context context){
        //設置波浪寬度
        waveLength = Density.dip2px(context,25);
        //設置波浪高度
        waveHeight = Density.dip2px(context,15);
        //設置自定義View的寬高
        waveDefaultSize = Density.dip2px(context,250);
        //設置自定義View的最大寬高
        waveMaxHeight = Density.dip2px(context,300);
        //Math.ceil(a)返回求不小於a的最小整數
        // 舉個例子:
        // Math.ceil(125.9)=126.0
        // Math.ceil(0.4873)=1.0
        // Math.ceil(-0.65)=-0.0
        //這裏是調整波浪數量 就是View中能容下幾個波浪 用到ceil就是必定讓View徹底能被波浪佔滿 爲循環繪製作準備 分母越小就約精準
        waveNumber = (int) Math.ceil(Double.parseDouble(String.valueOf(waveDefaultSize / waveLength / 2)));
        wavePath = new Path();
        wavePaint = new Paint();
        //設置顏色
        wavePaint.setColor(Color.parseColor("#ff7c9e"));
        //設置抗鋸齒
        wavePaint.setAntiAlias(true);

    }


    @Override
    protected void onDraw(Canvas canvas){
        super.onDraw(canvas);
        //繪製波浪線
        canvas.drawPath(paintWavePath(),wavePaint);
        Log.d("ssd",getWidth()+"");
    }

    /** * 繪製波浪線 * * @return */
    private Path paintWavePath(){
        //要先清掉路線
        wavePath.reset();
        //起始點移至(0,waveHeight)
        wavePath.moveTo(0,waveMaxHeight - waveDefaultSize);
        //最多能繪製多少個波浪
        //其實也能夠用 i < getWidth() ;i+=waveLength來判斷 這個沒那麼完美
        //繪製p0 - p1 繪製波浪線
        for(int i = 0;i < waveNumber ;i ++){
             wavePath.rQuadTo(waveLength / 2,waveHeight,waveLength,0);
             wavePath.rQuadTo(waveLength / 2,-waveHeight,waveLength,0);
        }
        //鏈接p1 - p2
        wavePath.lineTo(waveDefaultSize,waveDefaultSize);
        //鏈接p2 - p3
        wavePath.lineTo(0,waveDefaultSize);
        //鏈接p3 - p0
        wavePath.lineTo(0,waveMaxHeight - waveDefaultSize);
        //封閉起來填充
        wavePath.close();
        return wavePath;
    }

複製代碼

測量自適應View的寬高

在上面中,發現一個問題,就是寬和高都在初始化方法init中定死了,通常來說視圖View的寬高都是在xml文件中定義或者類文件中定義的,那麼就要重寫View的onMeasure方法:

@Override
    protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
         super.onMeasure(widthMeasureSpec,heightMeasureSpec);
        int height = measureSize(waveDefaultSize, heightMeasureSpec);
        int width = measureSize(waveDefaultSize, widthMeasureSpec);
        //獲取View的最短邊的長度
        int minSize = Math.min(height,width);
        //把View改成正方形
        setMeasuredDimension(minSize,minSize);
        //waveActualSize是實際的寬高
        waveActualSize = minSize;
        //Math.ceil(a)返回求不小於a的最小整數
        // 舉個例子:
        // Math.ceil(125.9)=126.0
        // Math.ceil(0.4873)=1.0
        // Math.ceil(-0.65)=-0.0
        //這裏是調整波浪數量 就是View中能容下幾個波浪 用到ceil就是必定讓View徹底能被波浪佔滿 爲循環繪製作準備 分母越小就約精準
        waveNumber = (int) Math.ceil(Double.parseDouble(String.valueOf(waveActualSize / waveLength / 2)));


    }

    /** * 返回指定的值 * @param defaultSize 默認的值 * @param measureSpec 模式 * @return */
    private int measureSize(int defaultSize,int measureSpec) {
        int result = defaultSize;
        int specMode = View.MeasureSpec.getMode(measureSpec);
        int specSize = View.MeasureSpec.getSize(measureSpec);

        //View.MeasureSpec.EXACTLY:若是是match_parent 或者設置定值就
        //View.MeasureSpec.AT_MOST:wrap_content
        if (specMode == View.MeasureSpec.EXACTLY) {
            result = specSize;
        } else if (specMode == View.MeasureSpec.AT_MOST) {
            result = Math.min(result, specSize);
        }
        return result;
    }
複製代碼

上面就很簡單了,就是增長了一個View的實際寬高變量waveActualSize,讓代碼擴展性更強和精確到更高。

繪製波浪上升

下面實現波浪高度隨着進度變化而變化,當進度增長時,波浪高度增高,當進度減小時,波浪高度減小,其實很簡單,也就是p0-p3,p1-p2的高度根據進度變化而變化,並增長動畫,代碼增長以下:

//當前進度值佔總進度值的佔比
    private float currentPercent;
    //當前進度值
    private float currentProgress;
    //進度的最大值
    private float maxProgress;
    //動畫對象
    private WaveProgressAnimat waveProgressAnimat;
    
     /** * 初始化一些畫筆路徑配置 * @param context */
    private void init(Context context){
        //......
        //佔比一開始設置爲0
        currentPercent = 0;
        //進度條進度開始設置爲0
        currentProgress = 0;
        //進度條的最大值設置爲100
        maxProgress = 100;
        //動畫實例化
        waveProgressAnimat = new WaveProgressAnimat();

    }
    
       /** * 繪製波浪線 * * @return */
    private Path paintWavePath(){
        //要先清掉路線
        wavePath.reset();
        //起始點移至(0,waveHeight) p0 -p1 的高度隨着進度的變化而變化
        wavePath.moveTo(0,(1 - currentPercent) * waveActualSize);
        //最多能繪製多少個波浪
        //其實也能夠用 i < getWidth() ;i+=waveLength來判斷 這個沒那麼完美
        //繪製p0 - p1 繪製波浪線
        for(int i = 0;i < waveNumber ;i ++){
             wavePath.rQuadTo(waveLength / 2,waveHeight,waveLength,0);
             wavePath.rQuadTo(waveLength / 2,-waveHeight,waveLength,0);
        }
        //鏈接p1 - p2
        wavePath.lineTo(waveActualSize,waveActualSize);
        //鏈接p2 - p3
        wavePath.lineTo(0,waveActualSize);
        //鏈接p3 - p0 p3-p0d的高度隨着進度變化而變化
        wavePath.lineTo(0,(1 - currentPercent) * waveActualSize);
        //封閉起來填充
        wavePath.close();
        return wavePath;
    }
    
      //新建一個動畫類
    public class WaveProgressAnimat extends Animation{


        //在繪製動畫的過程當中會反覆的調用applyTransformation函數,
        // 每次調用參數interpolatedTime值都會變化,該參數從0漸 變爲1,當該參數爲1時代表動畫結束
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t){
            super.applyTransformation(interpolatedTime, t);
            //更新佔比
            currentPercent = interpolatedTime * currentProgress / maxProgress;
            //從新繪製
            invalidate();

        }
    }

    /** * 設置進度條數值 * @param currentProgress 當前進度 * @param time 動畫持續時間 */
    public void setProgress(float currentProgress,int time){
         this.currentProgress = currentProgress;
         //從0開始變化
         currentPercent = 0;
         //設置動畫時間
         waveProgressAnimat.setDuration(time);
         //當前視圖開啓動畫
         this.startAnimation(waveProgressAnimat);
    }

複製代碼

最後在Activity調用一些代碼:

//進度爲50 時間是2500毫秒
        circleWaveProgressView.setProgress(50,2500);
複製代碼

最終效果以下圖:

波浪直線上升效果

繪製波浪左右平移

上面實現了波浪直線上升的動畫,下面實現波浪平移的動畫,添加左移的效果,這裏想到前面也實現了平移的效果,可是下面實現方式和上面有點出入,簡單來說就是移動p0座標,可是若是移動p0座標會出現波浪不鋪滿整個View的狀況,這裏運用到一種很常見的循環處理辦法。在飛機大戰的背景滾動圖,是兩張背景圖拼接起來,當飛機從第一個背景圖片最底端出發,向上移動了第一個背景圖片高度的距離時,將角色從新放回到第一個背景圖片的最底端,這樣就能實現背景圖片循環的效果。也就是一開始繪製兩端p0-p1,而後隨着進度變化,p0會左移,一開始不在View中的波浪會從右邊往左邊移動出現,當滑動最大距離時,又從新繪製最開始狀態,這樣就達到循環了。仍是先上分析圖:

水平位移分析圖
View的初始狀態是藍色區域,而後通過動畫位移慢慢變成紅色區域,代碼實現以下:

//波浪平移距離
    private float moveDistance = 0;
    
     /** * 繪製波浪線 * * @return */
    private Path paintWavePath(){
        //要先清掉路線
        wavePath.reset();
        //起始點移至(0,waveHeight) p0 -p1 的高度隨着進度的變化而變化
        wavePath.moveTo(-moveDistance,(1 - currentPercent) * waveActualSize);
        //最多能繪製多少個波浪
        //其實也能夠用 i < getWidth() ;i+=waveLength來判斷 這個沒那麼完美
        //繪製p0 - p1 繪製波浪線 這裏有一段是超出View的,在View右邊距的右邊 因此是* 2,爲了水平位移
        for(int i = 0; i < waveNumber * 2 ; i ++){
             wavePath.rQuadTo(waveLength / 2,waveHeight,waveLength,0);
             wavePath.rQuadTo(waveLength / 2,-waveHeight,waveLength,0);
        }
        //鏈接p1 - p2
        wavePath.lineTo(waveActualSize,waveActualSize);
        //鏈接p2 - p3
        wavePath.lineTo(0,waveActualSize);
        //鏈接p3 - p0 p3-p0d的高度隨着進度變化而變化
        wavePath.lineTo(0,(1 - currentPercent) * waveActualSize);
        //封閉起來填充
        wavePath.close();
        return wavePath;
    }
    
    
     /** * 設置進度條數值 * @param currentProgress 當前進度 * @param time 動畫持續時間 */
    public void setProgress(final float currentProgress, int time){
         this.currentProgress = currentProgress;
         //從0開始變化
         currentPercent = 0;
         //設置動畫時間
         waveProgressAnimat.setDuration(time);
         //設置循環播放
         waveProgressAnimat.setRepeatCount(Animation.INFINITE);
         //讓動畫勻速播放,避免出現波浪平移停頓的現象
         waveProgressAnimat.setInterpolator(new LinearInterpolator());
         waveProgressAnimat.setAnimationListener(new Animation.AnimationListener() {
             @Override
             public void onAnimationStart(Animation animation) {

             }

             @Override
             public void onAnimationEnd(Animation animation) {

             }

             @Override
             public void onAnimationRepeat(Animation animation) {
                 //波浪到達最高處後平移的速度改變,給動畫設置監聽便可,當動畫結束後以7000毫秒的時間運行,變慢了
                 if(currentPercent == currentProgress /maxProgress){
                     waveProgressAnimat.setDuration(7000);
                 }
             }
         });
         //當前視圖開啓動畫
         this.startAnimation(waveProgressAnimat);
    }
     //新建一個動畫類
    public class WaveProgressAnimat extends Animation{


        //在繪製動畫的過程當中會反覆的調用applyTransformation函數,
        // 每次調用參數interpolatedTime值都會變化,該參數從0漸 變爲1,當該參數爲1時代表動畫結束
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t){
            super.applyTransformation(interpolatedTime, t);
            //波浪高度達到最高就不用循環,只須要平移
            if(currentPercent < currentProgress / maxProgress){
                currentPercent = interpolatedTime * currentProgress / maxProgress;
            }
            //左移的距離根據動畫進度而改變
            moveDistance = interpolatedTime * waveNumber * waveLength * 2;
            //從新繪製
            invalidate();

        }
    }
    
複製代碼

最後的效果以下圖:

波浪上下左右移動效果圖

繪製圓形外框背景

這裏要用到PorterDuffXfermode的知識,其實也不難,先上PorterDuff.Mode各類模式的效果圖:

圖像合成示意圖
這張圖看起來很正常,但這張圖其實給 開發者形成很大的誤區,看下面這篇博文而且本身動手實踐一下 android PorterDuffXferMode真正的效果測試集合(對比官方demo)。 下面用到了 PorterDuff.Mode.SRC_IN,由於先繪製圓形背景,再繪製波浪線,而 PorterDuff.Mode.SRC_IN模式在二者相交的地方繪製源圖像,而且繪製的效果會受到目標圖像對應地方透明度的影響,看上圖就知道了,代碼以下:

//圓形背景畫筆
    private Paint circlePaint;
    //bitmap
    private Bitmap circleBitmap;
    //bitmap畫布
    private Canvas bitmapCanvas;
    
      /** * 初始化一些畫筆路徑配置 * @param context */
    private void init(Context context){
        //.......
        //繪製圓形背景開始
        wavePaint = new Paint();
        //設置畫筆爲取交集模式
        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);

        Log.d("ssd",getWidth()+"");
        //這裏用到了緩存 根據參數建立新位圖
        circleBitmap = Bitmap.createBitmap(waveActualSize, waveActualSize, Bitmap.Config.ARGB_8888);
        //以該bitmap爲低建立一塊畫布
        bitmapCanvas = new Canvas(circleBitmap);
        //繪製圓形 圓心 直徑都是很簡單得出
        bitmapCanvas.drawCircle(waveActualSize/2, waveActualSize/2, waveActualSize/2, circlePaint);
        //繪製波浪形
        bitmapCanvas.drawPath(paintWavePath(),wavePaint);
        //裁剪圖片
        canvas.drawBitmap(circleBitmap, 0, 0, null);
        //繪製波浪線
      // canvas.drawPath(paintWavePath(),wavePaint);

    }

複製代碼

實際效果以下圖:

最第一版一
簡單的進度條終於出來了~下面繼續完善,到這裏你可能發現,顏色,大小什麼的都在類中定死了,但實際中有不少屬性都要在佈局文件中設置的,同一個View在不一樣場景下狀態多是不同的,因此爲了提升擴展性,在 res\vaules文件下添加 attrs.xml文件,給 CircleWaveProgressView添加自定義屬性,以下:

<!--這裏的名字要和自定義的View名稱同樣,否則在xml佈局中沒法引用-->
    <declare-styleable name="CircleWaveProgressView">
        <!--波浪的顏色-->
        <attr name="wave_color" format="color"></attr>
        <!--圓形背景顏色-->
        <attr name="circlebg_color" format="color"></attr>
        <!--波浪長度-->
        <attr name="wave_length" format="dimension"></attr>
        <!--波浪高度-->
        <attr name="wave_height" format="dimension"></attr>
        <!--當前進度-->
        <attr name="currentProgress" format="float"></attr>
        <!--最大進度-->
        <attr name="maxProgress" format="float"></attr>
    </declare-styleable>

複製代碼

在自定義View爲屬性值賦值:

//波浪顏色
    private int wave_color;
    //圓形背景進度框顏色
    private int circle_bgcolor;
    
    
    public CircleWaveProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //獲取attrs文件下配置屬性
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleWaveProgressView);
        //獲取波浪寬度 第二個參數,若是xml設置這個屬性,則會取設置的默認值 也就是說xml沒有指定wave_length這個屬性,就會取Density.dip2px(context,25)
        waveLength = typedArray.getDimension(R.styleable.CircleWaveProgressView_wave_length,Density.dip2px(context,25));
        //獲取波浪高度
        waveHeight = typedArray.getDimension(R.styleable.CircleWaveProgressView_wave_height,Density.dip2px(context,15));
        //獲取波浪顏色
        wave_color = typedArray.getColor(R.styleable.CircleWaveProgressView_wave_color,Color.parseColor("#ff7c9e"));
        //圓形背景顏色
        circle_bgcolor = typedArray.getColor(R.styleable.CircleWaveProgressView_circlebg_color,Color.GRAY);
        //當前進度
        currentProgress = typedArray.getFloat(R.styleable.CircleWaveProgressView_currentProgress,50);
        //最大進度
        maxProgress = typedArray.getFloat(R.styleable.CircleWaveProgressView_maxProgress,100);
        //記得把TypedArray回收
        //程序在運行時維護了一個 TypedArray的池,程序調用時,會向該池中請求一個實例,用完以後,調用 recycle() 方法來釋放該實例,從而使其可被其餘模塊複用。
        //那爲何要使用這種模式呢?答案也很簡單,TypedArray的使用場景之一,就是上述的自定義View,會隨着 Activity的每一次Create而Create,
        //所以,須要系統頻繁的建立array,對內存和性能是一個不小的開銷,若是不使用池模式,每次都讓GC來回收,極可能就會形成OutOfMemory。
        //這就是使用池+單例模式的緣由,這也就是爲何官方文檔一再的強調:使用完以後必定 recycle,recycle,recycle
        typedArray.recycle();
        init(context);
    }
    
    /** * 初始化一些畫筆路徑配置 * @param context */
    private void init(Context context){
        //設置自定義View的寬高
        waveDefaultSize = Density.dip2px(context,250);
        //設置自定義View的最大寬高
        waveMaxHeight = Density.dip2px(context,300);

        wavePath = new Path();
        wavePaint = new Paint();
        //設置畫筆爲取交集模式
        wavePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        //圓形背景初始化
        circlePaint = new Paint();
        //設置圓形背景顏色
        circlePaint.setColor(circle_bgcolor);
        //設置抗鋸齒
        circlePaint.setAntiAlias(true);
        //設置波浪顏色
        wavePaint.setColor(wave_color);
        //設置抗鋸齒
        wavePaint.setAntiAlias(true);
        //佔比一開始設置爲0
        currentPercent = 0;
        //進度條進度開始設置爲0
        currentProgress = 0;
        //進度條的最大值設置爲100
        maxProgress = 100;
        //動畫實例化
        waveProgressAnimat = new WaveProgressAnimat();


複製代碼

下面就能夠在佈局文件自定義設置波浪顏色,高度,寬度以及圓形背景顏色:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
    tools:context=".MainActivity">

    <com.android.quard.CircleWaveProgressView
        android:id="@+id/circle_progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:wave_color="@color/colorPrimaryDark"
        app:circlebg_color="@android:color/black"
        />

</android.support.constraint.ConstraintLayout>
複製代碼

效果圖就不貼出來了。

繪製文字進度效果

下面要實現文字顯示進度,進度條確定缺不了具體數值的顯示,最簡單就是直接在View中實現繪製文字的操做,這種是很簡單的,我以前實現自定義View都是將邏輯放在裏面,這樣就顯得View很臃腫和擴展性不高,由於你想,假如我如今要改變字體位置和樣式,那就須要在這個View去改去大動干戈。假如這個View能開放出處理文字接口的話,也就是後面修改文字樣式只經過這個接口就能夠了,這樣就實現了文字和進度條這個View的解耦。

//進度顯示 TextView
    private TextView tv_progress;
    //進度條顯示值監聽接口
    private UpdateTextListener updateTextListener;
    
        //新建一個動畫類
    public class WaveProgressAnimat extends Animation{


        //在繪製動畫的過程當中會反覆的調用applyTransformation函數,
        // 每次調用參數interpolatedTime值都會變化,該參數從0漸 變爲1,當該參數爲1時代表動畫結束
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t){
            super.applyTransformation(interpolatedTime, t);
            //波浪高度達到最高就不用循環,只須要平移
            if(currentPercent < currentProgress / maxProgress){
                currentPercent = interpolatedTime * currentProgress / maxProgress;
                //這裏直接根據進度值顯示
                tv_progress.setText(updateTextListener.updateText(interpolatedTime,currentProgress,maxProgress));
            }
            //左邊的距離
            moveDistance = interpolatedTime * waveNumber * waveLength * 2;
            //從新繪製
            invalidate();

        }
    }
    
    //定義數值監聽
    public interface UpdateTextListener{
        /** * 提供接口 給外部修改數值樣式 等 * @param interpolatedTime 這個值是動畫的 從0變成1 * @param currentProgress 進度條的數值 * @param maxProgress 進度條的最大數值 * @return */
        String updateText(float interpolatedTime,float currentProgress,float maxProgress);
    }
    //設置監聽 
    public void setUpdateTextListener(UpdateTextListener updateTextListener){
        this.updateTextListener = updateTextListener;

    }

    /** * * 設置顯示內容 * @param tv_progress 內容 數值什麼均可以 * */
    public void setTextViewVaule(TextView tv_progress){
        this.tv_progress = tv_progress;

    }
複製代碼

而後在Activity文件實現CircleWaveProgressView.UpdateTextListener接口,進行邏輯處理:

public class MainActivity extends AppCompatActivity {

    private CircleWaveProgressView circleWaveProgressView;
    private TextView tv_value;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //TextView控件
        tv_value = findViewById(R.id.tv_value);
        //進度條控件
        circleWaveProgressView = findViewById(R.id.circle_progress);
        //將TextView設置進度條裏
        circleWaveProgressView.setTextViewVaule(tv_value);
        //設置字體數值顯示監聽
        circleWaveProgressView.setUpdateTextListener(new CircleWaveProgressView.UpdateTextListener() {
            @Override
            public String updateText(float interpolatedTime, float currentProgress, float maxProgress) {
                //取一位整數和而且保留兩位小數
                DecimalFormat decimalFormat=new DecimalFormat("0.00");
                String text_value = decimalFormat.format(interpolatedTime * currentProgress / maxProgress * 100)+"%";
                //最終把格式好的內容(數值帶進進度條)
                return text_value ;
            }
        });
        //設置進度和動畫時間
        circleWaveProgressView.setProgress(50,2500);
    }
}
複製代碼

佈局文件增長一個TextView:

<android.support.constraint.ConstraintLayout 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"
    tools:context=".MainActivity">

    <com.android.quard.CircleWaveProgressView
        android:id="@+id/circle_progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        />

    <TextView
        android:id="@+id/tv_value"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:textColor="#ffffff"
        android:textSize="24dp"
        />


</android.support.constraint.ConstraintLayout>
複製代碼

最終效果以下圖:

帶進度顯示的效果

繪製雙波浪效果

要實現第二層波浪平移方向和第一層波浪平移方向相反,要改一下繪製順序,。下圖:

第二層波浪分析圖
要從p1開始繪製,由於第二層是要右移,因此右邊要繪製多一次波浪,像繪製第一層波浪同樣,只不過此次是左邊,代碼以下:

//是否繪製雙波浪線
    private boolean isCanvasSecond_Wave;
    //第二層波浪的顏色
    private int second_WaveColor;
    //第二層波浪的畫筆
    private Paint secondWavePaint;
複製代碼

attrs文件增長第二層波浪的顏色:

<!--這裏的名字要和自定義的View名稱同樣,否則在xml佈局中沒法引用-->
    <declare-styleable name="CircleWaveProgressView">
        <!--波浪的顏色-->
        <attr name="wave_color" format="color"></attr>
        <!--圓形背景顏色-->
        <attr name="circlebg_color" format="color"></attr>
        <!--波浪長度-->
        <attr name="wave_length" format="dimension"></attr>
        <!--波浪高度-->
        <attr name="wave_height" format="dimension"></attr>
        <!--當前進度-->
        <attr name="currentProgress" format="float"></attr>
        <!--最大進度-->
        <attr name="maxProgress" format="float"></attr>
        <!--第二層波浪的顏色-->
        <attr name="second_color" format="color"></attr>
    </declare-styleable>
複製代碼

類文件:

//第二層波浪的顏色
        second_WaveColor = typedArray.getColor(R.styleable.CircleWaveProgressView_second_color,Color.RED);
複製代碼

init方法增長:

//初始化第二層波浪畫筆
        secondWavePaint = new Paint();
        secondWavePaint.setColor(second_WaveColor);
        secondWavePaint.setAntiAlias(true);
        //要覆蓋在第一層波浪上,因此選SRC_ATOP模式,第二層波浪徹底顯示,而且第一層非交集部分顯示。這個模式看上面的圖像合成圖文章就能夠了解
        secondWavePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
        //初始狀態不繪製第二層波浪
        isCanvasSecond_Wave = false;
複製代碼

onDraw方法增長:

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

        Log.d("ssd",getWidth()+"");
        //這裏用到了緩存 根據參數建立新位圖
        circleBitmap = Bitmap.createBitmap(waveActualSize, waveActualSize, Bitmap.Config.ARGB_8888);
        //以該bitmap爲低建立一塊畫布
        bitmapCanvas = new Canvas(circleBitmap);
        //繪製圓形 半徑設小了一點,就是爲了能讓波浪填充完整個圓形背景
        bitmapCanvas.drawCircle(waveActualSize/2, waveActualSize/2, waveActualSize/2 - Density.dip2px(getContext(),8), circlePaint);
        //繪製波浪形
        bitmapCanvas.drawPath(paintWavePath(),wavePaint);
        //是否繪製第二層波浪
        if(isCanvasSecond_Wave){
            bitmapCanvas.drawPath(cavasSecondPath(),secondWavePaint);

        }
        //裁剪圖片
        canvas.drawBitmap(circleBitmap, 0, 0, null);
        //繪製波浪線
      // canvas.drawPath(paintWavePath(),wavePaint);

    }
    
        //是否繪製第二層波浪
    public void isSetCanvasSecondWave(boolean isCanvasSecond_Wave){
        this.isCanvasSecond_Wave = isCanvasSecond_Wave;
    }

    /** * 繪製第二層波浪方法 * @return */
    private Path cavasSecondPath(){
        float secondWaveHeight = waveHeight;
        wavePath.reset();
        //移動到右上方,也就是p1點
        wavePath.moveTo(waveActualSize + moveDistance, (1 - currentPercent) * waveActualSize);
        //p1 - p0
        for(int i = 0; i < waveNumber * 2 ; i ++){
            wavePath.rQuadTo(-waveLength / 2,secondWaveHeight,-waveLength,0);
            wavePath.rQuadTo(-waveLength / 2,-secondWaveHeight,-waveLength,0);
        }
        //p0-p3 p3-p0d的高度隨着進度變化而變化
        wavePath.lineTo(0, waveActualSize);
        //鏈接p3 - p2
        wavePath.lineTo(waveActualSize,waveActualSize);
        //鏈接p2 - p1
        wavePath.lineTo(waveActualSize,(1 - currentPercent) * waveActualSize);
        //封閉起來填充
        wavePath.close();
        return wavePath;

    }
複製代碼

最後在Activty文件設置:

//是否繪製第二層波浪
        circleWaveProgressView.isSetCanvasSecondWave(true);
複製代碼

最終效果以下圖:

最終效果圖

總結

通過貝塞爾公式的推導和小例子實現,有了更深入的印象。有不少東西看起來並非那麼觸達,就好像當本身拿到一個開發需求時,技術評估發現會用到本身沒有以前沒有用到的技術,這時候就要多去參照別人實現的思路和方法,或者厚着臉皮問技術牛的人,學到了就是本身的,多付出就努力變得容易。

例子源碼

相關文章
相關標籤/搜索