前言:公司在做的一個專案,要求在地圖上以水波氣泡的形式來顯示站點,並且氣泡要有水波的動態效果。好吧!既然有這樣的需求,那就手擼一款水波氣泡吧!
效果圖預覽
最後完成的效果圖如下
不想看文章的話,可以點選這裡,直接獲取原始碼。
實現方式
步驟拆解
在需要自定義view的時候,我首先要做的就是將最後要實現的效果來進行拆分,拆分成許多小的步驟,然後一步步的來實現,最終達到想要的效果。
可以將文章開始的時候的效果圖拆分成以下幾部分:
- 畫出氣泡後面的白色背景。
- 畫內部的紫色氣泡。
- 用貝塞爾曲線讓內部的紫色氣泡動起來。
拆解之後,就可以按照拆解的步驟來一步步實現了。
畫白色背景
這裡畫白色背景有以下兩種方式:
- 用
path
直接描述一個白色背景的形狀。 - 用
path
描述一個三角形,然後在畫出一個圓形,即成最終的白色背景了。
第一種方式如下圖的左圖,用path
直接描述出了白色背景,這種方式可以用path.addArc()
來畫上部弧形,然後用path.moveTo()
和path.lineTo()
方法描述出下部分的尖角。
第二種實現的方式如下圖的右圖,直接畫出一個圓,再用path.moveTo()
和path.lineTo()
方法來描述出下部分的尖角。
本文采用的是第二種方式來實現的,具體程式碼如下
//此處程式碼是下部尖角的path
mBackgroundPath.moveTo(mResultWidth / 2 - mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2);
mBackgroundPath.lineTo(mResultWidth / 2, mResultWidth / 2 + mOutRadius + mOutRadius / 4);
mBackgroundPath.lineTo(mResultWidth / 2 + mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2);
//畫外部背景
canvas.drawPath(mBackgroundPath, mBackgroundPaint);
canvas.drawCircle(mResultWidth / 2, mResultWidth / 2, mOutRadius, mBackgroundPaint);
複製程式碼
畫內部的氣泡
內部的氣泡的形狀其實就是縮小的外部背景,具體的程式碼如下
//內部氣泡的尖角
mBubblesPath.moveTo(mResultWidth / 2 - mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2 - dp2px(getContext(), 5));
mBubblesPath.lineTo(mResultWidth / 2, mResultWidth / 2 + mOutRadius + mOutRadius / 4 - dp2px(getContext(), 5));
mBubblesPath.lineTo(mResultWidth / 2 + mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2 - dp2px(getContext(), 5));
//畫圓
mBubblesPath.addCircle(mResultWidth / 2, mResultWidth / 2, mInnerRadius, Path.Direction.CCW);
複製程式碼
到這裡已經將氣泡的基本形狀畫出來了,見下圖
我們會發現氣泡內部的顏色是漸變色,那漸變色是怎麼設定的呢?其實自定義view就是將想要的效果通過畫筆畫在畫布上,實現顏色的漸變肯定就是通過設定畫筆的屬性來實現的了,設定漸變色的程式碼如下
//設定漸變色
Shader shader = new LinearGradient(mResultWidth / 2, mResultWidth / 2 - mInnerRadius, mResultWidth / 2, mResultWidth / 2 + mInnerRadius, Color.parseColor("#9592FB"),
Color.parseColor("#3831D4"), Shader.TileMode.CLAMP);
mBubblesPaint.setShader(shader);
複製程式碼
LinearGradient(float x0, float y0, float x1, float y1, @ColorInt int color0, @ColorInt int color1, @NonNull TileMode tile)
x0
y0
x1
y1
:漸變的兩個端點的位置color0
color1
是端點的顏色tile
:端點範圍之外的著色規則,型別是TileMode
。TileMode
一共有 3 個值可選:CLAMP
,MIRROR
和REPEAT
。一般用CLAMP
就可以了。
讓內部氣泡動起來
氣泡內部的動畫是水波的形式,這裡畫水波用的是二階貝塞爾曲線,關於Android中貝塞爾曲線的知識可以參考這裡。實現氣泡內部水波效果的程式碼如下
/**
* 核心程式碼,計算path
*
* @return
*/
private Path getPath() {
int itemWidth = waveWidth / 2;//半個波長
Path mPath = new Path();
mPath.moveTo(-itemWidth * 3, baseLine);//起始座標
Log.d(TAG, "getPath: " + baseLine);
//核心的程式碼就是這裡
for (int i = -3; i < 2; i++) {
int startX = i * itemWidth;
mPath.quadTo(
startX + itemWidth / 2 + offset,//控制點的X,(起始點X + itemWidth/2 + offset)
getWaveHeight(i),//控制點的Y
startX + itemWidth + offset,//結束點的X
baseLine//結束點的Y
);//只需要處理完半個波長,剩下的有for迴圈自已就新增了。
}
Log.d(TAG, "getPath: ");
//下面這三句話是行程封閉的效果,不明白可以將下面3句程式碼註釋看下效果的變化
mPath.lineTo(width, height);
mPath.lineTo(0, height);
mPath.close();
return mPath;
}
//奇數峰值是正的,偶數峰值是負數
private float getWaveHeight(int num) {
if (num % 2 == 0) {
return baseLine + waveHeight;
}
return baseLine - waveHeight;
}
複製程式碼
上面的程式碼畫出的水波如下圖
到這裡已經畫出了水波,但現在水波還是靜止的,要讓水波不停的移動,就要新增屬性動畫,新增動畫的程式碼如下
/**
* 不斷的更新偏移量,並且迴圈。
*/
public void updateXControl() {
//設定一個波長的偏移
ValueAnimator mAnimator = ValueAnimator.ofFloat(0, waveWidth);
mAnimator.setInterpolator(new LinearInterpolator());
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float animatorValue = (float) animation.getAnimatedValue();
offset = animatorValue;//不斷的設定偏移量,並重畫
postInvalidate();
}
});
mAnimator.setDuration(1800);
mAnimator.setRepeatCount(ValueAnimator.INFINITE);
mAnimator.start();
}
複製程式碼
修改一下onDraw
中的程式碼,如下
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mBubblesPath.reset();
//設定漸變色
Shader shader = new LinearGradient(mResultWidth / 2, mResultWidth / 2 - mInnerRadius, mResultWidth / 2, mResultWidth / 2 + mInnerRadius, Color.parseColor("#9592FB"),
Color.parseColor("#3831D4"), Shader.TileMode.CLAMP);
mBubblesPaint.setShader(shader);
//此處程式碼是下部尖角的path
mBackgroundPath.moveTo(mResultWidth / 2 - mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2);
mBackgroundPath.lineTo(mResultWidth / 2, mResultWidth / 2 + mOutRadius + mOutRadius / 4);
mBackgroundPath.lineTo(mResultWidth / 2 + mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2);
//內部氣泡的尖角
mBubblesPath.moveTo(mResultWidth / 2 - mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2 - dp2px(getContext(), 5));
mBubblesPath.lineTo(mResultWidth / 2, mResultWidth / 2 + mOutRadius + mOutRadius / 4 - dp2px(getContext(), 5));
mBubblesPath.lineTo(mResultWidth / 2 + mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2 - dp2px(getContext(), 5));
//畫外部背景
canvas.drawPath(mBackgroundPath, mBackgroundPaint);
canvas.drawCircle(mResultWidth / 2, mResultWidth / 2, mOutRadius, mBackgroundPaint);
Log.d(TAG, "cx: " + mResultWidth / 2);
//畫水波
mBubblesPath.addCircle(mResultWidth / 2, mResultWidth / 2, mInnerRadius, Path.Direction.CCW);
canvas.drawPath(getPath(), mBubblesPaint);
}
複製程式碼
好了,現在水波已經可以移動了,看下效果
what!怎麼成這個樣子了呀,明顯不是我想要的效果呀,肯定是哪裡出錯了,經過我仔細的推敲,總結了出現上面問題的原因,原因如下圖
出現上面問題的原因就是因為下面三句程式碼
mPath.lineTo(width, height);
mPath.lineTo(0, height);
mPath.close();
複製程式碼
知道是這三句程式碼的原因,那應該怎麼修改呢?這三句程式碼好像不能動,不然就會出現波浪畫的不完整的情況,額.....,那應該修改哪裡呢?靈光一閃,不是可以裁剪畫布嘛,只要將畫布裁剪成想要的形狀,然後在畫波浪不久完美了。再修改onDraw
方法,修改後的程式碼如下
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mBubblesPath.reset();
//設定漸變色
Shader shader = new LinearGradient(mResultWidth / 2, mResultWidth / 2 - mInnerRadius, mResultWidth / 2, mResultWidth / 2 + mInnerRadius, Color.parseColor("#9592FB"),
Color.parseColor("#3831D4"), Shader.TileMode.CLAMP);
mBubblesPaint.setShader(shader);
//此處程式碼是下部尖角的path
mBackgroundPath.moveTo(mResultWidth / 2 - mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2);
mBackgroundPath.lineTo(mResultWidth / 2, mResultWidth / 2 + mOutRadius + mOutRadius / 4);
mBackgroundPath.lineTo(mResultWidth / 2 + mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2);
//內部氣泡的尖角
mBubblesPath.moveTo(mResultWidth / 2 - mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2 - dp2px(getContext(), 5));
mBubblesPath.lineTo(mResultWidth / 2, mResultWidth / 2 + mOutRadius + mOutRadius / 4 - dp2px(getContext(), 5));
mBubblesPath.lineTo(mResultWidth / 2 + mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2 - dp2px(getContext(), 5));
//畫外部背景
canvas.drawPath(mBackgroundPath, mBackgroundPaint);
canvas.drawCircle(mResultWidth / 2, mResultWidth / 2, mOutRadius, mBackgroundPaint);
Log.d(TAG, "cx: " + mResultWidth / 2);
//切割畫布,畫水波
canvas.save();
mBubblesPath.addCircle(mResultWidth / 2, mResultWidth / 2, mInnerRadius, Path.Direction.CCW);
//將畫布裁剪成內部氣泡的樣子
canvas.clipPath(mBubblesPath);
canvas.drawPath(getPath(), mBubblesPaint);
canvas.restore();
}
複製程式碼
到這裡已經實現了文章開始時的效果了,文章也該結束了。
結束語
本文主要是講解怎樣實現水波氣泡,並沒有講到View的測量,貼出的也只是繪製氣泡的程式碼,完整的程式碼可以點選這裡獲取。
雖然已經擼出了這個效果,但最後專案中並沒有用這種動態的氣泡,因為氣泡多的時候是在是卡……。最後,喜歡此demo,就隨手給個star吧!
ps: 歷史文章中有乾貨哦!
本文已由公眾號“AndroidShared”首發