Android自定義View-水波紋progressbar

Android自定義View-水波紋progressbar

  首先來看一看效果圖:
  process
  

先簡要說一下這裡需要涉及到的知識點:

  1. 2D繪圖基礎
  2. path貝塞爾二階曲線
  3. ValueAnimator
  4. PorterDuffXfermode

參考的文章:

  1. Path從懵逼到精通(2)——貝塞爾曲線

繪製思路:

  1. 繪製兩段二階貝塞爾曲線圍成的波浪封閉圖形,一段波浪的長度為螢幕寬度。
  2. 水平移動繪製好的圖形,形成波浪滾動的效果,並不斷迴圈。
  3. 不斷增加繪製圖形的高度,形成水位上升的效果。
  4. 通過 PorterDuffXfermode 的效果 SRC_IN 繪製成最終效果

第一步:繪製封閉的波浪圖形

首先是繪製下圖中紅色線條區域內的圖形:
這裡寫圖片描述
如上圖中所示,我們要繪製兩個螢幕寬度的波浪圖形,那麼先來看看我們要的效果圖:
這裡寫圖片描述

初始化成員變數:

private int mWaveHeight;//水波紋高度
private Paint mPaint;//繪製水波紋的畫筆
private Path mPath;//繪製水波紋的路徑
private int mWL;//螢幕寬度
private String TAG = "zoneLog";//Log 日誌的 Tag
private int mFu;//水波紋的振幅
private int mOffset;//水波紋移動的偏移值
private boolean mMIsAnimatorPlaying;//用於判斷是否第一次執行 ValueAnimator 
private Paint mTextPaint;//文字畫筆
private String mText;//進度值文字

構造方法中的初始化資料:

mPaint = new Paint();//用於繪製水波紋的畫筆
mPaint.setAntiAlias(true);
mPaint.setColor(Color.parseColor("#5DCEC6"));
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mFu = 100;//水波紋的振幅
mTextPaint = new Paint();//用於繪製文字的畫筆
mTextPaint.setColor(Color.WHITE);
mTextPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mTextPaint.setAntiAlias(true);
mTextPaint.setTextSize(75);
mOffset= 0;//水波紋的偏移值,用於水波紋的移動效果
mWaveHeight= 0;//進度值,也是水波紋的高度
mPath = new Path();//用於繪製水波紋路徑

首先繪製左邊,右邊,底部的三條線段:

mPath.moveTo(mWL, getHeight()/2);
mPath.lineTo(mWL, getHeight());//繪製右邊的線段
mPath.lineTo(-mWL, getHeight());//繪製底部的線段
mPath.lineTo(-mWL, getHeight()/2);//繪製左邊的線段

繪製螢幕中的波浪曲線:( 此處需要注意,第一段波浪是在螢幕外面的)

mPath.quadTo((-mWL * 3 / 4), getHeight() / 2   mFu, (-mWL / 2), getHeight() / 2); 
//畫出第一段波紋的第一條曲線
mPath.quadTo((-mWL / 4), getHeight() / 2 - mFu, 0, getHeight() / 2); 
//畫出第一段波紋的第二條曲線
mPath.quadTo((mWL / 4), getHeight() / 2   mFu, (mWL / 2), getHeight() / 2); 
//畫出第二段波紋的第一條曲線
mPath.quadTo((mWL * 3 / 4), getHeight() / 2 - mFu, mWL, getHeight() / 2);  
//畫出第二段波紋的第二條曲線
mPath.close();//封閉曲線
canvas.drawPath(mPath, mPaint);//繪製曲線

到現在為止,就已經繪製出了一個波浪圖形。

第二步:讓水波紋動起來

先看一下效果圖:
 
這裡寫圖片描述
 
 
這裡使用 ValueAnimator 來動態改變各個點的 x 軸座標,使水波紋有迴圈流動的效果。那麼新增了偏移值的程式碼是這樣的:

mPath.reset();//每次修改 mPath 的值,需要重置 mPath
mPath.moveTo(mWL   mOffset, getHeight() / 2);
mPath.lineTo(mWL   mOffset, getHeight());//繪製右邊的線段
mPath.lineTo(-mWL   mOffset, getHeight());//繪製底部的線段
mPath.lineTo(-mWL   mOffset, getHeight() / 2);//繪製左邊的線段
mPath.quadTo((-mWL * 3 / 4)   mOffset, getHeight() / 2   mFu, (-mWL / 2)   mOffset, getHeight() / 2); 
//畫出第一段波紋的第一條曲線
mPath.quadTo((-mWL / 4)   mOffset, getHeight() / 2 - mFu, 0   mOffset, getHeight() / 2); 
//畫出第一段波紋的第二條曲線
mPath.quadTo((mWL / 4)   mOffset, getHeight() / 2   mFu, (mWL / 2)   mOffset, getHeight() / 2); 
//畫出第二段波紋的第一條曲線
mPath.quadTo((mWL * 3 / 4)   mOffset, getHeight() / 2 - mFu, mWL   mOffset, getHeight() / 2);  
//畫出第二段波紋的第二條曲線
mPath.close();//封閉曲線
canvas.drawPath(mPath, mPaint);//繪製曲線

通過 ValueAnimator 動態改版 mOffset 的指:

if (!mMIsAnimatorPlaying) {//判斷是否第一次開始 ValueAnimator
mMIsAnimatorPlaying = true;
ValueAnimator animator = ValueAnimator.ofInt(0, mWL);//設定移動範圍為一個螢幕寬度
animator.setDuration(1000);//設定持續時間為1秒
animator.setRepeatCount(ValueAnimator.INFINITE);//設定為無限迴圈
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
mOffset = value;//修改偏移量
postInvalidate();//重新整理介面
}
});
animator.start();
}

第三步:讓水位漲起來

先看效果圖:

這裡寫圖片描述

同理,這裡也是通過 ValueAnimator 來不斷增加 mWaveHeight 的值,但是這裡指執行一次。那麼新增了 mWaveHeight 的程式碼是這樣的。

mPath.reset();//每次修改 mPath 的值,需要重置 mPath
mPath.moveTo(mWL   mOffset, getHeight() - mWaveHeight);//因為 y 軸正方向是向下的,所以減去水波紋的高度
mPath.lineTo(mWL   mOffset, getHeight());//繪製右邊的線段
mPath.lineTo(-mWL   mOffset, getHeight());//繪製底部的線段
mPath.lineTo(-mWL   mOffset, getHeight() - mWaveHeight);//繪製左邊的線段
mPath.quadTo((-mWL * 3 / 4)   mOffset, getHeight() - mWaveHeight   mFu, (-mWL / 2)   mOffset, getHeight() - mWaveHeight); 
//畫出第一段波紋的第一條曲線
mPath.quadTo((-mWL / 4)   mOffset, getHeight() - mWaveHeight - mFu, 0   mOffset, getHeight() - mWaveHeight); 
//畫出第一段波紋的第二條曲線
mPath.quadTo((mWL / 4)   mOffset, getHeight() - mWaveHeight   mFu, (mWL / 2)   mOffset, getHeight() - mWaveHeight); 
//畫出第二段波紋的第一條曲線
mPath.quadTo((mWL * 3 / 4)   mOffset, getHeight() - mWaveHeight - mFu, mWL   mOffset, getHeight() - mWaveHeight);  
//畫出第二段波紋的第二條曲線
mPath.close();//封閉曲線
canvas.drawPath(mPath, mPaint);//繪製曲線

動態改變水波紋的高度:

if (!mMIsAnimatorPlaying) {//判斷是否第一次開始 ValueAnimator
mMIsAnimatorPlaying = true;
ValueAnimator animator = ValueAnimator.ofInt(0, mWL);//設定移動範圍為一個螢幕寬度
animator.setDuration(1000);//設定持續時間為1秒
animator.setRepeatCount(ValueAnimator.INFINITE);//設定為無限迴圈
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
mOffset = value;//修改偏移量
postInvalidate();//重新整理介面
}
});
animator.start();
ValueAnimator animatorHeight = ValueAnimator.ofInt(0, getHeight()   mFu);
animatorHeight.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mWaveHeight = (int) animation.getAnimatedValue();
mText = String.valueOf((int) ((double) mWaveHeight / (double) (getHeight()   mFu) * 100))   "%";//計算進度的百分比
if (mWaveHeight == getHeight()   mFu) {//當水位高於螢幕高度,停止水波紋移動效果
animation.cancel();
}
if (mWaveHeight > getHeight() / 2) {//當水位高於文字的時候,字型為白色,否則為淺藍色
mTextPaint.setColor(Color.WHITE);
} else {
mTextPaint.setColor(Color.parseColor("#5DCEC6"));
}
postInvalidate();
}
});
animatorHeight.setDuration(10000);//持續10秒
animatorHeight.start();
}
canvas.drawText(mText, getWidth() / 2 - mTextPaint.measureText(mText) / 2, getHeight() / 2, mTextPaint);

第四步:使用 PorterDuffXfermode 實現最終效果

最終的效果就是文章一開始時候效果。
使用特效畫筆:

final int sc = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.qq3);//獲取 bitmap 資源
canvas.drawBitmap(bitmap, getWidth() / 2 - bitmap.getWidth() / 2, getHeight() / 2 - bitmap.getHeight() / 2, null);//繪製 bitmap 
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));//設定特效畫筆
mPaint.setColor(Color.parseColor("#5DCEC6"));

最終整合起來的程式碼是這樣的:

public class WaterWave extends View {
private int mWaveHeight;//水波紋高度
private Paint mPaint;//繪製水波紋的畫筆
private Path mPath;//繪製水波紋的路徑
private int mWL;//螢幕寬度
private String TAG = "zoneLog";//Log 日誌的 Tag
private int mFu;//水波紋的振幅
private int mOffset;//水波紋移動的偏移值
private boolean mMIsAnimatorPlaying;//用於判斷是否第一次執行 ValueAnimator
private Paint mTextPaint;//文字畫筆
private String mText;//進度值文字
public WaterWave(Context context) {
super(context);
init();
}
public WaterWave(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public WaterWave(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public WaterWave(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init() {
mPaint = new Paint();//用於繪製波浪
mPaint.setAntiAlias(true);
mPaint.setColor(Color.parseColor("#5DCEC6"));
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mFu = 100;//波浪的振幅
mTextPaint = new Paint();//用於繪製文字
mTextPaint.setColor(Color.WHITE);
mTextPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mTextPaint.setAntiAlias(true);
mTextPaint.setTextSize(75);
mOffset = 0;//水波紋移動的偏移值
mWaveHeight = 0;//進度值,也是水波紋的高度
mPath = new Path();//用於繪製波浪
}
@Override
protected void onDraw(final Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.WHITE);
mWL = getWidth();
mPath.reset();
final int sc = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.qq3);//獲取 bitmap 資源
canvas.drawBitmap(bitmap, getWidth() / 2 - bitmap.getWidth() / 2, getHeight() / 2 - bitmap.getHeight() / 2, null);//繪製 bitmap
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));//設定特效畫筆
mPaint.setColor(Color.parseColor("#5DCEC6"));
mPath.moveTo(mWL   mOffset, getHeight() - mWaveHeight);//因為 y 軸正方向是向下的,所以減去水波紋的高度
mPath.lineTo(mWL   mOffset, getHeight());//繪製右邊的線段
mPath.lineTo(-mWL   mOffset, getHeight());//繪製底部的線段
mPath.lineTo(-mWL   mOffset, getHeight() - mWaveHeight);//繪製左邊的線段
mPath.quadTo((-mWL * 3 / 4)   mOffset, getHeight() - mWaveHeight   mFu, (-mWL / 2)   mOffset, getHeight() - mWaveHeight); //畫出第一段波紋的第一條曲線
mPath.quadTo((-mWL / 4)   mOffset, getHeight() - mWaveHeight - mFu, 0   mOffset, getHeight() - mWaveHeight); //畫出第一段波紋的第二條曲線
mPath.quadTo((mWL / 4)   mOffset, getHeight() - mWaveHeight   mFu, (mWL / 2)   mOffset, getHeight() - mWaveHeight); //畫出第二段波紋的第一條曲線
mPath.quadTo((mWL * 3 / 4)   mOffset, getHeight() - mWaveHeight - mFu, mWL   mOffset, getHeight() - mWaveHeight);  //畫出第二段波紋的第二條曲線
mPath.close();//封閉曲線
canvas.drawPath(mPath, mPaint);//繪製曲線
if (!mMIsAnimatorPlaying) {//判斷是否第一次開始 ValueAnimator
mMIsAnimatorPlaying = true;
ValueAnimator animator = ValueAnimator.ofInt(0, mWL);//設定移動範圍為一個螢幕寬度
animator.setDuration(1000);//設定持續時間為1秒
animator.setRepeatCount(ValueAnimator.INFINITE);//設定為無限迴圈
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
mOffset = value;//修改偏移量
postInvalidate();//重新整理介面
}
});
animator.start();
ValueAnimator animatorHeight = ValueAnimator.ofInt(0, getHeight()   mFu);
animatorHeight.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mWaveHeight = (int) animation.getAnimatedValue();
mText = String.valueOf((int) ((double) mWaveHeight / (double) (getHeight()   mFu) * 100))   "%";//計算進度的百分比
if (mWaveHeight == getHeight()   mFu) {//當水位高於螢幕高度,停止水波紋移動效果
animation.cancel();
}
if (mWaveHeight > getHeight() / 2) {//當水位高於文字的時候,字型為白色,否則為淺藍色
mTextPaint.setColor(Color.WHITE);
} else {
mTextPaint.setColor(Color.parseColor("#5DCEC6"));
}
postInvalidate();
}
});
animatorHeight.setDuration(10000);//持續10秒
animatorHeight.start();
}
canvas.drawText(mText, getWidth() / 2 - mTextPaint.measureText(mText) / 2, getHeight() / 2, mTextPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(sc);// 還原畫布
}
}

使用到的圖片資源:
這裡寫圖片描述

好了,我們一步一步實現了最終效果,如果文中有什麼知識點是錯誤的或者更好的實現方法,請及時聯絡我進行修改,以免誤導別人。謝謝。