博主聲明:
轉(zhuǎn)載請在開頭附加本文鏈接及作者信息荒叶,并標記為轉(zhuǎn)載。本文由博主 威威喵 原創(chuàng)输虱,請多支持與指教些楣。
本文首發(fā)于此 博主:威威喵 | 博客主頁:https://blog.csdn.net/smile_running
博主這幾天一直在搞貝塞爾曲線(Bezier)動畫的研究,雖然我的數(shù)學不太好,但是也勉勉強強能夠看懂懂貝塞爾曲線的公式愁茁,套用還是很簡單的蚕钦。前幾次搞了幾個貝塞爾曲線動畫效果,感覺那個效果還是非常贊的鹅很,今天興致又來了嘶居,于是去搜索了一下 Android 相關的貝塞爾曲線的動畫實例,偶然看到一個 Android 充電進度的貝塞爾曲線動畫促煮,它的效果圖如下:
看到這個效果呢邮屁,我首先是想到用三階貝塞爾曲線公式來做,于是就屁顛屁顛的開始了污茵,套了三階貝塞爾曲線的公式樱报,發(fā)現(xiàn)效果沒出來,臥槽泞当。害我白高興一場迹蛤,以為我的數(shù)學還是可以的,結果襟士。盗飒。。
我最先的想法是通過點位去計算波形路徑陋桂,不過最后放棄了逆趣。哈哈,喜出望外嗜历,結果我發(fā)現(xiàn)了一個更簡單的做法宣渗,用 Path 類下面的一個三階貝塞爾曲線的封裝方法,很簡單就實現(xiàn)了波浪的效果梨州,這是我寫這個效果時所收獲到的意外驚喜痕囱,之前還沒字母使用過,接下來我們進行分析這個效果的實現(xiàn)暴匠,然后再講解一下 Path 類三階貝塞爾的簡單用法鞍恢。
多的就不扯淡了,我們直接開始吧每窖。國際慣例帮掉,先來看看最終的實現(xiàn)效果圖:
這個充電進度的動畫效果還行吧,上面我搜索到的是一張靜態(tài)圖窒典,我就是依照這那張圖的樣式做的蟆炊,可能顏色又一點點缺陷,這個自己再美化美化就好啦瀑志。
來吧盅称,拿到這個效果圖肩祥,首先就是分析一波。來看一下草圖
看上面那張圖缩膝,首先我們要把圓繪制到中心點吧混狠,這沒什么問題。因為三階貝塞爾曲線需要 2 個控制點疾层,從圖中我們知道 p1 和 p2 就是那條曲線的控制點将饺, 而且上圖 p1 p2 p3 p4 四個點獲取坐標都很容易。
//內(nèi)部
pIn0 = new PointF(rippleX - mDefCircleRadius, rippleY);
pIn1 = new PointF(rippleX - mRandom.nextInt((int) mDefCircleRadius), rippleY - mRandom.nextInt((int) (mDefCircleRadius / 4)));
pIn2 = new PointF(rippleX + mRandom.nextInt((int) mDefCircleRadius), rippleY + mRandom.nextInt((int) (mDefCircleRadius / 4)));
pIn3 = new PointF(rippleX + mDefCircleRadius, rippleY);
因為海浪波紋有兩條曲線組成痛黎,這兩條曲線是交錯的予弧,所以我們需要再來 4 個點
// 外部
pExt0 = new PointF(rippleX - mDefCircleRadius, rippleY);
pExt1 = new PointF(rippleX - mRandom.nextInt((int) mDefCircleRadius), rippleY + mRandom.nextInt((int) (mDefCircleRadius / 3)));
pExt2 = new PointF(rippleX + mRandom.nextInt((int) mDefCircleRadius), rippleY + mRandom.nextInt((int) (mDefCircleRadius / 3)));
pExt3 = new PointF(rippleX + mDefCircleRadius, rippleY);
得到曲線的點之后呢,我們就可以開始用 Path 類的一個方法去形成曲線的路徑了湖饱,因為波浪是有顏色的掖蛤,所以需要把 Path 給封閉起來,形成密閉的效果井厌。接著蚓庭,再來看一張草圖
用 Path 類制作一條曲線,并且我們要把 p0 ~ p5 這幾個點給封閉起來仅仆,形成海浪的效果器赞。想法是不錯,但是你會發(fā)現(xiàn)墓拜,這個形成的區(qū)域已經(jīng)超出了圓的范圍了吧港柜,那樣子就非常丑,猶如這個樣子:
圓圈外面多出了兩個藍色部分區(qū)域咳榜,丑的不行啊夏醉。 像這個樣子的情況,我最先想到的是 canvas 有沒有畫剪切區(qū)域的涌韩,后來找了一下畔柔,好像沒找到。陷入深思贸辈,后來靈機一動释树,想到我上一次實現(xiàn)的一種效果肠槽,是畫一個圓擎淤,從內(nèi)到外擴散的,感興趣的可以點擊鏈接秸仙,去看看我的文章:Android 視差動畫 — 雅虎新聞內(nèi)容揭示效果
這個圓效果呢嘴拢,就是從小變到大,逐漸的把內(nèi)容呈現(xiàn)出來寂纪。這就給我一個很好的啟示席吴,我可以繪制一個這樣的圓赌结,把外面藍色部分遮住不久好了嘛,也就相當于除了綠色包含的圓以外全部給遮住孝冒,這樣顯示的效果只能看到這個綠色的圓了柬姚,我們的目的也就達到了。這個就需要對畫筆的寬度進行計算庄涡,代碼如下:
private void drawMasked(Canvas canvas) {
//繪制一個遮罩層量承,屏蔽 Path Close 以外的區(qū)域
mMaskPaint.setStrokeWidth(mDiagonal + mDefCircleRadius * 2 - mPaintSize * 1.5f);
canvas.drawCircle(mCircleX, mCircleY, mDiagonal, mMaskPaint);
}
這樣就把露出來的藍色區(qū)域給遮擋住了,接下來還有一個難點穴店,就是如何根據(jù)進度值把海浪也給升高撕捍,總不能在固定位置浪啊浪吧。這就要考慮一個問題泣洞,我們需要根據(jù)圓的直徑和進度值的一個比例關系忧风,計算出當前海平面的高度,通過不斷的增加 progress(進度)球凰,海平面會隨著進度升高狮腿,而且這個期間波浪一直在流動的。這部分關鍵代碼如下:
// 直徑與進度的比例
rippleScale = 2 * mDefCircleRadius / 100;
// 繪制海浪的波紋效果弟蚀,分內(nèi)部和外部兩條
private void drawExternalRipple(Canvas canvas) {
// 計算進度的 x , y 位置
y = mCircleY - mDefCircleRadius + (100 - mProgress) * rippleScale;
x = caculateX(y);
float rippleY = y;
float rippleX = mCircleX;
//內(nèi)部
pIn0 = new PointF(rippleX - mDefCircleRadius, rippleY);
pIn1 = new PointF(rippleX - mRandom.nextInt((int) mDefCircleRadius), rippleY - mRandom.nextInt((int) (mDefCircleRadius / 4)));
pIn2 = new PointF(rippleX + mRandom.nextInt((int) mDefCircleRadius), rippleY + mRandom.nextInt((int) (mDefCircleRadius / 4)));
pIn3 = new PointF(rippleX + mDefCircleRadius, rippleY);
Path inPath = new Path();
inPath.moveTo(pIn0.x, pIn0.y);
inPath.cubicTo(pIn1.x, pIn1.y, pIn2.x, pIn2.y, pIn3.x, pIn3.y);
inPath.lineTo(mCircleX + mDefCircleRadius, mCircleY + mDefCircleRadius);
inPath.lineTo(mCircleX - mDefCircleRadius, mCircleY + mDefCircleRadius);
inPath.close();
canvas.drawPath(inPath, mInnerPaint);
// 外部
pExt0 = new PointF(rippleX - mDefCircleRadius, rippleY);
pExt1 = new PointF(rippleX - mRandom.nextInt((int) mDefCircleRadius), rippleY + mRandom.nextInt((int) (mDefCircleRadius / 3)));
pExt2 = new PointF(rippleX + mRandom.nextInt((int) mDefCircleRadius), rippleY + mRandom.nextInt((int) (mDefCircleRadius / 3)));
pExt3 = new PointF(rippleX + mDefCircleRadius, rippleY);
Path extPath = new Path();
extPath.moveTo(pExt0.x, pExt0.y);
extPath.cubicTo(pExt1.x, pExt1.y, pExt2.x, pExt2.y, pExt3.x, pExt3.y);
extPath.lineTo(mCircleX + mDefCircleRadius, mCircleY + mDefCircleRadius);
extPath.lineTo(mCircleX - mDefCircleRadius, mCircleY + mDefCircleRadius);
extPath.close();
canvas.drawPath(extPath, mExternalPaint);
}
上面代碼是計算進度條和圓的直徑的比例蚤霞,通過這個比例,我們可以拿到 path 中波浪逐漸上升的 y 坐標义钉,通過不斷的繪制 path 然后形成波浪的動畫效果昧绣,直到進度條為 100 時,我們就進行判斷處理
public void setProgress(int progress) {
this.mProgress = progress;
this.mArcProgress = mProgress * 3.6f;
if (mProgress <= 100) {
isFinished = false;
} else {
isFinished = true;
}
invalidate();
}
如果進度達到 100捶闸,我們就開始繪制完成時候的動畫夜畴,代碼如下
private void drawFinished(Canvas canvas) {
canvas.drawCircle(mCircleX, mCircleY, mDefCircleRadius, mArcPaint);
canvas.drawCircle(mCircleX, mCircleY, mDefCircleRadius, mInnerPaint);
canvas.drawText("充電完成", mCircleX - mTextPaint.getTextSize() * 2f, mCircleY + mTextPaint.getTextSize() / 2, mTextPaint);
}
只有這樣,當結束是才會顯示不同的效果删壮,否則不做處理的話贪绘,就是空空如也啦。
那么至此央碟,我們對這個效果的分析也就完成了税灌,并且手動進實現(xiàn)了一下,感覺收獲了不少亿虽,哈哈菱涤。最后呢,給出本效果的完整代碼洛勉,如下:
package nd.no.xww.qqmessagedragview;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import java.util.Random;
/**
* @author xww
* @desciption :
* @date 2019/8/6
* @time 12:11
* 博主:威威喵
* 博客:https://blog.csdn.net/smile_Running
*/
public class ChargeBezierView extends View {
private Paint mExternalPaint;
private Paint mInnerPaint;
private Paint mArcPaint;
private Paint mCirclePaint;
private Paint mTextPaint;
private Paint mMaskPaint;
private int mWidth;
private int mHeight;
// 充電進度值百分制
private int mProgress;
private float mArcProgress;
private float mPaintSize;
//水波紋于進度條的高度比
private float rippleScale;
//用于畫進度
private RectF mRect;
private Random mRandom;
private float mCircleX;
private float mCircleY;
private float mDefCircleRadius;
// 對角線的長度
private float mDiagonal;
private boolean isFinished = false;
//水波紋高度坐標
private float x;
private float y;
private void init() {
mExternalPaint = getPaint(Color.parseColor("#554F94CD"));
mInnerPaint = getPaint(Color.parseColor("#66B8FF"));
mArcPaint = getPaint(Color.parseColor("#7FFF00"));
mArcPaint.setStyle(Paint.Style.STROKE);//空心
mCirclePaint = getPaint(Color.parseColor("#F8F8FF"));
mCirclePaint.setStyle(Paint.Style.STROKE);//空心
mTextPaint = getPaint(Color.parseColor("#FF00ff"));
mMaskPaint = getPaint(Color.parseColor("#FFFFFF"));
mMaskPaint.setStyle(Paint.Style.STROKE);
mRandom = new Random();
mPaintSize = mTextPaint.getTextSize();
}
private Paint getPaint(int color) {
Paint paint = new Paint();
paint.setDither(true);
paint.setAntiAlias(true);
paint.setStrokeWidth(18f);
paint.setTextSize(60f);
paint.setColor(color);
return paint;
}
public ChargeBezierView(Context context) {
this(context, null);
}
public ChargeBezierView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public ChargeBezierView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@SuppressLint("DrawAllocation")
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mWidth = MeasureSpec.getSize(widthMeasureSpec);
mHeight = MeasureSpec.getSize(heightMeasureSpec);
mCircleX = mWidth / 2;
mCircleY = mHeight / 2;
mDefCircleRadius = mWidth / 4;
mRect = new RectF(mCircleX - mDefCircleRadius, mCircleY - mDefCircleRadius,
mCircleX + mDefCircleRadius, mCircleY + mDefCircleRadius);
mDiagonal = (float) Math.sqrt(Math.pow(mCircleX, 2) + Math.pow(mCircleY, 2));
rippleScale = 2 * mDefCircleRadius / 100;
}
@Override
protected void onDraw(Canvas canvas) {
if (isFinished) {
drawMasked(canvas);
drawFinished(canvas);
} else {
drawExternalRipple(canvas);
drawMasked(canvas);
drawProgressText(canvas);
drawCircle(canvas);
drawProgress(canvas);
}
}
// 繪制電量圓形軌道
private void drawCircle(Canvas canvas) {
canvas.drawCircle(mCircleX, mCircleY, mDefCircleRadius, mCirclePaint);
}
private void drawProgress(Canvas canvas) {
// -90 表示從上半軸 x=0 開始
canvas.drawArc(mRect, -90, mArcProgress, false, mArcPaint);
}
private void drawProgressText(Canvas canvas) {
canvas.drawText(mProgress + "%", mCircleX - mPaintSize, mCircleY + mTextPaint.getTextSize() / 2, mTextPaint);
}
private void drawMasked(Canvas canvas) {
//繪制一個遮罩層粘秆,屏蔽 Path Close 以外的區(qū)域
mMaskPaint.setStrokeWidth(mDiagonal + mDefCircleRadius * 2 - mPaintSize * 1.5f);
canvas.drawCircle(mCircleX, mCircleY, mDiagonal, mMaskPaint);
}
private void drawFinished(Canvas canvas) {
canvas.drawCircle(mCircleX, mCircleY, mDefCircleRadius, mArcPaint);
canvas.drawCircle(mCircleX, mCircleY, mDefCircleRadius, mInnerPaint);
canvas.drawText("充電完成", mCircleX - mTextPaint.getTextSize() * 2f, mCircleY + mTextPaint.getTextSize() / 2, mTextPaint);
}
private PointF pExt0;
private PointF pExt1;
private PointF pExt2;
private PointF pExt3;
private PointF pIn0;
private PointF pIn1;
private PointF pIn2;
private PointF pIn3;
ValueAnimator externalAnimator;
// 繪制海浪的波紋效果,分內(nèi)部和外部兩條
private void drawExternalRipple(Canvas canvas) {
// 計算進度的 x , y 位置
y = mCircleY - mDefCircleRadius + (100 - mProgress) * rippleScale;
x = caculateX(y);
float rippleY = y;
float rippleX = mCircleX;
//內(nèi)部
pIn0 = new PointF(rippleX - mDefCircleRadius, rippleY);
pIn1 = new PointF(rippleX - mRandom.nextInt((int) mDefCircleRadius), rippleY - mRandom.nextInt((int) (mDefCircleRadius / 4)));
pIn2 = new PointF(rippleX + mRandom.nextInt((int) mDefCircleRadius), rippleY + mRandom.nextInt((int) (mDefCircleRadius / 4)));
pIn3 = new PointF(rippleX + mDefCircleRadius, rippleY);
Path inPath = new Path();
inPath.moveTo(pIn0.x, pIn0.y);
inPath.cubicTo(pIn1.x, pIn1.y, pIn2.x, pIn2.y, pIn3.x, pIn3.y);
inPath.lineTo(mCircleX + mDefCircleRadius, mCircleY + mDefCircleRadius);
inPath.lineTo(mCircleX - mDefCircleRadius, mCircleY + mDefCircleRadius);
inPath.close();
canvas.drawPath(inPath, mInnerPaint);
// 外部
pExt0 = new PointF(rippleX - mDefCircleRadius, rippleY);
pExt1 = new PointF(rippleX - mRandom.nextInt((int) mDefCircleRadius), rippleY + mRandom.nextInt((int) (mDefCircleRadius / 3)));
pExt2 = new PointF(rippleX + mRandom.nextInt((int) mDefCircleRadius), rippleY + mRandom.nextInt((int) (mDefCircleRadius / 3)));
pExt3 = new PointF(rippleX + mDefCircleRadius, rippleY);
Path extPath = new Path();
extPath.moveTo(pExt0.x, pExt0.y);
extPath.cubicTo(pExt1.x, pExt1.y, pExt2.x, pExt2.y, pExt3.x, pExt3.y);
extPath.lineTo(mCircleX + mDefCircleRadius, mCircleY + mDefCircleRadius);
extPath.lineTo(mCircleX - mDefCircleRadius, mCircleY + mDefCircleRadius);
extPath.close();
canvas.drawPath(extPath, mExternalPaint);
}
public void setProgress(int progress) {
this.mProgress = progress;
this.mArcProgress = mProgress * 3.6f;
if (mProgress <= 100) {
isFinished = false;
} else {
isFinished = true;
}
invalidate();
}
// 圓的方程式 a2 = b2 + c2
private float caculateX(float y) {
x = (float) Math.sqrt(Math.pow(mDefCircleRadius, 2) - y * y);
return x;
}
}
還有一個是進行進度值設置的收毫,這個很簡單攻走,在 MainActivity 里面開一個子線程殷勘,然后設置一下進度值就可以了
chargeView = findViewById(R.id.chargeView);
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
progress++;
if (progress > 100) {
progress = 101;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
runOnUiThread(new Runnable() {
@Override
public void run() {
chargeView.setProgress(progress);
}
});
}
}
}).start();
使用起來就是這么簡單,不過還有一些與貝塞爾曲線相關的知識沒有介紹昔搂,感興趣的話玲销,可以去看我之前寫的幾篇文章,里面有關于貝塞爾的介紹摘符,還有一些比較炫酷的 Android 動畫效果哦痒玩。