前不久寫了一個(gè)六邊形的進(jìn)度條,個(gè)人感覺仍舊有很多美中不足(好吧,這里把“美中”去掉 - -)的地方,比如說像之前一位同事提到的View繪制邊界其實(shí)線條有一半是無法顯示的。所以在這此的代碼中將這個(gè)問題考慮了進(jìn)去,而且UI設(shè)計(jì)師認(rèn)為平角六邊形的效果有些粗糙,這里改成了圓角。還有就是先前的版本代碼上過于冗余,這里也盡我所能的簡化了下代碼。這里說下思路,具體的實(shí)現(xiàn)細(xì)節(jié)可以在文章的結(jié)尾處下載源碼。
先簡單的看一下效果圖:

實(shí)現(xiàn)步驟
- 首先,我們在最底層放上一個(gè)扇形的進(jìn)度條
(github下載地址:https://github.com/timqi/SectorProgressView)。

2.將一張圓角六邊形的圖片覆蓋在上面

3.在蓋上一張這樣的圖片把多余的部分遮住。

就像這樣

怎么樣?是不是感覺so easy..... : )
.
.
.
.
.
.
.
.
呵呵。。。。。。怎么可能 - - 。
.
.
.
.
.
.
.
好吧,我承認(rèn),其實(shí)我一開始就想用這個(gè)方法蒙混過關(guān)的,畢竟,我比較懶。(UI那邊看過也說這個(gè)可以通過,怎么說這個(gè)方案如果沒有他切片我也無法實(shí)行- -)。但是在我給ios端開發(fā)的同事確認(rèn)方案的時(shí)候,一切都變了- -,只聽他淡淡的飄出了一個(gè)字,鄙視中帶著蔑視,蔑視中帶著無視。。。。。。:好丑,你這進(jìn)度條下來是歪的啊? 然后他默默的在網(wǎng)上找了個(gè)開源的解決了這個(gè)歪的問題。- -

( 丑就算了,他竟然還加了一個(gè)修飾詞),不過不是他的提醒,我也不會(huì)想到把這個(gè)進(jìn)度條認(rèn)認(rèn)真真的重頭寫一遍。
好了,廢話到這里 (畢竟在描述一個(gè)故事的時(shí)候總要交代下故事背景),接下來進(jìn)入正題。
公共API
/**
* 設(shè)置進(jìn)度
*
* @param progress 當(dāng)前進(jìn)度
*/
public void setProgress(float progress)
/**
* 設(shè)置進(jìn)度條的寬度
*
* @param width 進(jìn)度條的寬度 dp值
*/
public void setProgressWith(float width)
/**
* 設(shè)置中央背景的顏色
*
* @param color 中央背景顏色
*/
public void setCenterColor(int color)
/**
* 設(shè)置已完成進(jìn)度條的顏色
*
* @param color 已完成的進(jìn)度條顏色
*/
public void setFinishedColor(int color)
/**
* 設(shè)置未完成進(jìn)度條的顏色
*
* @param color 未完成的進(jìn)度條的顏色
*/
public void setUnfinishedColor(int color)
沒興趣的朋友下面的內(nèi)容都可以省略并直接翻到文末,點(diǎn)擊相應(yīng)鏈接下載控件即可。
點(diǎn)的計(jì)算
首先,我們先考慮繪制未完成的進(jìn)度條,和中間的背景圖案。這里我們可以明確的看出,軌跡是由六條60°的圓弧和六條線段組成。所在在繪制之前,我們需要確定圓角的六個(gè)圓弧所在的圓的位置,也就是RecfF中的(left,top,right,bottom)的參數(shù)(此處具體參照自定義View中是如何繪制圓弧的,google or baidu),即每個(gè)圓所在外切矩形的左上角坐標(biāo)和右下角的坐標(biāo)。和每段圓弧連接的線段的兩個(gè)端點(diǎn)的的坐標(biāo)。 也就是總共24個(gè)點(diǎn)的坐標(biāo)。由于邊界顯示只會(huì)顯示一半線寬的問題,這里我們在邊界處計(jì)算坐標(biāo)的時(shí)候還要考慮到線粗的問題,頂部和左邊分別考慮到縱坐標(biāo)和橫坐標(biāo)加上一半線寬,底部和右邊則考慮在縱坐標(biāo)和橫坐標(biāo)上減去一半的線寬。
關(guān)于24個(gè)點(diǎn)的坐標(biāo)的計(jì)算具體代碼如下,此處 l 表示此圓角六邊形所在的外切平角正六邊形的長度,r 則表示圓弧所在圓的半徑。mProgressWidth表示進(jìn)度條寬度的一半。
具體怎么計(jì)算的這里參考下圖and代碼。

/**
* 初始化各個(gè)點(diǎn)
*/
private void initPoints() {
//六條線段的十二個(gè)端點(diǎn),從進(jìn)度開始位置的第一條線段開始順時(shí)針計(jì)算
linePoints = new PointView[12];
linePoints[0] = new PointView((float) (l * cos30 - r * tan30 * cos30 + r),
(float) (r * tan30 * sin30 + mProgressWidth));
linePoints[1] = new PointView((float) (l * cos30 * 2 - r * tan30 * cos30 -
mProgressWidth), (float) (l * sin30 - r * tan30 * sin30));
linePoints[2] = new PointView((float) (l * cos30 * 2 - mProgressWidth),
(float) (l * sin30 + r * tan30));
linePoints[3] = new PointView((float) (l * cos30 * 2 - mProgressWidth),
(float) (l * sin30 + l - r * tan30));
linePoints[4] = new PointView((float) (l * cos30 * 2 - r * tan30 * cos30 -
mProgressWidth), (float) (l * sin30 + l + r * tan30 * sin30));
linePoints[5] = new PointView((float) (l * cos30 + r * tan30 * cos30),
(float) (l * sin30 * 2 + l - r * tan30 * sin30 - mProgressWidth));
linePoints[6] = new PointView((float) (l * cos30 - r * tan30 * cos30),
(float) (l * sin30 * 2 + l - r * tan30 * sin30 - mProgressWidth));
linePoints[7] = new PointView((float) (r * sin30 + mProgressWidth),
(float) (l * sin30 + l + r * tan30 * sin30));
linePoints[8] = new PointView(0 + mProgressWidth,
(float) (l * sin30 + l - r * tan30));
linePoints[9] = new PointView(0 + mProgressWidth,
(float) (l * sin30 + r * tan30));
linePoints[10] = new PointView((float) (r * sin30 + mProgressWidth),
(float) (l * sin30 - r * tan30 * sin30));
linePoints[11] = new PointView((float) (l * cos30 - r * tan30 * cos30),
(float) (r * tan30 * sin30 + mProgressWidth));
//六條圓弧的所在的圓的外接矩形的左上角坐標(biāo)和右下角坐標(biāo)(用于確定遠(yuǎn)的位置),
//從進(jìn)度開始位置的第一條圓弧開始順時(shí)針計(jì)算
arcPoints = new PointView[12];
arcPoints[0] = new PointView((float) (l * cos30 - r),
(float) (r / cos30 - r + mProgressWidth));
arcPoints[1] = new PointView((float) (l * cos30 + r),
(float) (r / cos30 - r + 2 * r + mProgressWidth));
arcPoints[2] = new PointView((float) (l * cos30 * 2 - r * 2 - mProgressWidth),
(float) (l * sin30 + r * tan30 - r));
arcPoints[3] = new PointView((float) (l * cos30 * 2 - mProgressWidth),
(float) (l * sin30 + r * tan30 + r));
arcPoints[4] = new PointView((float) (l * cos30 * 2 - r * 2 - mProgressWidth),
(float) (l * sin30 + l - r * tan30 - r));
arcPoints[5] = new PointView((float) (l * cos30 * 2 - mProgressWidth),
(float) (l * sin30 + l - r * tan30 + r));
arcPoints[6] = new PointView((float) (l * cos30 - r),
(float) (l * sin30 * 2 + l - r / cos30 - r - mProgressWidth));
arcPoints[7] = new PointView((float) (l * cos30 + r),
(float) (l * sin30 * 2 + l - r / cos30 + r - mProgressWidth));
arcPoints[8] = new PointView(0 + mProgressWidth,
(float) (l * sin30 + l - r * tan30 - r));
arcPoints[9] = new PointView(r * 2 + mProgressWidth,
(float) (l * sin30 + l - r * tan30 + r));
arcPoints[10] = new PointView(0 + mProgressWidth,
(float) (l * sin30 + r * tan30 - r));
arcPoints[11] = new PointView(r * 2 + mProgressWidth,
(float) (l * sin30 + r * tan30 + r));
}
繪制背景和未完成的進(jìn)度條軌跡
計(jì)算出這些點(diǎn)的坐標(biāo)后我們就可以調(diào)用canvas中的drawArc()和drawLine()方法來繪制圓弧和線段了,為了使代碼看上去更簡潔,我們這里可以通過循環(huán)來做。將這寫坐標(biāo)放到數(shù)組中也是為了方便循環(huán)。
/**
* 畫未完成的進(jìn)度條
*/
private void drawUnfinishedProgress(Canvas canvas) {
for (int i = 0; i < 6; i++) {
RectF rectF = new RectF(arcPoints[i * 2].x, arcPoints[i * 2].y,
arcPoints[i * 2 + 1].x, arcPoints[i * 2 + 1].y);
canvas.drawArc(rectF, -120 + i * 60, 60, false, mUnfinishedPaint);
canvas.drawLine(linePoints[i * 2].x, linePoints[i * 2].y,
linePoints[i * 2 + 1].x, linePoints[i * 2 + 1].y, mUnfinishedPaint);
}
}
/**
* 畫背景
*/
private void drawBg(Canvas canvas) {
//線段部分
Path path = new Path();
path.moveTo(linePoints[0].x, linePoints[0].y);
for (int i = 1; i < 12; i++) {
path.lineTo(linePoints[i].x, linePoints[i].y);
}
path.close();
canvas.drawPath(path, mBgPaint);
//圓弧部分
for (int i = 0; i < 6; i++) {
RectF rectF = new RectF(arcPoints[i * 2].x, arcPoints[i * 2].y,
arcPoints[i * 2 + 1].x, arcPoints[i * 2 + 1].y);
canvas.drawArc(rectF, -120 + i * 60, 60, true, mBgPaint);
}
}
進(jìn)度條的繪制
1.進(jìn)度區(qū)間的劃分
接下來就是一個(gè)最復(fù)雜的部分,也就是具體進(jìn)度的繪制。首先這里的第一段圓弧是特殊的,因?yàn)檫M(jìn)度條開始是以第一段圓弧的中點(diǎn)作為起點(diǎn),順時(shí)針走,直到回到第一段圓弧的中點(diǎn)。這里我將進(jìn)度條分成了十三個(gè)區(qū)間,起點(diǎn)到每一個(gè)區(qū)間右邊界軌跡的長度與整個(gè)圖形的周長比作為具體進(jìn)度區(qū)間的范圍 。如下圖所示。

具體代碼如下:mc代表的是每段弧的長度,ml則代表每條線段的長度(這里再次說明之前的l 表示是圓角六邊形所對應(yīng)的外接平角六邊形的邊長)。
/**
* 進(jìn)度節(jié)點(diǎn)的計(jì)算
*/
private void calculateProgress() {
mc = (float) (PI * r * 2 / 6);
ml = (float) (l - (r * tan30 * 2));
c = (float) (PI * r * 2 + 6 * ml);
progressParts[0] = ((mc / 2) / c) * 100;
for (int i = 1; i < 12; i++) {
if (i % 2 == 1) {
progressParts[i] = progressParts[i - 1] + ml / c * 100;
} else if (i % 2 == 0) {
progressParts[i] = progressParts[i - 1] + mc / c * 100;
}
}
progressParts[12] = 100;
}
這里我們將所有的區(qū)間打印出來看一下結(jié)果。
D/MagicalProgress: 區(qū)間范圍:(0 ,1.7836796]
D/MagicalProgress: 區(qū)間范圍:(1.7836796 ,14.882988]
D/MagicalProgress: 區(qū)間范圍:(14.882988 ,18.450348]
D/MagicalProgress: 區(qū)間范圍:(18.450348 ,31.549656]
D/MagicalProgress: 區(qū)間范圍:(31.549656 ,35.117016]
D/MagicalProgress: 區(qū)間范圍:(35.117016 ,48.216324]
D/MagicalProgress: 區(qū)間范圍:(48.216324 ,51.783684]
D/MagicalProgress: 區(qū)間范圍:(51.783684 ,64.882996]
D/MagicalProgress: 區(qū)間范圍:(64.882996 ,68.450356]
D/MagicalProgress: 區(qū)間范圍:(68.450356 ,81.54967]
D/MagicalProgress: 區(qū)間范圍:(81.54967 ,85.11703]
D/MagicalProgress: 區(qū)間范圍:(85.11703 ,98.21634]
D/MagicalProgress: 區(qū)間范圍:(98.21634 ,100.0]
2.繪制已完成的軌跡
每次當(dāng)我們的進(jìn)度條走到一個(gè)新的區(qū)間的時(shí)候,我們要將之前走過的區(qū)間填充完整,舉個(gè)栗子,當(dāng)我們的進(jìn)度走到34%的時(shí)候,我們處在第四個(gè)區(qū)間,這里我們還要將前三個(gè)區(qū)間的軌跡也繪制好。具體代碼如下。
/**
* 繪制已完成的軌跡
*
* @param index 所處區(qū)間的上一個(gè)區(qū)間的索引
*/
private void drawTrail(Canvas canvas, int index) {
if (index < 0 || index > 13) {
throw new IllegalArgumentException("the index must
less than 14 and more than 0");
}
for (int i = 0; i < index; i++) {
//頭部
if (i == 0) {
RectF rectF = new RectF(arcPoints[0].x, arcPoints[0].y,
arcPoints[1].x, arcPoints[1].y);
canvas.drawArc(rectF, -90, 30, false, mFinishedPaint);
//線段部分
} else if (i % 2 == 1)
canvas.drawLine(linePoints[i - 1].x, linePoints[i - 1].y,
linePoints[i].x, linePoints[i].y, mFinishedPaint);
//圓弧部分
} else if (i != 0 && i != 12 && i % 2 == 0) {
RectF rectF = new RectF(arcPoints[i].x, arcPoints[i].y,
arcPoints[i + 1].x, arcPoints[i + 1].y);
canvas.drawArc(rectF, -120 + i * 30, 60, false, mFinishedPaint);
//尾部
} else if (i == 12) {
RectF rectF = new RectF(arcPoints[0].x, arcPoints[0].y,
arcPoints[1].x, arcPoints[1].y);
canvas.drawArc(rectF, -120, 30, false, mFinishedPaint);
}
}
}
3.進(jìn)度條的繪制
接下來我們就可以進(jìn)行進(jìn)度條的繪制了,進(jìn)度條區(qū)間如果處在圓弧內(nèi),我們只要算出現(xiàn)在進(jìn)度占所在區(qū)間的比例(參照公式p的計(jì)算方法)再乘以60度,讓他繪制對應(yīng)角度的圓弧即可。至于線段可能會(huì)超微復(fù)雜一點(diǎn)??梢詤⒄障聢D,p為當(dāng)前進(jìn)度占所在區(qū)間的百分比,(x, y)指當(dāng)前進(jìn)度所在的坐標(biāo)。
p 的計(jì)算公式:
float percent = (mProgress - progressParts[i]) /
(progressParts[i + 1] - progressParts[i]);

之后得出了如下的通項(xiàng)公式:
**x = x(i) + [x(i+1) - x(i)] * p **
**y = y(i) + [y(i+1) - y(i)] * p **
有了通項(xiàng)之后我們明顯這里采用循環(huán)的方法來簡潔代碼(好吧- -,我承認(rèn),之前那個(gè)確實(shí)就是坨shit),由于首尾比較特殊,特別我們這里頭部和尾部單獨(dú)拎出來做:
/**
* 畫進(jìn)度條
*/
private void drawProgress(Canvas canvas) {
//頭部的繪制
if (mProgress > 0 && mProgress <= progressParts[0]) {
RectF rectFOne = new RectF(arcPoints[0].x, arcPoints[0].y,
arcPoints[1].x, arcPoints[1].y);
float oval = mProgress / progressParts[0] * 30;
canvas.drawArc(rectFOne, -90, oval, false, mFinishedPaint);
}
for (int i = 0; i < 11; i++) {
if (mProgress > progressParts[i] && mProgress <= progressParts[i + 1]) {
drawTrail(canvas, i + 1);
float percent = (mProgress - progressParts[i]) /
(progressParts[i + 1] - progressParts[i]);
//線段部分的繪制
if (i % 2 == 0) {
canvas.drawLine(linePoints[i].x, linePoints[i].y,
linePoints[i].x + ((linePoints[i + 1].x - linePoints[i].x) * percent),
linePoints[i].y + ((linePoints[i + 1].y - linePoints[i].y) * percent),
mFinishedPaint);
//圓弧部分的繪制
} else if (i % 2 == 1) {
RectF rectF = new RectF(arcPoints[i + 1].x, arcPoints[i + 1].y,
arcPoints[i + 2].x, arcPoints[i + 2].y);
float oval = percent * 60;
canvas.drawArc(rectF, -120 + (i + 1) * 30, oval, false, mFinishedPaint)
}
}
}
//尾部的繪制
if (mProgress > progressParts[11] && mProgress <= 100) {
drawTrail(canvas, 12);
float percent = (mProgress - progressParts[11]) / (100 - progressParts[11]);
RectF rectFOne = new RectF(arcPoints[0].x, arcPoints[0].y,
arcPoints[1].x, arcPoints[1].y);
float oval = percent * 30;
canvas.drawArc(rectFOne, -120, oval, false, mFinishedPaint);
}
}
一臉懵逼和懶得看朋友(老實(shí)講,要不是我自己寫的,我肯定會(huì)是一臉懵逼和懶得看的里面的一種 - -)可以直接通過下方鏈接下載控件,并在需要的時(shí)候應(yīng)用到項(xiàng)目里。