Android -- Magical ProgressBar 2.0

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

效果圖

實(shí)現(xiàn)步驟

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

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


步驟2

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

步驟三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代碼。

具體計(jì)算流程.png
/**
 * 初始化各個(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ū)間的范圍 。如下圖所示。

進(jìn)度條的劃分.png

具體代碼如下: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]);
線段區(qū)間進(jìn)度條計(jì)算過程

之后得出了如下的通項(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)目里。

https://github.com/HandGrab/MagicalProgress

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,733評論 25 709
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,037評論 4 61
  • 18- UIBezierPath官方API中文翻譯(待校對) ----------------- 華麗的分割線 -...
    醉臥欄桿聽雨聲閱讀 1,159評論 1 1
  • 原來沒有目標(biāo)最可怕 原來所有的反常只是為了掩飾自己的不安 原來心像一個(gè)夾層蛋糕,外表的逞強(qiáng)是為掩飾“以為”的軟糯夾...
    板栗欣閱讀 195評論 0 0
  • 平凡如伴侶只是很遙遠(yuǎn) 燈等蹬 暢懷的人登場了 我是HHH 所謂良人伴侶 皆是幻象 自身幻想虛擬 外求幻想之象 錯(cuò)走...
    執(zhí)筆夜雨獨(dú)行閱讀 250評論 0 0

友情鏈接更多精彩內(nèi)容