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

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

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


參考效果圖

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

  • Paint勺鸦、Canvas這兩個基礎的類必須熟悉。
  • 用作渲染的Shader類及其子類目木,以及后文中使用的是SweepGradient梯度渲染换途,用作漸變圓環(huán),需要了解。
  • canvas.save() & canvas.restore() 的作用與關系怀跛。
  • 由Paint引申的PathEffect距贷、PorterDuffXfermode,已經(jīng)Matrix等類要有個基本的概念吻谋。
  • 圖層繪制的一些概念忠蝗。
  • 臟矩形技術。

如果你已經(jīng)基本了解了上面涉及到的知識點漓拾。
OK阁最。那接下來我們就一步一步實現(xiàn)這個效果。

1.環(huán)形漸變

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

核心代碼如下:

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

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);// 畫出色環(huán)和中心園
canvas.restore();

效果如圖1所示:

圖1

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

圖2

我們已知SweepGradient是一個360度均勻分布的漸變,我們一共設置了6個漸變色:從紅色(ff0000)到紫色(ff00ff),使其均勻分布在圓環(huán)上。
而繪制圓的時候帮毁,我們先將canvas的原點(在android2D圖形系統(tǒng)中其坐標系原點在視圖左上角)通過canvas.translate()平移至了圓環(huán)的中心點实苞。在此我們使用canvas.rotate()旋轉(zhuǎn)操作,旋轉(zhuǎn)150度烈疚,使其紅色漸變的開始位置處于圖片左下方(此處正確的理解應該是這樣:由于我們對畫布旋轉(zhuǎn)了150度黔牵,所以我們在繪制完圓環(huán)之后,通過restore()方法又使得畫布回歸到原來位置爷肝,從而達到了將紅色漸變位于左下方的目的)猾浦。調(diào)整完canvas之后陆错,我們通過canvas.drawOval()將圓繪制上去。最后將畫布回歸到原來的位置金赦。
此處還使用了canvas.save()canvas.restore()組合操作音瓷。簡單介紹一下:由于此處我們對畫布有平移、旋轉(zhuǎn)操作夹抗。為了不造成對后續(xù)繪制的影響绳慎,使其復雜度增加。我們使用save()和restore()的組合來使得畫布回歸到它原來的位置漠烧。此舉有時候會對性能產(chǎn)生一定的影響杏愤,本文只是step by step的實現(xiàn)教程,而且此效果并不會強依賴于性能已脓,所以性能在此處先放一邊珊楼。文末我會注明可以優(yōu)化的點,供大家思考度液、討論厕宗。
在這里調(diào)用完restore()的表象就是canvas的原點又回到了視圖的左上角。關于具體對canvas.save()canvas.restore()的解釋恨诱,網(wǎng)上有一大堆媳瞪。這里不詳細展開。大致可以理解為save()為保存當前canvas狀態(tài)照宝,restore則為恢復上一次save()的狀態(tài)。

2.繪制內(nèi)圓

核心代碼如下

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繪制虛線。
細心的讀者還可以發(fā)現(xiàn)乍丈,我們使用的繪制圓形的方法不一樣剂碴。前面使用的是drawOval繪制橢圓,而在此處使用的是drawCircle直接畫圓轻专,效果都一樣忆矛。具體區(qū)別可以自己體會,一個是框死了畫內(nèi)切橢圓请垛,另一個是直接畫圓催训。

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個區(qū),每個區(qū)占40度,每個區(qū)有10個小間隔混稽,每個小間隔的角度就是4度采驻。由于圓弧有30度是在水平線以下的审胚,所以我們的循環(huán)規(guī)則是上述代碼。canvas.rotate(-(-30 + 4 * i), CENTER_X, CENTER_X);此處由于CENTER_X==CENTER_Y==r,將上述代碼修改為canvas.rotate(-(-30 + 4 * i), CENTER_X, CENTER_Y);或許更容易理解礼旅。rotate中參數(shù)>0為順時針旋轉(zhuǎn)膳叨,<0為逆時針旋轉(zhuǎn)。

4.圓環(huán)變圓弧

到目前為止痘系,我們畫的還只是個漸變圓環(huán)懒鉴,與效果圓弧還有些不同。下面我們將圓環(huán)處理為圓弧碎浇。
** 核心代碼如下 **

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

** 代碼講解:**
首先我們需要調(diào)整視圖的高度临谱。在這之前我們都是令width==height,保證繪制出一個整圓∨В現(xiàn)在根據(jù)我們的假設圓弧度數(shù)240度悉默,其在水平線以下為30度,即PI/6苟穆。由數(shù)學公式計算得知抄课,其視圖高度為 height = r * tan(PI/6) + r
這還不夠雳旅,調(diào)整完視圖的高度跟磨,我們需要將一些雜線,從視圖中除去攒盈,讓其看上去更像是個圓弧抵拘。
如圖6所示未去雜線的時候

圖6

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

5.文字的繪制

** 核心代碼如下**

private static final String[] text = {"950","極好","700","優(yōu)秀","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

** 代碼講解 **
我們已知每個區(qū)為40度。從參考效果圖上可以看出每隔20度就會有一段文字谆吴。我們知道在繪制文字的時候倒源,都是從左往右寫的。所以我們在旋轉(zhuǎn)畫布的時候纪铺,起始點需要在原來的基礎上再加90度相速,即逆時針旋轉(zhuǎn)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(),此處我們要從左下角開始繪制圓弧旺隙,所以我們的起始旋轉(zhuǎn)角度為-210度绒极。
由于我們此處的原點在圓心。圖片要跟隨著已知的旋轉(zhuǎn)角度進行旋轉(zhuǎn)蔬捷。我們知道針對canvas.rotate()方法垄提,當旋轉(zhuǎn)角度>0的時候,是順時針旋轉(zhuǎn)周拐;<0為逆時針旋轉(zhuǎn)铡俐。由于此處我們圖片的箭頭朝向向右,為了保證圖片的朝向指向圓心妥粟。我們旋轉(zhuǎn)的規(guī)則為- 30 + currentRotateAngle,保證每一次在繪制圖形的時候审丘,都是在(x,y)為(-r1 - bitmapWidth * 3/ 8,-bitmapHeight/2)這個位置的時候繪制。最后恢復canvas勾给。
關于在計算totalRotateAngle滩报、currentRotateAngle以及 value的時候,都是些簡單的算法播急。夾雜著很多硬編碼脓钾,耐心點應該可以讀懂,不做過多解釋桩警。


實現(xiàn)的七七八八可训,大致思路應該是這樣。

一些問題

  1. 在上文也提到了生真,參考的效果圖沉噩,并非是一個平滑的漸變。仔細觀察的話柱蟀,在600處有處瞬斷的跡象。
    解決思路:利用上面講到過的PorterDuffXfermode蚜厉,將兩段不同的環(huán)形漸變长已,拼接而成。到達此效果昼牛。
  2. 關于優(yōu)化
  • onDraw()方法中术瓮,canvas.save()與canvas.restore()方法多次使用,造成不必要的性能浪費贰健。
  • 在執(zhí)行箭頭轉(zhuǎn)動效果的時候胞四,不需要在canvas上每次全部都重新繪制。只需要繪制需要繪制的部分區(qū)域即可伶椿,即臟矩形辜伟。在這里也就是箭頭所滾動范圍內(nèi)的部分區(qū)域圓環(huán)氓侧。讀者可以自行實現(xiàn)。
  1. 關于多線程
    細心的人可以發(fā)現(xiàn)方法setReferValue(),并沒有考慮多線程的情況导狡。此處只是demo约巷,場景也有限。沒做特殊處理旱捧。有興趣的讀者可以自行實現(xiàn)独郎。

后記

之前一直沒有記錄博客的習慣。現(xiàn)在寫完兩篇枚赡,發(fā)現(xiàn)將代碼翻譯成文不是一件容易的事氓癌。代碼在周三就基本完成了,文章也是一直拖著到現(xiàn)在才整理出來發(fā)布贫橙。要將每一個知識點贪婉,能夠簡單的表述出來,是比較難的一件事情料皇。落筆成文同面對面與人講述谓松,會不太一樣。以后要多加強這方面的練習践剂。也希望讀者們能夠一起來嘗試記錄鬼譬。遺留的問題,都不是很難逊脯,讀者可以自行嘗試的去實現(xiàn)优质。今天腦子有點疼,就寫到此了军洼。

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

enjoy it巩螃!


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

weixin.jpg

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

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末避乏,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子甘桑,更是在濱河造成了極大的恐慌拍皮,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件跑杭,死亡現(xiàn)場離奇詭異铆帽,居然都是意外死亡,警方通過查閱死者的電腦和手機德谅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門爹橱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人窄做,你說我怎么就攤上這事愧驱∥考迹” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵冯键,是天一觀的道長惹盼。 經(jīng)常有香客問我,道長惫确,這世上最難降的妖魔是什么手报? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮改化,結果婚禮上掩蛤,老公的妹妹穿的比我還像新娘。我一直安慰自己陈肛,他們只是感情好揍鸟,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著句旱,像睡著了一般阳藻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上谈撒,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天腥泥,我揣著相機與錄音,去河邊找鬼啃匿。 笑死蛔外,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的溯乒。 我是一名探鬼主播夹厌,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼裆悄!你這毒婦竟也來了矛纹?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤光稼,失蹤者是張志新(化名)和其女友劉穎崖技,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體钟哥,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年瞎访,在試婚紗的時候發(fā)現(xiàn)自己被綠了腻贰。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡扒秸,死狀恐怖播演,靈堂內(nèi)的尸體忽然破棺而出冀瓦,到底是詐尸還是另有隱情,我是刑警寧澤写烤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布翼闽,位于F島的核電站,受9級特大地震影響洲炊,放射性物質(zhì)發(fā)生泄漏感局。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一暂衡、第九天 我趴在偏房一處隱蔽的房頂上張望询微。 院中可真熱鬧,春花似錦狂巢、人聲如沸撑毛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽藻雌。三九已至,卻和暖如春斩个,著一層夾襖步出監(jiān)牢的瞬間胯杭,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工萨驶, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留歉摧,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓腔呜,卻偏偏與公主長得像叁温,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子核畴,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容