一 寫在前面的話
前幾天寫了一篇Android 繪圖之PorterDuffXferMode實(shí)例講解與源碼解析,沒看過的可以先進(jìn)去看看。今天我們就來使用PorterDuffXferMode的DST_IN模式,本來想寫個(gè)圓形頭像來看看效果就行了,但是恰巧群里有朋友問到水波紋的進(jìn)度效果。于是乎去看了下實(shí)現(xiàn)方式正好也可以用到PorterDuffXferMode,立馬決定還是做個(gè)個(gè)效果更炫。先看看下面效果圖,就這么一張動(dòng)圖,懶癌犯了懶得做中間效果動(dòng)圖。

二 效果分析
看了上面效果圖,閑來分析下怎么做出這個(gè)效果。
- 靜態(tài)波浪效果(通過Canvas的drawPath方法畫二階貝塞爾曲線實(shí)現(xiàn))
- 動(dòng)態(tài)波浪效果(改變波浪高度和X方向繪制位置后定時(shí)invalidate)
- 圓形效果(通過PorterDuffXferMode的DST_IN和動(dòng)態(tài)波浪效果組合)
- 進(jìn)度文字(最終效果上繪制出文字)
三 波浪效果
-
靜態(tài)波浪效果實(shí)現(xiàn)
首先我們通過Canvas的drawPath方法畫二階貝塞爾曲線的方法,來繪制靜態(tài)波浪效果。我們先看效果,然后看代碼。代碼一般都注視得比較詳細(xì)。避免單獨(dú)再解釋代碼。來上靜態(tài)波浪效果圖:
靜態(tài)波浪效果1.png先上一段xml,然后是代碼。原則上不改xml,就不再上了:
<com.joker.widget.test.WaveProgressBar
android:id="@+id/wave"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_gravity="center_horizontal"
android:padding="10dp"/>
//靜態(tài)波浪效果1
public class WaveProgressBar extends View {
private Paint mWavePaint = new Paint(); //水波畫筆
private Path mPath = new Path(); //路徑
private int mWidth; //控件寬
private int mHeight; //控件高
private int mWaveWidth = 200; //水波寬
private int mWaveHeight = 100; //水波高
int currentY = 200; //當(dāng)前Y值
public WaveProgressBar(Context context) {
this(context, null);
}
public WaveProgressBar(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public WaveProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mWavePaint.setAntiAlias(true);
mWavePaint.setColor(Color.GREEN);
}
//獲取控件的寬和高
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (w > 0) {
mWidth = w;
}
if (h > 0) {
mHeight = h;
}
}
@Override
protected void onDraw(Canvas canvas) {
if (mWidth <= 0 || mHeight <= 0) {
return;
}
//繪制一個(gè)灰色背景 方便看效果
canvas.drawColor(Color.GRAY);
mPath.reset();
//路徑起點(diǎn)
mPath.moveTo(0, 200);
//i+2: 一上一下2個(gè)半波為一組 count+2: 保證波浪占滿控件
for (int i = 0, count = mWidth / mWaveWidth + 2; i < count; i += 2) {
//上半波
mPath.quadTo(mWaveWidth * (i + 0.5f), currentY - mWaveHeight, mWaveWidth * (i + 1), currentY);
//下班波
mPath.quadTo(mWaveWidth * (i + 1.5f), currentY + mWaveHeight, mWaveWidth * (i + 2), currentY);
}
//繪制路徑
canvas.drawPath(mPath, mWavePaint);
}
}
接下來我們讓波浪封閉起來,并且讓它水位升高,有總水杯里的水的感覺。還是和前面一樣,看圖看代碼:


//封閉起來的靜態(tài)波浪效果
public class WaveProgressBar extends View {
private Paint mWavePaint = new Paint();
private Path mPath = new Path();
private int mWidth;
private int mHeight;
private int mWaveWidth = 200; //水波寬
private int mWaveHeight = 100; //水波高
int currentY = 200; //當(dāng)前Y值
public WaveProgressBar(Context context) {
this(context, null);
}
public WaveProgressBar(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public WaveProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mWavePaint.setAntiAlias(true);
mWavePaint.setColor(Color.GREEN);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (w > 0) {
mWidth = w;
}
if (h > 0) {
mHeight = h;
}
}
@Override
protected void onDraw(Canvas canvas) {
if (mWidth <= 0 || mHeight <= 0) {
return;
}
canvas.drawColor(Color.GRAY);
mPath.reset();
//路徑起點(diǎn)
mPath.moveTo(0, 200);
//i+2: 一上一下2個(gè)半波為一組 count+2: 保證波浪占滿控件
for (int i = 0, count = mWidth / mWaveWidth + 2; i < count; i += 2) {
//上半波
mPath.quadTo(mWaveWidth * (i + 0.5f), currentY - mWaveHeight, mWaveWidth * (i + 1), currentY);
//下班波
mPath.quadTo(mWaveWidth * (i + 1.5f), currentY + mWaveHeight, mWaveWidth * (i + 2), currentY);
}
//一下兩句代碼就完成了水波的封閉效果
mPath.lineTo(mWidth, mHeight);
mPath.lineTo(0, mHeight);
mPath.close();
canvas.drawPath(mPath, mWavePaint);
}
}
還是不得不啰嗦一下,我們這里添加了2行代碼實(shí)現(xiàn)了水波的封閉:
mPath.lineTo(mWidth, mHeight);
mPath.lineTo(0, mHeight);
接下來就是水位的上升了,但是這個(gè)地方就上代碼就有點(diǎn)故意拉長篇幅的嫌疑了。其實(shí)很簡(jiǎn)單我們只需要改變currentY的值,就可以實(shí)現(xiàn)水位的上升了。
這里關(guān)于Canvas路徑繪制,或者是坐標(biāo)系搞不明白的可以看我的:Android 繪圖之Canvas相關(guān)API使用一文,在里面有比較詳細(xì)的說明。
-
動(dòng)態(tài)波浪效果實(shí)現(xiàn)
上面我們已經(jīng)實(shí)現(xiàn)了靜態(tài)的波浪效果了,代碼還是比較簡(jiǎn)單的,接下來我們讓波浪動(dòng)次打次動(dòng)次打次動(dòng)起來。
動(dòng)態(tài)波浪效果.gif
//動(dòng)態(tài)波浪效果
public class WaveProgressBar extends View {
private Paint mWavePaint = new Paint();
private Path mPath = new Path();
private int mWidth;
private int mHeight;
private int mWaveWidth = 200; //水波寬
private int mWaveHeight = 100; //水波高
int currentY = 200; //當(dāng)前Y值
private double mRate = 0.1; //流速
private int distance; //距離
private Handler mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
distance += mWaveWidth * mRate; //計(jì)算流動(dòng)總距離
distance = distance % (mWaveWidth << 1); //根據(jù)總距離算出當(dāng)前移動(dòng)距離
invalidate(); //刷新重繪
mHandler.sendEmptyMessageDelayed(0, 20); //20ms發(fā)送一個(gè)消息
return false;
}
});
public WaveProgressBar(Context context) {
this(context, null);
}
public WaveProgressBar(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public WaveProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mWavePaint.setAntiAlias(true);
mWavePaint.setColor(Color.GREEN);
//20ms發(fā)送一個(gè)消息
mHandler.sendEmptyMessageDelayed(0, 20);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (w > 0) {
mWidth = w;
}
if (h > 0) {
mHeight = h;
}
}
@Override
protected void onDraw(Canvas canvas) {
if (mWidth <= 0 || mHeight <= 0) {
return;
}
canvas.drawColor(Color.GRAY);
mPath.reset();
//路徑起點(diǎn)
mPath.moveTo(0, 200);
//i+2: 一上一下2個(gè)半波為一組 count+2: 保證波浪占滿控件
for (int i = 0, count = mWidth / mWaveWidth + 2; i < count; i += 2) {
//上半波 減去distance 造成左移效果
mPath.quadTo(mWaveWidth * (i + 0.5f) - distance, currentY - mWaveHeight, mWaveWidth * (i + 1) - distance, currentY);
//下班波
mPath.quadTo(mWaveWidth * (i + 1.5f) - distance, currentY + mWaveHeight, mWaveWidth * (i + 2) - distance, currentY);
}
mPath.lineTo(mWidth, mHeight);
mPath.lineTo(0, mHeight);
mPath.close();
canvas.drawPath(mPath, mWavePaint);
}
}
看效果圖的水流還是很急的,減小速率就可以讓它流慢點(diǎn),我們先不關(guān)心這個(gè),說說實(shí)現(xiàn)思路。主要是利用Handler定時(shí)發(fā)送消息通知重繪的方式,每20ms計(jì)算出移動(dòng)距離并刷新重繪一次。在繪制的時(shí)候減去移動(dòng)距離就有了左移效果,其他的說明在代碼中注釋很詳盡了。
四 圓形效果
水波效果我們就做到這里,接下來就是利用PorterDuffXferMode的DST_IN模式來實(shí)現(xiàn)圓形效果,我們先繪制的水波后繪制的圓,所以我們需要使用DST_IN。其實(shí)繪制圓形頭像效果原理也是一樣的,至于用什么模式就跟你繪制的先后順序,還有需求有關(guān)。不了解PorterDuffXferMode可以去看看我之前的文章,本文后我會(huì)給出相關(guān)鏈接地址。

//圓形波浪效果
public class WaveProgressBar extends View {
private Paint mWavePaint = new Paint();
private Path mPath = new Path();
private int mWidth;
private int mHeight;
private int mWaveWidth = 200; //水波寬
private int mWaveHeight = 100; //水波高
int currentY = 200; //當(dāng)前Y值
private double mRate = 0.01; //流速
private int distance; //距離
private PorterDuffXfermode mXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
private Bitmap mCircleBitmap; //圓形bitmap
private RectF mBorderRectF; //邊框矩形
private int mBorderRadius; //邊框半徑
private Handler mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
distance += mWaveWidth * mRate; //計(jì)算流動(dòng)總距離
distance = distance % (mWaveWidth << 1);
invalidate();
mHandler.sendEmptyMessageDelayed(0, 20);
return false;
}
});
public WaveProgressBar(Context context) {
this(context, null);
}
public WaveProgressBar(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public WaveProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mWavePaint.setAntiAlias(true);
mWavePaint.setColor(Color.GREEN);
mHandler.sendEmptyMessageDelayed(0, 20);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (w > 0) {
mWidth = w;
}
if (h > 0) {
mHeight = h;
}
mWidth = mHeight = Math.min(mWidth, mHeight);
//創(chuàng)建邊框矩形
mBorderRectF = new RectF(0, 0, mWidth, mHeight);
//邊框半徑
mBorderRadius = (mWidth >> 1);
}
@Override
protected void onDraw(Canvas canvas) {
if (mWidth <= 0 || mHeight <= 0) {
return;
}
canvas.drawBitmap(createWaveBitmap(), 0, 0, mWavePaint);
}
//創(chuàng)建水波bitmap
private Bitmap createWaveBitmap() {
Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.GRAY);
mPath.reset();
//路徑起點(diǎn)
mPath.moveTo(0, 200);
//i+2: 一上一下2個(gè)半波為一組 count+2: 保證波浪占滿控件
for (int i = 0, count = mWidth / mWaveWidth + 2; i < count; i += 2) {
//上半波 減去distance 造成左移效果
mPath.quadTo(mWaveWidth * (i + 0.5f) - distance, currentY - mWaveHeight, mWaveWidth * (i + 1) - distance, currentY);
//下班波
mPath.quadTo(mWaveWidth * (i + 1.5f) - distance, currentY + mWaveHeight, mWaveWidth * (i + 2) - distance, currentY);
}
mPath.lineTo(mWidth, mHeight);
mPath.lineTo(0, mHeight);
mPath.close();
canvas.drawPath(mPath, mWavePaint);
if (mCircleBitmap == null) {
//創(chuàng)建圓形的bitmap
mCircleBitmap = createShapeBitmap();
}
//設(shè)置mXfermode模式
mWavePaint.setXfermode(mXfermode);
canvas.drawBitmap(mCircleBitmap, 0, 0, mWavePaint);
mWavePaint.setXfermode(null);
return bitmap;
}
//創(chuàng)建形狀的bitmap
private Bitmap createShapeBitmap() {
Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawCircle(mBorderRectF.centerX(), mBorderRectF.centerY(), mBorderRadius, mWavePaint);
return bitmap;
}
}
這里把onDraw里面的代碼抽取出來了,把onDraw的背景繪制放在了createWaveBitmap中便于觀看效果,利用PorterDuffXferMode的DST_IN模式來實(shí)現(xiàn)圓形效果。
五 繪制文字
我們已經(jīng)完成了圓形的水波紋效果,接下來就是將文字繪制在圓形的水波里面。繪制文字就比較簡(jiǎn)單了,調(diào)用Canvas的drawText方法在圓形水波紋上將文字繪制出來就O啦。

public class WaveProgressBar extends View {
private Paint mWavePaint = new Paint();
private Paint mTextPaint = new Paint();
private Path mPath = new Path();
private int mWidth;
private int mHeight;
private int mWaveWidth = 200; //水波寬
private int mWaveHeight = 100; //水波高
int currentY = 200; //當(dāng)前Y值
private double mRate = 0.1; //流速
private int distance; //距離
private PorterDuffXfermode mXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
private Bitmap mCircleBitmap; //圓形bitmap
private RectF mBorderRectF; //邊框矩形
private int mBorderRadius; //邊框半徑
private Handler mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
distance += mWaveWidth * mRate; //計(jì)算流動(dòng)總距離
distance = distance % (mWaveWidth << 1);
invalidate();
mHandler.sendEmptyMessageDelayed(0, 20);
return false;
}
});
public WaveProgressBar(Context context) {
this(context, null);
}
public WaveProgressBar(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public WaveProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mWavePaint.setAntiAlias(true);
mWavePaint.setColor(Color.GREEN);
mTextPaint.setAntiAlias(true);
mTextPaint.setColor(Color.WHITE);
mTextPaint.setTextSize(32);
mHandler.sendEmptyMessageDelayed(0, 20);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (w > 0) {
mWidth = w;
}
if (h > 0) {
mHeight = h;
}
mWidth = mHeight = Math.min(mWidth, mHeight);
//創(chuàng)建邊框矩形
mBorderRectF = new RectF(0, 0, mWidth, mHeight);
//邊框半徑
mBorderRadius = (mWidth >> 1);
}
@Override
protected void onDraw(Canvas canvas) {
if (mWidth <= 0 || mHeight <= 0) {
return;
}
canvas.drawBitmap(createWaveBitmap(), 0, 0, mWavePaint);
canvas.drawText("80%", mBorderRectF.centerX(), mBorderRectF.centerY(), mTextPaint);
}
//創(chuàng)建水波bitmap
private Bitmap createWaveBitmap() {
Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.GRAY);
mPath.reset();
//路徑起點(diǎn)
mPath.moveTo(0, 200);
//i+2: 一上一下2個(gè)半波為一組 count+2: 保證波浪占滿控件
for (int i = 0, count = mWidth / mWaveWidth + 2; i < count; i += 2) {
//上半波 減去distance 造成左移效果
mPath.quadTo(mWaveWidth * (i + 0.5f) - distance, currentY - mWaveHeight, mWaveWidth * (i + 1) - distance, currentY);
//下班波
mPath.quadTo(mWaveWidth * (i + 1.5f) - distance, currentY + mWaveHeight, mWaveWidth * (i + 2) - distance, currentY);
}
mPath.lineTo(mWidth, mHeight);
mPath.lineTo(0, mHeight);
mPath.close();
canvas.drawPath(mPath, mWavePaint);
if (mCircleBitmap == null) {
//創(chuàng)建圓形的bitmap
mCircleBitmap = createShapeBitmap();
}
//設(shè)置mXfermode模式
mWavePaint.setXfermode(mXfermode);
canvas.drawBitmap(mCircleBitmap, 0, 0, mWavePaint);
mWavePaint.setXfermode(null);
return bitmap;
}
//創(chuàng)建形狀的bitmap
private Bitmap createShapeBitmap() {
Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawCircle(mBorderRectF.centerX(), mBorderRectF.centerY(), mBorderRadius, mWavePaint);
return bitmap;
}
}
這一步?jīng)]什么可說的,新創(chuàng)建了mTextPaint 畫筆,設(shè)置了字體顏色大小等屬性,最后在原本畫好的效果上再繪制上文字就成了。可我們文章開頭的效果圖上,文字是隨著水位而上升的,其實(shí)這個(gè)很簡(jiǎn)單,改變文本繪制的Y軸位置就能達(dá)到上升效果,我們的水波紋效果到這里就完成了它的雛形。
上面的演示代碼還是比較雜亂,創(chuàng)建Bitmap都還是在onDraw中,沒有考慮控件的測(cè)量,也不支持自定義屬性等。大家可以根據(jù)上面的步驟,自己整理實(shí)現(xiàn)定義成自己想要的效果。
最后我將源碼整理了下,優(yōu)化后的源碼后續(xù)會(huì)補(bǔ)上地址,就不在文中給出了。
附:
Android 繪圖之Canvas相關(guān)API使用
Android 繪圖之PorterDuffXferMode實(shí)例講解與源碼解析

