前言
大家好,我是深紅騎士,愛開玩笑,技術一渣渣,熱愛鑽研,這篇文章是今年的最後一篇了,首先祝大家在新的一年裡心想事成,諸事順利。今天來學習貝塞爾曲線,之前一直想學,可惜沒時間。什麼是貝塞爾曲線呢?一開始我也是不懂的,當查了很多資料,現在還是不夠了解,其推導公式還是不能深入瞭解。對釋出這曲線的法國工程師皮埃爾·貝塞爾
由衷敬佩,貝塞爾曲線,又稱貝茲曲線或者貝濟埃曲線,是應用於二維圖形
應用程式的數學曲線.1962年,皮埃爾·貝塞爾
運用貝塞爾曲線為汽車的主體進行設計,貝塞爾曲線最初由Paul de Casteljau於1959年運用de Casteljau
演算法開發,以穩定的述職方法求出貝塞爾曲線。其實貝塞爾曲線就在我們日常生活中,如一些成熟的點陣圖軟體中:PhotoSHop,Flash5等。在前端開發中,貝塞爾曲線也是無處不在:前端2D或者3D圖形圖示庫都會使用貝塞爾曲線;它可以用來繪製曲線,在svg和canvas中,原生提供的曲線繪製都是用貝塞爾曲線實現的;在css的transition-timing-function
屬性,可以使用貝塞爾曲線來描述過渡的緩動計算。
貝塞爾曲線原理
貝塞爾曲線是用一系列點來控制曲線狀態的,我將這一系列點分為三個點:起點
,終點
,控制點
。通過改變這些點,貝塞爾曲線就會發生變化。
- 起點:確定曲線的起點
- 終點:確定曲線的終點
- 控制點:確定曲線的控制點
一階曲線原理
一階曲線就是一條直線,只有兩個點,就是起點
和終點
,也就是最終效果就是一條線段。還是直接上圖比較直觀:
二階曲線原理
二階曲線由兩個資料點(起始點和終點),一個控制點來描述曲線狀態,如下圖,下面A點是起始點,C是終點,B是控制點。
紅線AC是怎麼生成的呢?繼續上圖: 簡單來看連線AB,BC兩條線段,在AB,BC分別取D,E兩點,連線DE。如下圖: D在AB線段上從A往B做一階曲線運動,E在BC線段上從B往C做一階曲線運動,而F在DE上做一階曲線運動,那麼F這點就是貝塞爾曲線上的一個點,動態圖如下: 再簡單理解就是:二階貝塞爾曲線就是起點
和終點
不斷變化的一階貝塞爾曲線。二階公式如下:
那麼上面這個公司怎麼推匯出來的呢?同樣為了方便,我就在紙上寫了:
三階曲線原理
三階曲線其實就是由兩個資料點(起始點和終點),兩個控制點來描述曲線的狀態,如下圖,下面A是起始點,D是終點,B和C是控制點。
動態圖如下: 可以這麼理解,兩個資料點和控制點不斷變化的二階貝塞爾曲線,即拆分為p0p1p2和p1p2p3兩個二階貝塞爾曲線。三階公式如下: 那麼上面這個公式是怎麼推匯出來的呢?直接上圖: 四階,五階的效果圖和推導公式就不上了,原理是一樣的。通過上面一階,二階,三階的推導可以發現這樣一個規律:沒N階貝塞爾曲線都可以拆分為兩個N-1階,和高數中二項式展開一樣,就是階數越高,控制點之間就會越近,繪製的曲線就會更加絲滑。通用公式如下: 把貝塞爾曲線原理弄懂了,下面就可以用來做實際性的東西了。Android中的貝塞爾曲線
在Android中,Path類中有四個方法與貝塞爾曲線相關的,也就是已經封裝了關於貝塞爾曲線的函式,開發者直接呼叫即可:
//二階貝賽爾
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函式的定義:
/**
* 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來模擬畫出上面這條軌跡的輔助控制點的位置: 下面通過草圖分析確定起始點,終點,控制點的位置,注意,下面的分析圖位置不是很準備,只是為了確定控制點的位置。 先看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();
}
}
複製程式碼
佈局檔案如下:
<?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();
}
}
複製程式碼
直接上效果圖:
當使用者點選螢幕時,首先觸發的是MotionEvent.DOWN
這個條件,然後呼叫path.move(event.getX(),event.getY())
,當使用者移動手指時,就用path.lineTo(event.getX,event.getY())
將各個點連線起來,然後呼叫invalidate
重新繪製。這裡簡單說一下在MotionEvent.ACTION_DOWN
為什麼要返回return true
。return true
表示當前的控制元件已經消費了按下事件,剩下的ACTION_UP
和
ACTION_MOVE
都會被執行;如果在case MotionEvent.ACTION_DOWN
下返回return false
,後續的MOTION_MOVE
,MMOTION_UP
都不會被接收到,因為沒有消費ACTION_DOWN
,系統就會認為ACTION_DOWN
沒有發生過,所以ACTION_MOVE
和ACTION_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)開始。用mBeforeX
和mBeforeY
記錄手指移動的前一個橫縱座標,而這個點是做控制點,最後返回return true
為了讓ACTION_MOVE
和ACTION_UP
向本控制元件傳遞。下面說說在ACTION_MOVE
方法的邏輯處理,首先是確定結束點,上面也說了結束點是線段的中間位置,所以用了兩條公式來endX = (mBeforeX + event.getX()) / 2;
和endY = (mBeforeY + event.getY()) / 2;
求這個中間位置的橫縱座標,而控制點就是上個手指觸控螢幕的位置,後面就是更新前一個手指座標。這裡注意一下,上面也說了當連續呼叫quardTo
的時候,第一個起始點是Path.moveTo(x,y)
來設定的,其他部分,前面呼叫quadTo
的終點是下一個quard
的起點,這裡所說的起始點就是上一個線段的中間點。上面的邏輯用一句話表示:把各個線段的中間點作為起始點和終點,把前一個手指位置作為控制點,最終效果如下:
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.lineTo
和path.close
來連線封閉起來,構成一個填充圖形,分析如下圖:
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
各種模式的效果圖:
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);
複製程式碼
最終效果如下圖:
總結
經過貝塞爾公式的推導和小例子實現,有了更深刻的印象。有很多東西看起來並不是那麼觸達,就好像當自己拿到一個開發需求時,技術評估發現會用到自己沒有之前沒有用到的技術,這時候就要多去參照別人實現的思路和方法,或者厚著臉皮問技術牛的人,學到了就是自己的,多付出就努力變得容易。