源起
- 公司專案用到一個進度條動畫,需要自己繪製從0載入到100的情況,而且效果需要很酷炫。最後是採用了lottie動畫+一部分自定義View來實現,拆分了出來。過後,覺得其實自己實現這樣的一個效果也不難,便開始了以下的嘗試。先看下效果
分析
- 分析下:最外層是一個由小圓點順時針旋轉的圓、第二層是直接一個圓、第三層有兩層:內層是由矩形逆時針旋轉的圓,外層是一個圓環、最後是文字展示。
- 實現思路是這樣:分成多個View來實現,第一層圓點是一個View,繪製完成是使用ObjectAnimator進行旋轉;第二層和第三層的外層和文字是一個View,第三層內層矩形是一個View,同樣是繪製完成使用ObjectAnimator旋轉。(這裡為何分成三個View咧,因為涉及到旋轉動畫,我想實現旋轉的時候,是對畫布canvas物件進行旋轉,而在一個View裡只有一個畫布canvas物件,如果所有都繪製在一個canvas上,則旋轉的時候就都會旋轉,實現不了我要的效果。如果讀者有更好的解決方案歡迎提出)
開幹
第一部分
- 進度條動畫 感謝這篇部落格作者給我提供了很好的思路,讓我舉一反三,如果讀者第一部分看的很模糊的話,可以結合這篇部落格享用
- 第一層圓點和第三層內層矩形應該怎麼繪製?這裡實現大同小異放在一起說
- 看到此圖不用望而生畏,其實很簡單,我們最終在畫布上繪製的還是一個一個的圓,藍色圓是真正要繪製的,空白圓是間隔距離,不需要繪製出來。現在總共是100個藍色圓,100個空心圓,每個圓佔據的角度是360/200=1.8度;繪製完藍色圓以後,呼叫canvas.ratate()將畫布旋轉3.6度到下一個藍色圓圓心位置開始繪製圓,最終就有100個藍色圓了。
- 怎麼繪製藍色圓?需要確定藍色圓的圓心位置和半徑。我們可以在onMeasure()中拿到View的width和height,除以2得到就是圓心座標(centerX,centerY),R0就是寬或者高/2可以得到的半徑,我們要求出r:
R1 + r = R0
R1 * sin0.9° = r
由以上兩式可以得出:r = (R0*sin0.9°)/(1+sin0.9°)——R0已知
小藍圓的圓心為:(centerX,centerY-R0+r)
- 這樣就可以繪製出來了,這裡把sin0.9°提高到sin1°,讓藍色圓大一點。
關鍵程式碼如下:(為了美觀,我將100個藍色小圓改成了50個,而且每次旋轉3.6*2=7.2度,也就是隔開了三個白色小圓的距離,繪製50次,每次轉過了7.2度,50 * 7.2=360度,每次都繪製一個藍色圓,最後是50個小藍圓)
@Override
protected void onDraw(Canvas canvas) {
Log.i("onMeasure","執行了onDraw");
super.onDraw(canvas);
mSin_1 = (float) Math.sin(Math.toRadians(1));
// 大圓半徑
float outerRadius = (getWidth() < getHeight() ? getWidth() : getHeight()) / 2f;
//小圓點半徑
float dotRadius = mSin_1 * outerRadius / (1 + mSin_1);
float centerX = getWidth() / 2f;
float centerY = getHeight() / 2f;
mPaint.setStyle(Paint.Style.FILL);
int count = 0;
while (count++ < 50) {
canvas.drawCircle(centerX, centerY - outerRadius + dotRadius, dotRadius, mPaint);
canvas.rotate(7.2f, centerX, centerY);
}
}
複製程式碼
第二部分
- 繪製矩形形成的圓,原理也是一樣的,只不過把繪製小藍圓變成繪製矩形。繪製矩形需要確定左上角和右下角這兩個點的位置就可以了。然後將畫布旋轉某一角度值繼續繪製即可。這裡繪製了50次,每次旋轉10度,總共是500度>360度,保證大於360度即可,多餘的會重複覆蓋,但如果小於360度,就會導致繪製殘缺。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setShader(null);
mPaint.setAntiAlias(true); // 抗鋸齒
mPaint.setDither(true); // 防抖動
// 半徑,這裡減去40是將半徑縮小40
float outerRadius = (getWidth() < getHeight() ? getWidth() : getHeight()) / 2f-40;
float centerX = getWidth() / 2f;
float centerY = getHeight() / 2f;
mPaint.setStyle(Paint.Style.FILL);
int count = 0;
int des = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 6, getResources().getDisplayMetrics());
while (count++ < 50) {
canvas.drawRect(centerX-3,centerY-outerRadius,centerX+3,centerY-outerRadius+des,
mPaint);
canvas.rotate(10.0f, centerX, centerY);
}
}
複製程式碼
第三部分
- 繪製圓環和圓,繪製文字這幾個是放到一個View處理的,因為不用涉及到旋轉,所以可以放到一起繪製。這部分是參考了另外一篇部落格,有興趣的讀者也可以享用 自定義進度條
- 畫圓這個比較簡單不說,圓環的話,其實就是畫圓弧,畫圓弧的時候會先確定一個外接矩形,設定畫筆型別為描邊circlePaint.setStyle(Paint.Style.STROKE);同時不包含圓心,就可以了。具體的可以看下這篇部落格 drawArc畫圓弧介紹 ,這裡不便展開。
private void drawCircle(Canvas canvas, int center, int radius)
{
//畫一個簡單的圓
firstPaint.setShader(null); // 清除上一次的shader
firstPaint.setColor(firstColor); // 設定底部圓環的顏色,這裡使用第一種顏色
firstPaint.setStyle(Paint.Style.STROKE); // 設定繪製的圓為空心
canvas.drawCircle(center, center, radius+40, firstPaint);
//畫一個圓環
RectF oval = new RectF(center - radius, center - radius, center + radius, center + radius);
circlePaint.setShader(null);
// 繪製顏色漸變圓環
// shader類是Android在圖形變換中非常重要的一個類。Shader在三維軟體中我們稱之為著色器,其作用是來給影象著色。
LinearGradient linearGradient = new LinearGradient(circleWidth, circleWidth, getMeasuredWidth()
- circleWidth, getMeasuredHeight() - circleWidth, colorArray, null, Shader.TileMode.MIRROR);
circlePaint.setShader(linearGradient);
//這裡注意設定為描邊型別
circlePaint.setStyle(Paint.Style.STROKE);
circlePaint.setShadowLayer(10, 10, 10, Color.RED);
circlePaint.setColor(secondColor); // 設定圓弧的顏色
circlePaint.setStrokeCap(Paint.Cap.ROUND); // 把每段圓弧改成圓角的
// 計算每次畫圓弧時掃過的角度,這裡計算要注意分母要轉為float型別,否則alphaAngle永遠為0
alphaAngle = currentValue * 360.0f / maxValue * 1.0f;
canvas.drawArc(oval, -90, alphaAngle, false, circlePaint);
}
複製程式碼
- 可以看到繪製圓環的時候,用到alphaAngle,這個是掃過的角度,根據currentValue當前的進度值,得出當前掃過的角度。maxValue是100最大進度值。更新進度的時候會呼叫以下這個方法進行更新,在invalidate()被呼叫後,View會進行重繪,回撥onDraw()方法,得出新的alphaAngle角度值,繪製出對應進度的圓環。
public void setProgress(int progress)
{
int percent = progress * maxValue / 100;
if (percent < 0)
{
percent = 0;
}
if (percent > 100)
{
percent = 100;
}
this.currentValue = percent;
//更新View
invalidate();
}
複製程式碼
- 繪製文字也是同樣的道理,通過setProgress()方法來更新currentValue值。這裡繪製文字有個注意的地方:文字的繪製位置居中。drawText()方法的第二三個引數分別是Text文字x,y座標,這裡為什麼設定為center和baseline這兩個值是有原因的,牆裂推薦這篇部落格瞭解原因:drawText介紹 這部分的主要程式碼如下:
private void drawText(Canvas canvas, int center, int radius)
{
float result = (currentValue * 100.0f / maxValue * 1.0f); // 計算進度
String percent = String.format("%.1f", result) + "%";
textPaint.setTextAlign(Paint.Align.CENTER); // 設定文字居中,文字的x座標要注意
textPaint.setColor(textColor); // 設定文字顏色
textPaint.setTextSize(40); // 設定要繪製的文字大小
textPaint.setStrokeWidth(0); // 注意此處一定要重新設定寬度為0,否則繪製的文字會重疊
Rect bounds = new Rect(); // 文字邊框
textPaint.getTextBounds(percent, 0, percent.length(), bounds); // 獲得繪製文字的邊界矩形
Paint.FontMetricsInt fontMetrics = textPaint.getFontMetricsInt(); // 獲取繪製Text時的四條線
int baseline = center + (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom;
canvas.drawText(percent, center, baseline, textPaint); // 繪製表示進度的文字
}
複製程式碼
總結
- 寫到這其實還沒完,只是對裡面的重點剖析了下,剩下的細節讀者可以看下原始碼,我也封裝成了一個依賴庫,有需要可以使用。對應的demo使用放到github上,喜歡的可以點個star。
- 在專案的build.gradle中新增:
allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}
複製程式碼
- 在module的build.gradle中新增:
implementation 'com.github.guoxiaozhou:AnimationDemo:0.5'
複製程式碼
- 具體使用demo地址:github.com/guoxiaozhou… 專案中包含依賴庫原始碼,有興趣的可以看看細節,有問題或者錯誤直接聯絡我