你們好,我是深紅騎士,愛開玩笑,技術一渣渣,熱愛鑽研,這篇文章是今年的最後一篇了,首先祝你們在新的一年裏心想事成,諸事順利。今天來學習貝塞爾曲線,以前一直想學,惋惜沒時間。什麼是貝塞爾曲線呢?一開始我也是不懂的,當查了不少資料,如今仍是不夠了解,其推導公式仍是不能深刻了解。對發佈這曲線的法國工程師皮埃爾·貝塞爾
由衷敬佩,貝塞爾曲線,又稱貝茲曲線或者貝濟埃曲線,是應用於二維圖形
應用程序的數學曲線.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階,和高數中二項式展開同樣,就是階數越高,控制點之間就會越近,繪製的曲線就會更加絲滑。通用公式以下: 把貝塞爾曲線原理弄懂了,下面就能夠用來作實際性的東西了。在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函數的定義: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來模擬畫出上面這條軌跡的輔助控制點的位置: 下面經過草圖分析肯定起始點,終點,控制點的位置, 注意,下面的分析圖位置不是很準備,只是爲了肯定控制點的位置。 先看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);
註釋,再看看效果:
經過上面的簡單例子,能夠得出如下兩點:
下面來看看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
實現的曲線會更順滑。
直接看這個函數的說明:
/** * 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);
}
複製代碼
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的控制點和終點座標。
那麼第一條曲線就容易繪製出來了,而且第一條曲線的終點也知道了是(400,400),那麼第二句path.rQuadTo(100,100,200,0)是基於這個終點(400,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;
}
複製代碼
在上面中,發現一個問題,就是寬和高都在初始化方法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);
複製代碼
最終效果以下圖:
通過貝塞爾公式的推導和小例子實現,有了更深入的印象。有不少東西看起來並非那麼觸達,就好像當本身拿到一個開發需求時,技術評估發現會用到本身沒有以前沒有用到的技術,這時候就要多去參照別人實現的思路和方法,或者厚着臉皮問技術牛的人,學到了就是本身的,多付出就努力變得容易。