一步一步教你如何實現阿里芝麻信用分儀表盤效果

原文地址:http://makerchen.com/2016/05/29/android-alibaba/

廢話不多說,先看下效果:


參考效果圖

該效果一眼看上去比較簡單,但其涉及的知識點還是挺多的。尤其是需要讀者對android.graphics包下的API有一定的了解。
先對涉及到的知識點羅列如下,還不是很了解的讀者可以先自行百度做個簡單的涉獵,對后續文章的理解會有很大幫助。

  • Paint、Canvas這兩個基礎的類必須熟悉。
  • 用作渲染的Shader類及其子類,以及后文中使用的是SweepGradient梯度渲染,用作漸變圓環,需要了解。
  • canvas.save() & canvas.restore() 的作用與關系。
  • 由Paint引申的PathEffect、PorterDuffXfermode,已經Matrix等類要有個基本的概念。
  • 圖層繪制的一些概念。
  • 臟矩形技術。

如果你已經基本了解了上面涉及到的知識點。
OK。那接下來我們就一步一步實現這個效果。

1.環形漸變

或許大家都有印象,在ApiDemos中提供過一個例子仿照PS做的取色器效果。有興趣的讀者可以具體查看ApiDemos下的ColorPickerDialog的實現。這里我們參考他的寫法,就可以做出一個簡單的環形漸變了。
當然ColorPickerDialog中的核心代碼也正是使用了剛才所提及的SweepGradient類用作渲染。該類屬于Shader的子類,當然其兄弟類還有BitmapShader位圖渲染、LinearGradient線性渲染、RadialGradient環形渲染、SweepGradient梯度渲染以及ComposeShader組合渲染。網上有一大堆關于他們的介紹,可以做出很多很棒的效果。此處不展開。

核心代碼如下:

private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);// 漸變色環畫筆,抗鋸齒
private final int[] mColors = new int[] { 0xffff0000, 0xffffff00, 0xff00ff00,
0xff00ffff,0xff0000ff,0xffff00ff };// 漸變色環顏色

Shader s = new SweepGradient(0, 0, mColors, null);
mPaint.setShader(s);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(40);

float r = CENTER_X - mPaint.getStrokeWidth() * 0.5f;
canvas.save() ;
canvas.translate(CENTER_X, CENTER_X);// 移動中心
canvas.rotate(150);
canvas.drawOval(new RectF(-r, -r, r, r), mPaint);// 畫出色環和中心園
canvas.restore();

效果如圖1所示:

圖1

代碼講解:
參考效果圖上看,顏色是有紅色漸變(并非線性漸變,這里我們先按照簡單的實現)為綠色,而且效果并非為一個整圓。為了計算方便,我們假設該圓環的角度為240度。
如圖2所示

圖2

我們已知SweepGradient是一個360度均勻分布的漸變,我們一共設置了6個漸變色:從紅色(ff0000)到紫色(ff00ff),使其均勻分布在圓環上。
而繪制圓的時候,我們先將canvas的原點(在android2D圖形系統中其坐標系原點在視圖左上角)通過canvas.translate()平移至了圓環的中心點。在此我們使用canvas.rotate()旋轉操作,旋轉150度,使其紅色漸變的開始位置處于圖片左下方(此處正確的理解應該是這樣:由于我們對畫布旋轉了150度,所以我們在繪制完圓環之后,通過restore()方法又使得畫布回歸到原來位置,從而達到了將紅色漸變位于左下方的目的)。調整完canvas之后,我們通過canvas.drawOval()將圓繪制上去。最后將畫布回歸到原來的位置。
此處還使用了canvas.save()canvas.restore()組合操作。簡單介紹一下:由于此處我們對畫布有平移、旋轉操作。為了不造成對后續繪制的影響,使其復雜度增加。我們使用save()和restore()的組合來使得畫布回歸到它原來的位置。此舉有時候會對性能產生一定的影響,本文只是step by step的實現教程,而且此效果并不會強依賴于性能,所以性能在此處先放一邊。文末我會注明可以優化的點,供大家思考、討論。
在這里調用完restore()的表象就是canvas的原點又回到了視圖的左上角。關于具體對canvas.save()canvas.restore()的解釋,網上有一大堆。這里不詳細展開。大致可以理解為save()為保存當前canvas狀態,restore則為恢復上一次save()的狀態。

2.繪制內圓

核心代碼如下

paintMiddleCircle.setColor(Color.GRAY);
paintInnerCircle.setColor(Color.GRAY);
paintMiddleCircle.setStrokeWidth(4);
paintInnerCircle.setStrokeWidth(4);
paintMiddleCircle.setStyle(Paint.Style.STROKE);
paintInnerCircle.setStyle(Paint.Style.STROKE);
PathEffect effects = new DashPathEffect(new float[]{5,5,5,5},1);
paintInnerCircle.setPathEffect(effects);

canvas.save() ;
canvas.translate(CENTER_X, CENTER_X);
canvas.drawCircle(0, 0, CENTER_X * 5 / 8, paintInnerCircle);
canvas.drawCircle(0, 0, CENTER_X * 3 / 4, paintMiddleCircle);
canvas.restore();

效果如圖3所示

圖3

代碼講解:
該功能比較簡單。
在此處需要了解PathEffect及其子類的作用,這里我們使用DashPathEffect繪制虛線。
細心的讀者還可以發現,我們使用的繪制圓形的方法不一樣。前面使用的是drawOval繪制橢圓,而在此處使用的是drawCircle直接畫圓,效果都一樣。具體區別可以自己體會,一個是框死了畫內切橢圓,另一個是直接畫圓。

3.繪制輔助線

核心代碼如下

paintGap1.setColor(Color.WHITE);
paintGap2.setColor(Color.WHITE);
paintGap1.setStrokeWidth(2);
paintGap2.setStrokeWidth(4);

int a = (int) (2 * CENTER_X - mPaint.getStrokeWidth());
for ( int i=0;i<=60; i++) {    
    canvas.save() ;    
    canvas.rotate(-(-30 + 4 * i), CENTER_X, CENTER_X);    
    if ( i % 10 == 0 ) {        
        canvas.drawLine( a ,CENTER_X, 2 * CENTER_X, CENTER_X, paintGap2);    
    } else {       
        canvas.drawLine( a ,CENTER_X, 2 * CENTER_X, CENTER_X, paintGap1);    
    }    
    canvas.restore();
}

效果如圖4所示

圖4

代碼講解
在上面,我們曾假設了圓弧的角度為240度。便于計算,我們將該圓弧劃分為6個區,每個區占40度,每個區有10個小間隔,每個小間隔的角度就是4度。由于圓弧有30度是在水平線以下的,所以我們的循環規則是上述代碼。canvas.rotate(-(-30 + 4 * i), CENTER_X, CENTER_X);此處由于CENTER_X==CENTER_Y==r,將上述代碼修改為canvas.rotate(-(-30 + 4 * i), CENTER_X, CENTER_Y);或許更容易理解。rotate中參數>0為順時針旋轉,<0為逆時針旋轉。

4.圓環變圓弧

到目前為止,我們畫的還只是個漸變圓環,與效果圓弧還有些不同。下面我們將圓環處理為圓弧。
** 核心代碼如下 **

width = MeasureSpec.getSize(widthMeasureSpec);
height =  (int) ( ( Math.tan(Math.PI / 6) + 1 ) * width / 2 ) ;

Path path = new Path();
path.moveTo(CENTER_X, CENTER_X);
path.lineTo(0, height);
path.lineTo(width, height);
path.lineTo(CENTER_X, CENTER_X);
path.close();
canvas.drawPath(path, paintBg);

效果圖5如下

圖5

** 代碼講解:**
首先我們需要調整視圖的高度。在這之前我們都是令width==height,保證繪制出一個整圓。現在根據我們的假設圓弧度數240度,其在水平線以下為30度,即PI/6。由數學公式計算得知,其視圖高度為 height = r * tan(PI/6) + r
這還不夠,調整完視圖的高度,我們需要將一些雜線,從視圖中除去,讓其看上去更像是個圓弧。
如圖6所示未去雜線的時候

圖6

我們利用圖層互相遮罩的原理。以圓心和視圖的兩個頂點,連接成一個三角形,可以達到掩蓋其與雜線的目的。也就是后面代碼的作用。
記住在onDraw時候的一個原則:先畫的在畫布下方,后畫的在畫布上方,后畫的會覆蓋先畫的。從而達到圖5的效果。

5.文字的繪制

** 核心代碼如下**

private static final String[] text = {"950","極好","700","優秀","650","良好","600","中等","550","較差","350","很差","150"};

for ( int i=0;i<=12;i++) {   
     canvas.save();    
     canvas.rotate(-(-120 + 20 * i ), CENTER_X, CENTER_X);    
     canvas.drawText(text[i],CENTER_X - 20 ,CENTER_X * 3 / 16,paintText);   
     canvas.restore();
}

效果圖7如下

圖7

** 代碼講解 **
我們已知每個區為40度。從參考效果圖上可以看出每隔20度就會有一段文字。我們知道在繪制文字的時候,都是從左往右寫的。所以我們在旋轉畫布的時候,起始點需要在原來的基礎上再加90度,即逆時針旋轉120度,然后繪入文字。當然這段繪制的過程需要在繪制三角形之后,否則部分文字會被三角形的遮罩遮蓋起來。

6.最后的動效

if ( isSetReferValue ) {    
    float r1 = CENTER_X * 6 / 8 ;    
    canvas.save();    
    canvas.translate(CENTER_X, CENTER_X);    
    canvas.drawArc(new RectF(-r1, -r1, r1, r1), -210, currentRotateAngle, false, paintMiddleArc);    
    canvas.rotate( - 30 + currentRotateAngle );    
    Matrix matrix = new Matrix();    
    matrix.preTranslate(-r1 - bitmapWidth * 3/ 8,-bitmapHeight/2);    
    canvas.drawBitmap(bitmapLocation,matrix,paintBitmap);    
    canvas.restore();
}

public void setReferValue ( int referValue ,final RotateListener rotateListener) {    
    isSetReferValue = !isSetReferValue ;    
    if ( referValue <= 150 ) {        
        totalRotateAngle = 0f ;    
    } else if ( referValue <= 550 ) {        
        totalRotateAngle = ( referValue - 150 ) * 80 / 400f ;    
    } else if ( referValue <= 700 ) {        
        totalRotateAngle = ( referValue - 550 ) * 120 / 150f + 80 ;    
    } else if ( referValue <= 950 ) {        
        totalRotateAngle = ( referValue - 700 ) * 40 / 250f + 200;      
    } else {        
        totalRotateAngle = 210f ;    
    }    
    rotateAngle = totalRotateAngle / 60 ;    
    new Thread(new Runnable() {        
        @Override        
        public void run() {            
             boolean rotating = true ;            
             float value = 350;           
             while (rotating) {                
                  try {                    
                        Thread.sleep(16);                
                  } catch (InterruptedException e) {                    
                        e.printStackTrace();                
                  }                
                  currentRotateAngle += rotateAngle;                
                  if ( currentRotateAngle >= totalRotateAngle ) {
                      currentRotateAngle = totalRotateAngle;                          
                      rotating = false;                
                  }                
                  if ( null != rotateListener) {                    
                      if ( currentRotateAngle <= 80 ) {                        
                            value = 350 + ( currentRotateAngle / 80 ) * 400 ;                    
                      } else if ( currentRotateAngle <= 200 ) {                        
                            value = 550 + ( ( currentRotateAngle  - 80 )/ 120 ) * 150 ;                   
                      } else {                        
                            value = 700 + ( ( currentRotateAngle - 200 ) / 40 ) * 250 ;                    
                      }                    
                      rotateListener.rotate(currentRotateAngle,value);                                    
                  }                
                  postInvalidate();            
              }        
          }    
      }).start();
 }

效果圖8如下

圖8

代碼講解
繪制的代碼中。首先我們要了解到繪制圓弧的方法為canvas.drawArc(),此處我們要從左下角開始繪制圓弧,所以我們的起始旋轉角度為-210度。
由于我們此處的原點在圓心。圖片要跟隨著已知的旋轉角度進行旋轉。我們知道針對canvas.rotate()方法,當旋轉角度>0的時候,是順時針旋轉;<0為逆時針旋轉。由于此處我們圖片的箭頭朝向向右,為了保證圖片的朝向指向圓心。我們旋轉的規則為- 30 + currentRotateAngle,保證每一次在繪制圖形的時候,都是在(x,y)為(-r1 - bitmapWidth * 3/ 8,-bitmapHeight/2)這個位置的時候繪制。最后恢復canvas。
關于在計算totalRotateAnglecurrentRotateAngle以及 value的時候,都是些簡單的算法。夾雜著很多硬編碼,耐心點應該可以讀懂,不做過多解釋。


實現的七七八八,大致思路應該是這樣。

一些問題

  1. 在上文也提到了,參考的效果圖,并非是一個平滑的漸變。仔細觀察的話,在600處有處瞬斷的跡象。
    解決思路:利用上面講到過的PorterDuffXfermode,將兩段不同的環形漸變,拼接而成。到達此效果。
  2. 關于優化
  • onDraw()方法中,canvas.save()與canvas.restore()方法多次使用,造成不必要的性能浪費。
  • 在執行箭頭轉動效果的時候,不需要在canvas上每次全部都重新繪制。只需要繪制需要繪制的部分區域即可,即臟矩形。在這里也就是箭頭所滾動范圍內的部分區域圓環。讀者可以自行實現。
  1. 關于多線程
    細心的人可以發現方法setReferValue(),并沒有考慮多線程的情況。此處只是demo,場景也有限。沒做特殊處理。有興趣的讀者可以自行實現。

后記

之前一直沒有記錄博客的習慣。現在寫完兩篇,發現將代碼翻譯成文不是一件容易的事。代碼在周三就基本完成了,文章也是一直拖著到現在才整理出來發布。要將每一個知識點,能夠簡單的表述出來,是比較難的一件事情。落筆成文同面對面與人講述,會不太一樣。以后要多加強這方面的練習。也希望讀者們能夠一起來嘗試記錄。遺留的問題,都不是很難,讀者可以自行嘗試的去實現。今天腦子有點疼,就寫到此了。

源代碼在此下載:http://pan.baidu.com/s/1kTKUowJ

enjoy it!


想及時了解最新信息。掃一掃,添加關注微信公眾號

weixin.jpg

原文地址:http://makerchen.com/2016/05/29/android-alibaba/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,412評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,514評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,373評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,975評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,743評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,199評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,262評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,414評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,951評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,780評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,527評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,218評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,649評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,889評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,673評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容