好長時間沒寫blog了,內心感受有點空蕩蕩的,今天有時間就來寫一個關於自定義視圖的的blog吧。關於這篇blog,網上已經有不少案例了,其實沒什麼難度的。可是咱們在開發的過程當中有時候會用到一些自定義的View以達到咱們所須要的效果。其實網上的不少案例咱們看完以後,發現這部分沒什麼難度的,我總結了兩點:html
一、準備紙和筆,計算座標java
二、在onDraw方法中開始畫圖,invalidate方法刷新,onTouchEvent方法監聽觸摸事件android
對於繪圖相關的知識,以前在弄JavaSE相關的知識的時候,寫過俄羅斯方塊,也是用畫筆繪製的,而後在刷新。原理都同樣的。express
在這個過程當中,咱們須要瞭解兩個很重要的東西:apache
畫筆:Paintcanvas
畫布:Canvas數組
http://blog.csdn.net/t12x3456/article/details/10473225
多線程
那麼下面咱們就開始來介紹相關的內容了。我主要分一下步驟來說解一下:app
一、介紹Android中的Paint和Canvas的概念和使用方法less
二、介紹Android中重要的概念渲染對象Shader
三、自定義一個LabelView(和Android中的TextView差很少)
四、自定義漸變的圓形和長條的SeekBar
五、自定義顏色選擇器
六、自定義閃爍的TextView
七、實現360手機衛士中的流量監控的折線圖
那麼咱們也知道今天的文章內容應該會不少,因此你們要有耐心的看完。這篇文章若是看懂了,也耐心的看完了,相信對自定義View的相關知識瞭解的應該也差很少了,本身在開發的過程當中實現本身想要的簡單的View應該是沒有問題的。
下面開始正式介紹內容
Android中的Paint和Canvas的概念是很簡單的,就是咱們用畫筆在畫布上進行繪製沒什麼難度的,咱們只要拿到畫筆Paint和畫布Canvas對象就能夠進行操做了。固然Canvas對象提供了不少繪製圖形的方法,下面來看一下代碼吧:
package com.example.drawpathdemo; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; import android.graphics.Shader; import android.util.AttributeSet; import android.view.View; public class DrawView extends View { public DrawView(Context context) { super(context); } public DrawView(Context context, AttributeSet attributeSet) { super(context, attributeSet); } @SuppressLint("DrawAllocation") @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); /* * 方法 說明 drawRect 繪製矩形 drawCircle 繪製圓形 drawOval 繪製橢圓 drawPath 繪製任意多邊形 * drawLine 繪製直線 drawPoin 繪製點 */ // 建立畫筆 Paint p = new Paint(); p.setColor(Color.RED);// 設置紅色 canvas.drawText("畫圓:", 10, 20, p);// 畫文本 canvas.drawCircle(60, 20, 10, p);// 小圓 p.setAntiAlias(true);// 設置畫筆的鋸齒效果。 true是去除,你們一看效果就明白了 canvas.drawCircle(120, 20, 20, p);// 大圓 canvas.drawText("畫線及弧線:", 10, 60, p); p.setColor(Color.GREEN);// 設置綠色 canvas.drawLine(60, 40, 100, 40, p);// 畫線 canvas.drawLine(110, 40, 190, 80, p);// 斜線 //畫笑臉弧線 p.setStyle(Paint.Style.STROKE);//設置空心 RectF oval1=new RectF(150,20,180,40); canvas.drawArc(oval1, 180, 180, false, p);//小弧形 oval1.set(190, 20, 220, 40); canvas.drawArc(oval1, 180, 180, false, p);//小弧形 oval1.set(160, 30, 210, 60); canvas.drawArc(oval1, 0, 180, false, p);//小弧形 canvas.drawText("畫矩形:", 10, 80, p); p.setColor(Color.GRAY);// 設置灰色 p.setStyle(Paint.Style.FILL);//設置填滿 canvas.drawRect(60, 60, 80, 80, p);// 正方形 canvas.drawRect(60, 90, 160, 100, p);// 長方形 canvas.drawText("畫扇形和橢圓:", 10, 120, p); /* 設置漸變色 這個正方形的顏色是改變的 */ Shader mShader = new LinearGradient(0, 0, 100, 100, new int[] { Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.LTGRAY }, null, Shader.TileMode.REPEAT); // 一個材質,打造出一個線性梯度沿著一條線。 p.setShader(mShader); // p.setColor(Color.BLUE); RectF oval2 = new RectF(60, 100, 200, 240);// 設置個新的長方形,掃描測量 canvas.drawArc(oval2, 200, 130, true, p); // 畫弧,第一個參數是RectF:該類是第二個參數是角度的開始,第三個參數是多少度,第四個參數是真的時候畫扇形,是假的時候畫弧線 //畫橢圓,把oval改一下 oval2.set(210,100,250,130); canvas.drawOval(oval2, p); canvas.drawText("畫三角形:", 10, 200, p); // 繪製這個三角形,你能夠繪製任意多邊形 Path path = new Path(); path.moveTo(80, 200);// 此點爲多邊形的起點 path.lineTo(120, 250); path.lineTo(80, 250); path.close(); // 使這些點構成封閉的多邊形 canvas.drawPath(path, p); // 你能夠繪製不少任意多邊形,好比下面畫六連形 p.reset();//重置 p.setColor(Color.LTGRAY); p.setStyle(Paint.Style.STROKE);//設置空心 Path path1=new Path(); path1.moveTo(180, 200); path1.lineTo(200, 200); path1.lineTo(210, 210); path1.lineTo(200, 220); path1.lineTo(180, 220); path1.lineTo(170, 210); path1.close();//封閉 canvas.drawPath(path1, p); /* * Path類封裝複合(多輪廓幾何圖形的路徑 * 由直線段*、二次曲線,和三次方曲線,也可畫以油畫。drawPath(路徑、油漆),要麼已填充的或撫摸 * (基於油漆的風格),或者能夠用於剪斷或畫畫的文本在路徑。 */ //畫圓角矩形 p.setStyle(Paint.Style.FILL);//充滿 p.setColor(Color.LTGRAY); p.setAntiAlias(true);// 設置畫筆的鋸齒效果 canvas.drawText("畫圓角矩形:", 10, 260, p); RectF oval3 = new RectF(80, 260, 200, 300);// 設置個新的長方形 canvas.drawRoundRect(oval3, 20, 15, p);//第二個參數是x半徑,第三個參數是y半徑 //畫貝塞爾曲線 canvas.drawText("畫貝塞爾曲線:", 10, 310, p); p.reset(); p.setStyle(Paint.Style.STROKE); p.setColor(Color.GREEN); Path path2=new Path(); path2.moveTo(100, 320);//設置Path的起點 path2.quadTo(150, 310, 170, 400); //設置貝塞爾曲線的控制點座標和終點座標 canvas.drawPath(path2, p);//畫出貝塞爾曲線 //畫點 p.setStyle(Paint.Style.FILL); canvas.drawText("畫點:", 10, 390, p); canvas.drawPoint(60, 390, p);//畫一個點 canvas.drawPoints(new float[]{60,400,65,400,70,400}, p);//畫多個點 //畫圖片,就是貼圖 Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher); canvas.drawBitmap(bitmap, 250,360, p); } }運行一下看效果圖:
在代碼中咱們看到了咱們新建了一個Paint畫筆對象,對於畫筆對象,它有不少設置屬性的:
void setARGB(int a, int r, int g, int b) 設置Paint對象顏色,參數一爲alpha透明通道
void setAlpha(int a) 設置alpha不透明度,範圍爲0~255
void setAntiAlias(boolean aa) //是否抗鋸齒,默認值是false
void setColor(int color) //設置顏色,這裏Android內部定義的有Color類包含了一些常見顏色定義
void setFakeBoldText(boolean fakeBoldText) //設置僞粗體文本
void setLinearText(boolean linearText) //設置線性文本
PathEffect setPathEffect(PathEffect effect) //設置路徑效果
Rasterizer setRasterizer(Rasterizer rasterizer) //設置光柵化
Shader setShader(Shader shader) //設置陰影 ,咱們在後面會詳細說一下Shader對象的
void setTextAlign(Paint.Align align) //設置文本對齊
void setTextScaleX(float scaleX) //設置文本縮放倍數,1.0f爲原始
void setTextSize(float textSize) //設置字體大小
Typeface setTypeface(Typeface typeface) //設置字體,Typeface包含了字體的類型,粗細,還有傾斜、顏色等
注:
Paint mp = new paint();
mp.setTypeface(Typeface.DEFAULT_BOLD)
經常使用的字體類型名稱還有:
Typeface.DEFAULT //常規字體類型
Typeface.DEFAULT_BOLD //黑體字體類型
Typeface.MONOSPACE //等寬字體類型
Typeface.SANS_SERIF //sans serif字體類型
Typeface.SERIF //serif字體類型
除了字體類型設置以外,還能夠爲字體類型設置字體風格,如設置粗體:
Paint mp = new Paint();
Typeface font = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD);
p.setTypeface( font );
經常使用的字體風格名稱還有:
Typeface.BOLD //粗體
Typeface.BOLD_ITALIC //粗斜體
Typeface.ITALIC //斜體
Typeface.NORMAL //常規
void setUnderlineText(boolean underlineText) //設置下劃線
void setStyle(Style style) //設置畫筆樣式
注:
經常使用的樣式
Paint.Style.FILL
Paint.Style.STROKE
Paint.Style.FILL_AND_STROKE
這裏的FILL和STROKE兩種方式用的最多,他們的區別也很好理解的,FILL就是填充的意思,STROKE就是空心的意思,只有 圖形的輪廓形狀,內部是空的。
void setStrokeWidth(float width) //在畫筆的樣式爲STROKE的時候,圖形的輪廓寬度
對於畫布對象Canvas咱們是從onDraw方法中獲取到的,因此這裏咱們就能夠看不來了,咱們在自定義視圖的時候都會繼承View類,而後在他的onDraw方法中拿到Canvas對象,進行各類繪製了。下面就來一一看一下各類繪製的方法吧:
首先來看一下如何建立一個畫筆對象:
Paint p = new Paint(); p.setColor(Color.RED);// 設置紅色咱們能夠設置畫筆的顏色,固然還有其餘的方法,咱們能夠設置畫筆的粗細,是否有鋸齒等,後面會說道。
咱們想一下,若是畫出一個圓形的話須要哪些要素,學過幾何的同窗都知道:圓形座標+半徑 就能夠肯定一個圓形了
canvas.drawCircle(120, 20, 20, p);參數一:圓心的x座標
參數二:圓心的y座標
參數三:圓的半徑
參數四:畫筆對象
還有一個這裏咱們設置了畫筆是否有鋸齒:
p.setAntiAlias(true);// 設置畫筆的鋸齒效果。 true是去除,你們一看效果就明白了關於這個鋸齒,其實很好理解,就是若是沒有鋸齒效果,那麼畫出來的圓形就很光滑,有鋸齒看上去的圓形很粗糙的。可是默認狀況下,畫筆是有鋸齒的。之因此這樣,是由於在沒有鋸齒效果的狀況下,繪製圖形效率會比有鋸齒效果低,因此係統考慮了效率問題,就把默認值設置成有鋸齒了,咱們在實際繪圖過程當中須要衡量一下的。
咱們想一下,若是畫出一個直線的話須要哪些要素,起始點座標+終點座標 就能夠肯定一條直線了
canvas.drawLine(60, 40, 100, 40, p);// 畫線參數一:起始點的x座標
參數二:起始點的y座標
參數三:終點的x座標
參數四:終點的y座標
參數五:畫筆對象
咱們想一下,若是畫出橢圓的話須要哪些要素,長軸+短軸的長度
RectF oval1=new RectF(150,20,180,40); canvas.drawOval(oval2, p);
參數一:橢圓的外接矩形
參數二:畫筆對象
這裏先來講一下RectF的相關知識吧:在繪圖中這個對象是十分重要的,它表示的是一個矩形,它有四個參數:
left,top,right,bottom
這四個值是相對於設備屏幕的起始點開始的。
RectF oval1=new RectF(150,20,180,40);好比上面的這個矩形,說白了就是這樣:
矩形的左上角的座標是:(150,20)
矩形的右下角的座標是:(180,30)
那麼咱們就知道這個矩形的寬是:180-150=30;高是:40-20=20
其實還有與RectF對象對應的還有一個對象:Rect,它也是四個參數,和RectF惟一的區別就是,Rect中的參數是float類型的,RectF中的參數是int類型的
那麼一個矩形就能夠肯定一個橢圓的,這個矩形就是和這個橢圓外接:
那麼橢圓的長軸就是矩形的寬,短軸就是矩形的高
這樣就能夠肯定一個橢圓了,那麼若是咱們想畫一個圓形的話用這種方式也是能夠的,只要把RectF設置成正方形就能夠了。
咱們想一下,若是畫出一個弧線的話須要哪些要素,起始的弧度+弧線的弧度+外圍的矩形大小
這個和上面畫橢圓很類似的,就至關於在他的基礎上多了其實弧度+弧線的弧度
p.setStyle(Paint.Style.STROKE);//設置空心 RectF oval1=new RectF(150,20,180,40); canvas.drawArc(oval1, 180, 180, false, p);//小弧形
參數一:外接弧形的矩形
參數二:弧線開始的弧度
參數三:弧線的弧度
參數四:是一個boolean類型的參數:true的時候畫扇形,是false的時候畫弧線
參數五:畫筆對象
RectF oval1=new RectF(150,20,180,40); canvas.drawRect(oval1, p);參數一:矩形對象
參數二:畫筆對象
RectF oval3 = new RectF(80, 260, 200, 300);// 設置個新的長方形 canvas.drawRoundRect(oval3, 20, 15, p);//第二個參數是x半徑,第三個參數是y半徑參數一:矩形大小
參數二:圓角的x半徑(橢圓的長軸的一半)
參數三:圓角的y半徑(橢圓的短軸的一半)
參數四:畫筆對象
其實這個和正常的矩形不同的是:在四個角是有弧度的,那麼弧度的話,就會想到橢圓了,咱們在上面說道橢圓的幾個要素:長軸和短軸,那麼這裏就是取長軸的一半和短軸的一半。
咱們想一下,若是繪製三角形/多邊形的話須要哪些要素,能肯定多邊形的形狀最重要的因素就是角,那麼這些角就是一個座標
Path path = new Path(); path.moveTo(80, 200);// 此點爲多邊形的起點 path.lineTo(120, 250); path.lineTo(80, 250); path.close(); // 使這些點構成封閉的多邊形 canvas.drawPath(path, p);這裏須要介紹一下Path對象了,這個對象見名知意,是路徑的意思,它有兩個參數:
參數一:x座標
參數二:y座標
路徑是多個點相鏈接的。因此Path提供了兩個方法:moveTo和lineTo
moveTo方法的做用是設置咱們繪製路徑的開始點,若是沒有這個方法的調用的話,系統默認的開始點是(0,0)點
lineTo方法就是將路徑的上一個座標點和當前座標點進行鏈接,或者能夠認爲設置多邊形的每一個角的座標點
那麼對於三角形的話,咱們須要三個點便可。
這個畫三角形其實咱們用上面的畫直線的方法也能夠實現的,反過來也是,咱們用Path對象也是能夠畫出一條直線的,那麼他們的本質區別是:
繪製路徑方式的焦點是角(座標點)
繪製直線的方式的焦點是邊(長度)
canvas.drawPoint(60, 390, p);//畫一個點 canvas.drawPoints(new float[]{60,400,65,400,70,400}, p);//畫多個點這裏有兩個方法:
drawPoint
參數一:點的x座標
參數二:點的y座標
參數三:畫筆對象
drawPoints
參數一:多個點的數組
參數二:畫筆對象
這種曲線其實咱們在開發過程當中不多用到,不過在圖形學中繪製貝塞爾曲線的時候,咱們須要的要素是:起始點+控制點+終點
Path path2=new Path(); path2.moveTo(100, 320);//設置Path的起點 path2.quadTo(150, 310, 170, 400); //設置貝塞爾曲線的控制點座標和終點座標 canvas.drawPath(path2, p);//畫出貝塞爾曲線它也是使用Path對象的。不過用的是quadTo方法
參數一:控制點的x座標
參數二:控制點的y座標
參數三:終點的x座標
參數四:終點的y座標
這裏須要注意的是,調用moveTo方法來肯定開始座標的,若是沒有調用這個方法,那麼起始點座標默認是:(0,0)
//畫圖片,就是貼圖 Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher); canvas.drawBitmap(bitmap, 250,360, p);參數一:圖片對象Bitmap
參數二:圖片相對於設備屏幕的left值
參數二:圖片相對於設備屏幕的top值
其實咱們能夠把圖片認爲是一個矩形,由於圖片自己是有長度和寬度的,因此這裏只須要矩形的左上角的座標點,就能夠肯定這張圖片在屏幕中的位置了。
上面就介紹完了Path對象和Canvas對象,他們兩個對象是咱們自定義視圖的基礎,因此這部份內容必定要掌握,固然這兩個對象沒什麼難度的,只要對幾何圖形有點了解的同窗,這些東東是很簡單的。
下面再來看下一個知識點:顏色渲染Shader對象
爲何我要把Shader對象單獨拿出來講一下呢?由於這個對象在對於咱們處理圖形特效的時候是很是有用的
下面來看一下Android中Shader對象
在Android Api中關於顏色渲染的幾個重要的類:
Shader,BitmapShader,ComposeShader,LinearGradient,RadialGradient,SweepGradient
它們之間的關係是:
Shader是後面幾個類的父類
該類做爲基類主要是返回繪製時顏色的橫向跨度。其子類能夠做用與Piant。經過 paint.setShader(Shader shader);來實現一些渲染效果。之做用與圖形不做用與bitmap。
構造方法爲默認的構造方法。
枚舉:
emun Shader.TileMode
定義了平鋪的3種模式:
static final Shader.TileMode CLAMP: 邊緣拉伸.
static final Shader.TileMode MIRROR:在水平方向和垂直方向交替景象, 兩個相鄰圖像間沒有縫隙.
Static final Shader.TillMode REPETA:在水平方向和垂直方向重複擺放,兩個相鄰圖像間有縫隙縫隙.
方法:
1. boolean getLoaclMatrix(Matrix localM); 若是shader有一個非本地的矩陣將返回true.
localM:若是不爲null將被設置爲shader的本地矩陣.
2. void setLocalMatrix(Matrix localM);
設置shader的本地矩陣,若是localM爲空將重置shader的本地矩陣。
Shader的直接子類:
BitmapShader : 位圖圖像渲染
LinearGradient : 線性渲染
RadialGradient : 環形渲染
SweepGradient : 掃描漸變渲染/梯度渲染
ComposeShader : 組合渲染,能夠和其餘幾個子類組合起來使用
是否是很像Animation及其子類的關係(AlphaAnimation,RotateAnimation,ScaleAnimation,TranslateAnimation, AnimationSet)
既有具體的渲染效果,也有渲染效果的組合
下面說下Shader的使用步驟:
1. 構建Shader對象
2. 經過Paint的setShader方法設置渲染對象
3.設置渲染對象
4.繪製時使用這個Paint對象
那麼下面就開始來介紹各個Shader的相關知識:
public BitmapShader(Bitmap bitmap,Shader.TileMode tileX,Shader.TileMode tileY)
調用這個方法來產生一個畫有一個位圖的渲染器(Shader)。
bitmap 在渲染器內使用的位圖
tileX The tiling mode for x to draw the bitmap in. 在位圖上X方向渲染器平鋪模式
tileY The tiling mode for y to draw the bitmap in. 在位圖上Y方向渲染器平鋪模式
TileMode:
CLAMP :若是渲染器超出原始邊界範圍,會複製範圍內邊緣染色。
REPEAT :橫向和縱向的重複渲染器圖片,平鋪。
MIRROR :橫向和縱向的重複渲染器圖片,這個和REPEAT重複方式不同,他是以鏡像方式平鋪。
代碼:
package com.tony.shader; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Shader; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.OvalShape; import android.util.AttributeSet; import android.view.View; public class BitmapShaderView extends View { private BitmapShader bitmapShader = null; private Bitmap bitmap = null; private Paint paint = null; private ShapeDrawable shapeDrawable = null; private int BitmapWidth = 0; private int BitmapHeight = 0; public BitmapShaderView(Context context) { super(context); // 獲得圖像 bitmap = ((BitmapDrawable) getResources().getDrawable(R.drawable.cat)) .getBitmap(); BitmapWidth = bitmap.getWidth(); BitmapHeight = bitmap.getHeight(); // 構造渲染器BitmapShader bitmapShader = new BitmapShader(bitmap, Shader.TileMode.MIRROR,Shader.TileMode.REPEAT); } public BitmapShaderView(Context context,AttributeSet set) { super(context, set); } @Override protected void onDraw(Canvas canvas) { // TODO Auto-generated method stub super.onDraw(canvas); //將圖片裁剪爲橢圓形 //構建ShapeDrawable對象並定義形狀爲橢圓 shapeDrawable = new ShapeDrawable(new OvalShape()); //獲得畫筆並設置渲染器 shapeDrawable.getPaint().setShader(bitmapShader); //設置顯示區域 shapeDrawable.setBounds(20, 20,BitmapWidth-140,BitmapHeight); //繪製shapeDrawable shapeDrawable.draw(canvas); } }效果圖:
相信不少人都看過歌詞同步的效果, 一是豎直方向的滾動,另外一方面是水平方面的歌詞顏色漸變點亮效果,這種效果怎麼作呢? 這就須要用到LinearGradient線性渲染,下面仍是先看具體的使用:
LinearGradient有兩個構造函數;
public LinearGradient(float x0, float y0, float x1, float y1, int[] colors, float[] positions,Shader.TileMode tile)
參數:
float x0: 漸變起始點x座標
float y0:漸變起始點y座標
float x1:漸變結束點x座標
float y1:漸變結束點y座標
int[] colors:顏色 的int 數組
float[] positions: 相對位置的顏色數組,可爲null, 若爲null,可爲null,顏色沿漸變線均勻分佈
Shader.TileMode tile: 渲染器平鋪模式
public LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1,Shader.TileMode tile)
float x0: 漸變起始點x座標
float y0:漸變起始點y座標
float x1:漸變結束點x座標
float y1:漸變結束點y座標
int color0: 起始漸變色
int color1: 結束漸變色
Shader.TileMode tile: 渲染器平鋪模式
代碼:
package com.tony.shader; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.LinearGradient; import android.graphics.Paint; import android.util.AttributeSet; import android.graphics.Shader; import android.view.View; public class LinearGradientView extends View { private LinearGradient linearGradient = null; private Paint paint = null; public LinearGradientView(Context context) { super(context); linearGradient = new LinearGradient(0, 0, 100, 100, new int[] { Color.YELLOW, Color.GREEN, Color.TRANSPARENT, Color.WHITE }, null, Shader.TileMode.REPEAT); paint = new Paint(); } public LinearGradientView(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onDraw(Canvas canvas) { // TODO Auto-generated method stub super.onDraw(canvas); //設置渲染器 paint.setShader(linearGradient); //繪製圓環 canvas.drawCircle(240, 360, 200, paint); } }效果:
關於這個渲染對象,咱們須要多解釋一下了,由於這個渲染器用的地方不少:
咱們在具體看一下他的構造方法中的參數含義:
Paint paint2 = new Paint(); paint2.setColor(Color.BLACK); paint2.setStrokeWidth(5); paint2.setStyle(Paint.Style.FILL); Shader mShader = new LinearGradient(0,0,100,100, Color.RED, Color.BLUE, Shader.TileMode.CLAMP); paint2.setShader(mShader); Rect rect = new Rect(); rect.left = 0; rect.right = 300; rect.top = 0; rect.bottom = 300; canvas.drawRect(rect, paint2);效果圖:
咱們把構造方法中的值改變一下:
Shader mShader = new LinearGradient(0,0,300,300, Color.RED, Color.BLUE, Shader.TileMode.CLAMP);在看一下效果:
這裏咱們就是到了構造方法中的四個參數值的含義了:
參數一:渲染開始點的x座標
參數二:渲染開始點的y座標
參數三:渲染結束點的x座標
參數四:渲染結束點的y座標
由於這裏咱們設置矩形的大小是高和寬都是300
因此,從第一個例子中咱們能夠看出:渲染結束點以後的顏色是最後一種顏色:藍色
咱們在將代碼改變一下:
Shader mShader = new LinearGradient(0,0,300,0, Color.RED, Color.BLUE, Shader.TileMode.CLAMP);效果:
結束點的座標設置成:(300,0)就實現了橫向渲染
固然咱們也能夠實現縱向渲染的,這裏就不演示了。
在修改一下代碼:
Shader mShader = new LinearGradient(0,0,100,100, Color.RED, Color.BLUE, Shader.TileMode.MIRROR);效果:
咱們將渲染模式改爲:Shader.TileMode.MIRROR 鏡像模式了
咱們看到效果,當渲染結束點是(100,100)的時候,那麼後面仍是會繼續渲染的,並且是相反的(就像照鏡子同樣),而後在渲染一下,每次渲染的效果都是和以前的相反。由於矩形的長度和寬度都是300,因此這裏會渲染三次。
咱們在將代碼修改一下:
Shader mShader = new LinearGradient(0,0,100,100, Color.RED, Color.BLUE, Shader.TileMode.REPEAT);將渲染模式改爲:Shader.TileMode.REPEAT 重複模式了
效果:
這裏看到也是會渲染三次的,可是和鏡像模式不一樣的是,它們的渲染方向都是一致的。
從上面的三種渲染模式能夠看出來,以後渲染的結束點小於渲染圖形的大小的時候纔會有效果的,若是咱們把大小改一下:
Shader mShader = new LinearGradient(0,0,300,300, Color.RED, Color.BLUE, Shader.TileMode.REPEAT);咱們渲染結束點改爲矩形的高度和寬度大小
效果和Shader.TileMode.CLAMP同樣的。
這種渲染器用的地方仍是不少的,咱們後面介紹長條漸變的SeekBar就要用到這種渲染器
圓形渲染器,這種渲染器很好理解,就是同心圓的渲染機制
public RadialGradient(float x, float y, float radius, int[] colors, float[] positions,Shader.TileMode tile)
float x: 圓心X座標
float y: 圓心Y座標
float radius: 半徑
int[] colors: 渲染顏色數組
floate[] positions: 相對位置數組,可爲null, 若爲null,可爲null,顏色沿漸變線均勻分佈
Shader.TileMode tile:渲染器平鋪模式
public RadialGradient(float x, float y, float radius, int color0, int color1,Shader.TileMode tile)
float x: 圓心X座標
float y: 圓心Y座標
float radius: 半徑
int color0: 圓心顏色
int color1: 圓邊緣顏色
Shader.TileMode tile:渲染器平鋪模式
代碼:
package com.tony.shader; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RadialGradient; import android.graphics.Shader; import android.util.AttributeSet; import android.view.View; public class RadialGradientView extends View { Paint mPaint = null; // 環形漸變渲染 Shader mRadialGradient = null; public RadialGradientView(Context context) { super(context); //1.圓心X座標2.Y座標3.半徑 4.顏色數組 5.相對位置數組,可爲null 6.渲染器平鋪模式 mRadialGradient = new RadialGradient(240, 240, 240, new int[] { Color.YELLOW, Color.GREEN, Color.TRANSPARENT, Color.RED }, null, Shader.TileMode.REPEAT); mPaint = new Paint(); } public RadialGradientView(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onDraw(Canvas canvas) { // 繪製環形漸變 mPaint.setShader(mRadialGradient); // 第一個,第二個參數表示圓心座標 // 第三個參數表示半徑 canvas.drawCircle(240, 360, 200, mPaint); } }效果:
關於這個圓形渲染器,咱們能夠實現水波紋的效果:
package com.tony.testshader; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RadialGradient; import android.graphics.Shader; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.OvalShape; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.MotionEvent; import android.view.View; /** * 水波紋效果 * @author tony * */ public class WaterRipplesView extends View { Shader mBitmapShader = null; Bitmap mBitmapPn = null; Paint mPaint = null; Shader mRadialGradient = null; Canvas mCanvas = null; ShapeDrawable mShapeDrawable = null; public WaterRipplesView(Context context) { super(context); // 初始化工做 Bitmap bitmapTemp = ((BitmapDrawable) getResources().getDrawable( R.drawable.leaf)).getBitmap(); DisplayMetrics dm = getResources().getDisplayMetrics(); // 建立與當前使用的設備窗口大小一致的圖片 mBitmapPn = Bitmap.createScaledBitmap(bitmapTemp, dm.widthPixels, dm.heightPixels, true); // 建立BitmapShader object mBitmapShader = new BitmapShader(mBitmapPn, Shader.TileMode.REPEAT, Shader.TileMode.MIRROR); mPaint = new Paint(); } public WaterRipplesView(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onDraw(Canvas canvas) { // TODO Auto-generated method stub super.onDraw(canvas); // 將圖片裁剪爲橢圓型 // 建立ShapeDrawable object,並定義形狀爲橢圓 mShapeDrawable = new ShapeDrawable(new OvalShape());// OvalShape:橢圓 // 設置要繪製的橢圓形的東西爲ShapeDrawable圖片 mShapeDrawable.getPaint().setShader(mBitmapShader); // 設置顯示區域 mShapeDrawable.setBounds(0, 0, mBitmapPn.getWidth(), mBitmapPn.getHeight()); // 繪製ShapeDrawable mShapeDrawable.draw(canvas); if (mRadialGradient != null) { mPaint.setShader(mRadialGradient); canvas.drawCircle(0, 0, 1000, mPaint); } } // @覆寫觸摸屏事件 public boolean onTouchEvent(MotionEvent event) { // @設置alpha通道(透明度) mPaint.setAlpha(400); mRadialGradient = new RadialGradient(event.getX(), event.getY(), 48, new int[] { Color.WHITE, Color.TRANSPARENT },null, Shader.TileMode.REPEAT); // @重繪 postInvalidate(); return true; } }重寫觸發方法,獲取觸發點座標,設置渲染器的圓形座標,便可
梯度渲染器,或者是扇形選擇器,和雷達掃描效果差很少
public SweepGradient(float cx, float cy, int[] colors, float[] positions)
Parameters:
cx 渲染中心點x 座標
cy 渲染中心y 點座標
colors 圍繞中心渲染的顏色數組,至少要有兩種顏色值
positions 相對位置的顏色數組,可爲null, 若爲null,可爲null,顏色沿漸變線均勻分佈
public SweepGradient(float cx, float cy, int color0, int color1)
Parameters:
cx 渲染中心點x 座標
cy 渲染中心點y 座標
color0 起始渲染顏色
color1 結束渲染顏色
代碼:
package com.tony.testshader; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Shader; import android.graphics.SweepGradient; import android.util.AttributeSet; import android.view.View; public class SweepGradientView extends View { Paint mPaint = null; // 梯度渲染 Shader mSweepGradient = null; public SweepGradientView(Context context) { super(context); // 建立SweepGradient對象 // 第一個,第二個參數中心座標 // 後面的參數與線性渲染相同 mSweepGradient = new SweepGradient(240, 360, new int[] {Color.CYAN,Color.DKGRAY,Color.GRAY,Color.LTGRAY,Color.MAGENTA, Color.GREEN,Color.TRANSPARENT, Color.BLUE }, null); mPaint = new Paint(); } public SweepGradientView(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 繪製梯度漸變 mPaint.setShader(mSweepGradient); canvas.drawCircle(240, 360, 200, mPaint); } }效果圖:
這種渲染器用到的地方不少,咱們後面說道自定義圓形漸變的SeekBar就會用到這種渲染器
下面咱們就是開始正式的進入自定義視圖View了
在講解正式內容以前,咱們先來看一下基本知識
public LabelView(Context context, AttributeSet attrs)
否則會報錯:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec)); }來設置View的大小:
private int measureWidth(int measureSpec) { int result = 0; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); Log.i("DEMO","measureSpec:"+Integer.toBinaryString(measureSpec)); Log.i("DEMO","specMode:"+Integer.toBinaryString(specMode)); Log.i("DEMO","specSize:"+Integer.toBinaryString(specSize)); /** * 通常來講,自定義控件都會去重寫View的onMeasure方法,由於該方法指定該控件在屏幕上的大小。 protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) onMeasure傳入的兩個參數是由上一層控件傳入的大小,有多種狀況,重寫該方法時須要對計算控件的實際大小,而後調用setMeasuredDimension(int, int)設置實際大小。 onMeasure傳入的widthMeasureSpec和heightMeasureSpec不是通常的尺寸數值,而是將模式和尺寸組合在一塊兒的數值。 咱們須要經過int mode = MeasureSpec.getMode(widthMeasureSpec)獲得模式,用int size = MeasureSpec.getSize(widthMeasureSpec)獲得尺寸。 mode共有三種狀況,取值分別爲MeasureSpec.UNSPECIFIED, MeasureSpec.EXACTLY, MeasureSpec.AT_MOST。 MeasureSpec.EXACTLY是精確尺寸,當咱們將控件的layout_width或layout_height指定爲具體數值時如andorid:layout_width="50dip", 或者爲FILL_PARENT是,都是控件大小已經肯定的狀況,都是精確尺寸。 MeasureSpec.AT_MOST是最大尺寸,當控件的layout_width或layout_height指定爲WRAP_CONTENT時, 控件大小通常隨着控件的子空間或內容進行變化,此時控件尺寸只要不超過父控件容許的最大尺寸便可。所以,此時的mode是AT_MOST,size給出了父控件容許的最大尺寸。 MeasureSpec.UNSPECIFIED是未指定尺寸,這種狀況很少,通常都是父控件是AdapterView,經過measure方法傳入的模式。 所以,在重寫onMeasure方法時要根據模式不一樣進行尺寸計算。下面代碼就是一種比較典型的方式: */ if (specMode == MeasureSpec.EXACTLY) { // We were told how big to be result = specSize; } else { // Measure the text result = (int) mTextPaint.measureText(mText) + getPaddingLeft() + getPaddingRight(); if (specMode == MeasureSpec.AT_MOST) { // Respect AT_MOST value if that was what is called for by measureSpec result = Math.min(result, specSize); } } return result; }在這個方法中咱們須要計算View的具體寬度了:
int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec);MeasureSpec提供了兩個方法:getMode和getSize
這兩個方法是獲取計算模式和大小的,他們內部實現是用位操做的,咱們看一下源碼:
用一個int類型就能夠將mode和size表示出來:int類型是32位的,這裏用高2位表示mode.低30位表示大小。
咱們能夠在上面打印一下log看一下:
內部處理很簡單,直接進行位相與操做就能夠了:
通常來講,自定義控件都會去重寫View的onMeasure方法,由於該方法指定該控件在屏幕上的大小。
protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec)
onMeasure傳入的兩個參數是由上一層控件傳入的大小,有多種狀況,重寫該方法時須要對計算控件的實際大小,而後調用
setMeasuredDimension(int, int)設置實際大小。onMeasure傳入的widthMeasureSpec和heightMeasureSpec不是通常的尺寸數值,而是將模式和尺寸組合在一塊兒的數值。咱們須要經過
int mode = MeasureSpec.getMode(widthMeasureSpec)獲得模式,
用int size = MeasureSpec.getSize(widthMeasureSpec)獲得尺寸。
Mode共有三種狀況,
取值分別爲
MeasureSpec.UNSPECIFIED
MeasureSpec.EXACTLY
MeasureSpec.AT_MOST
A) MeasureSpec.EXACTLY是精確尺寸,當咱們將控件的layout_width或layout_height指定爲具體數值時如andorid:layout_width="50dip",或者爲FILL_PARENT是,都是控件大小已經肯定的狀況,都是精確尺寸。
B) MeasureSpec.AT_MOST是最大尺寸,當控件的layout_width或layout_height指定爲WRAP_CONTENT時,
控件大小通常隨着控件的子空間或內容進行變化,此時控件尺寸只要不超過父控件容許的最大尺寸便可。所以,此時的mode是AT_MOST,size給出了父控件容許的最大尺寸。
C) MeasureSpec.UNSPECIFIED是未指定尺寸,這種狀況很少,通常都是父控件是AdapterView,經過measure方法傳入的模式。
所以,在重寫onMeasure方法時要根據模式不一樣進行尺寸計算。下面代碼就是一種比較典型的方式:
獲取座標,計算座標,而後經過invalidate和postInvalidate方法進行畫面的刷新操做便可
關於這兩個刷新方法的區別是:invalidate方法是在UI線程中調用的,postInvalidate能夠在子線程中調用,並且最重要的是postInvalidate能夠延遲調用
這個View主要實現的功能和Android中提供的TextView差很少
代碼:
package com.example.drawpathdemo; /* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Need the following import to get access to the app resources, since this // class is in a sub-package. import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.util.AttributeSet; import android.util.Log; import android.view.View; /** * Example of how to write a custom subclass of View. LabelView * is used to draw simple text views. Note that it does not handle * styled text or right-to-left writing systems. * */ public class LabelView extends View { private Paint mTextPaint; private String mText; private int mAscent; /** * Constructor. This version is only needed if you will be instantiating * the object manually (not from a layout XML file). * @param context */ public LabelView(Context context) { super(context); initLabelView(); } /** * Construct object, initializing with any attributes we understand from a * layout file. These attributes are defined in * SDK/assets/res/any/classes.xml. * * @see android.view.View#View(android.content.Context, android.util.AttributeSet) */ public LabelView(Context context, AttributeSet attrs) { super(context, attrs); initLabelView(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LabelView); CharSequence s = a.getString(R.styleable.LabelView_text); if (s != null) { setText(s.toString()); } // Retrieve the color(s) to be used for this view and apply them. // Note, if you only care about supporting a single color, that you // can instead call a.getColor() and pass that to setTextColor(). setTextColor(a.getColor(R.styleable.LabelView_textColor, 0xFF000000)); int textSize = a.getDimensionPixelOffset(R.styleable.LabelView_textSize, 0); if (textSize > 0) { setTextSize(textSize); } a.recycle(); } private final void initLabelView() { mTextPaint = new Paint(); mTextPaint.setAntiAlias(true); // Must manually scale the desired text size to match screen density mTextPaint.setTextSize(16 * getResources().getDisplayMetrics().density); mTextPaint.setColor(0xFF000000); setPadding(3, 3, 3, 3); } /** * Sets the text to display in this label * @param text The text to display. This will be drawn as one line. */ public void setText(String text) { mText = text; requestLayout(); invalidate(); } /** * Sets the text size for this label * @param size Font size */ public void setTextSize(int size) { // This text size has been pre-scaled by the getDimensionPixelOffset method mTextPaint.setTextSize(size); requestLayout(); invalidate(); } /** * Sets the text color for this label. * @param color ARGB value for the text */ public void setTextColor(int color) { mTextPaint.setColor(color); invalidate(); } /** * @see android.view.View#measure(int, int) */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec)); } /** * Determines the width of this view * @param measureSpec A measureSpec packed into an int * @return The width of the view, honoring constraints from measureSpec */ private int measureWidth(int measureSpec) { int result = 0; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); Log.i("DEMO","measureSpec:"+Integer.toBinaryString(measureSpec)); Log.i("DEMO","specMode:"+Integer.toBinaryString(specMode)); Log.i("DEMO","specSize:"+Integer.toBinaryString(specSize)); /** * 通常來講,自定義控件都會去重寫View的onMeasure方法,由於該方法指定該控件在屏幕上的大小。 protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) onMeasure傳入的兩個參數是由上一層控件傳入的大小,有多種狀況,重寫該方法時須要對計算控件的實際大小,而後調用setMeasuredDimension(int, int)設置實際大小。 onMeasure傳入的widthMeasureSpec和heightMeasureSpec不是通常的尺寸數值,而是將模式和尺寸組合在一塊兒的數值。 咱們須要經過int mode = MeasureSpec.getMode(widthMeasureSpec)獲得模式,用int size = MeasureSpec.getSize(widthMeasureSpec)獲得尺寸。 mode共有三種狀況,取值分別爲MeasureSpec.UNSPECIFIED, MeasureSpec.EXACTLY, MeasureSpec.AT_MOST。 MeasureSpec.EXACTLY是精確尺寸,當咱們將控件的layout_width或layout_height指定爲具體數值時如andorid:layout_width="50dip", 或者爲FILL_PARENT是,都是控件大小已經肯定的狀況,都是精確尺寸。 MeasureSpec.AT_MOST是最大尺寸,當控件的layout_width或layout_height指定爲WRAP_CONTENT時, 控件大小通常隨着控件的子空間或內容進行變化,此時控件尺寸只要不超過父控件容許的最大尺寸便可。所以,此時的mode是AT_MOST,size給出了父控件容許的最大尺寸。 MeasureSpec.UNSPECIFIED是未指定尺寸,這種狀況很少,通常都是父控件是AdapterView,經過measure方法傳入的模式。 所以,在重寫onMeasure方法時要根據模式不一樣進行尺寸計算。下面代碼就是一種比較典型的方式: */ if (specMode == MeasureSpec.EXACTLY) { // We were told how big to be result = specSize; } else { // Measure the text result = (int) mTextPaint.measureText(mText) + getPaddingLeft() + getPaddingRight(); if (specMode == MeasureSpec.AT_MOST) { // Respect AT_MOST value if that was what is called for by measureSpec result = Math.min(result, specSize); } } return result; } /** * Determines the height of this view * @param measureSpec A measureSpec packed into an int * @return The height of the view, honoring constraints from measureSpec */ private int measureHeight(int measureSpec) { int result = 0; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); mAscent = (int) mTextPaint.ascent(); if (specMode == MeasureSpec.EXACTLY) { // We were told how big to be result = specSize; } else { // Measure the text (beware: ascent is a negative number) result = (int) (-mAscent + mTextPaint.descent()) + getPaddingTop() + getPaddingBottom(); if (specMode == MeasureSpec.AT_MOST) { // Respect AT_MOST value if that was what is called for by measureSpec result = Math.min(result, specSize); } } return result; } /** * Render the text * * @see android.view.View#onDraw(android.graphics.Canvas) */ @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawText(mText, getPaddingLeft(), getPaddingTop() - mAscent, mTextPaint); } }首先來看一下構造方法方法:
/** * Construct object, initializing with any attributes we understand from a * layout file. These attributes are defined in * SDK/assets/res/any/classes.xml. * * @see android.view.View#View(android.content.Context, android.util.AttributeSet) */ public LabelView(Context context, AttributeSet attrs) { super(context, attrs); initLabelView(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LabelView); CharSequence s = a.getString(R.styleable.LabelView_text); if (s != null) { setText(s.toString()); } // Retrieve the color(s) to be used for this view and apply them. // Note, if you only care about supporting a single color, that you // can instead call a.getColor() and pass that to setTextColor(). setTextColor(a.getColor(R.styleable.LabelView_textColor, 0xFF000000)); int textSize = a.getDimensionPixelOffset(R.styleable.LabelView_textSize, 0); if (textSize > 0) { setTextSize(textSize); } a.recycle(); }這裏面咱們用到了自定義屬性的相關知識,不瞭解的同窗能夠轉戰:
http://blog.csdn.net/jiangwei0910410003/article/details/17006087
拿到字體的大小、顏色、內容。
而後再來看一下:
onDraw方法:
/** * Render the text * * @see android.view.View#onDraw(android.graphics.Canvas) */ @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawText(mText, getPaddingLeft(), getPaddingTop() - mAscent, mTextPaint); }這裏使用drawText方法開始繪製文本內容,關於getPaddingLeft就是獲取字體在View中的Padding值,可是這裏有一個知識點:
mAscent變量,它是經過:
mAscent = (int) mTextPaint.ascent();獲取到的。關於Paint的兩個方法ascent和descent,這裏解釋一下:以下圖
1.基準點是baseline
2.ascent:是baseline之上至字符最高處的距離
3.descent:是baseline之下至字符最低處的距離
4.leading:是上一行字符的descent到下一行的ascent之間的距離,也就是相鄰行間的空白距離
5.top:是指的是最高字符到baseline的值,即ascent的最大值
6.bottom:是指最低字符到baseline的值,即descent的最大值
再來看一下設置字體顏色的方法:
/** * Sets the text color for this label. * @param color ARGB value for the text */ public void setTextColor(int color) { mTextPaint.setColor(color); invalidate(); }就是設置畫筆的顏色,設置完以後須要生效,那麼調用invalidate方法刷新一下便可。
上面的LabelView就定義好了。是否是很簡單。沒什麼難度。主要就是定義一個畫筆繪製文本,而後在計算view的大小便可。
下面來看一下用法:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:test="http://schemas.android.com/apk/res/com.example.drawpathdemo" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.example.drawpathdemo.MainActivity" > <com.example.drawpathdemo.LabelView android:id="@+id/progressview" android:layout_width="100dp" android:layout_height="wrap_content" test:text="aaa" test:textColor="#990099" test:textSize="16dp"/> </RelativeLayout>注意前綴test的定義方式:xmlns:test="http://schemas.android.com/apk/res/com.example.drawpathdemo"
xmlns:test="http://schemas.android.com/apk/res/包名"便可
效果圖:
咱們知道系統自帶的SeekBar控件是沒有漸變色的,好比下面這種效果:
看到這裏咱們可能會想到了,咱們以前說到的Shader渲染對象了,這裏咱們選擇LinearGradient渲染器來實現
代碼:
package com.example.drawpathdemo; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.RectF; import android.graphics.Shader; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; public class SpringProgressView extends View { private static final int[] SECTION_COLORS = {0xffffd300,Color.GREEN,0xff319ed4}; private float maxCount; private float currentCount; private Paint mPaint; private int mWidth,mHeight; private Bitmap bitMap; public SpringProgressView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(context); } public SpringProgressView(Context context, AttributeSet attrs) { super(context, attrs); initView(context); } public SpringProgressView(Context context) { super(context); initView(context); } private void initView(Context context) { /*bitMap = BitmapFactory.decodeResource(context.getResources(), R.drawable.scrubber_control_pressed_holo);*/ } @SuppressLint("DrawAllocation") @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mPaint = new Paint(); mPaint.setAntiAlias(true); int round = mHeight/2; mPaint.setColor(Color.GRAY); RectF rectBg = new RectF(0, 0, mWidth, mHeight); canvas.drawRoundRect(rectBg, round, round, mPaint); mPaint.setColor(Color.WHITE); RectF rectBlackBg = new RectF(2, 2, mWidth-2, mHeight-2); canvas.drawRoundRect(rectBlackBg, round, round, mPaint); float section = currentCount/maxCount; RectF rectProgressBg = new RectF(3, 3, (mWidth-3)*section, mHeight-3); if(section <= 1.0f/3.0f){ if(section != 0.0f){ mPaint.setColor(SECTION_COLORS[0]); }else{ mPaint.setColor(Color.TRANSPARENT); } }else{ int count = (section <= 1.0f/3.0f*2 ) ? 2 : 3; int[] colors = new int[count]; System.arraycopy(SECTION_COLORS, 0, colors, 0, count); float[] positions = new float[count]; if(count == 2){ positions[0] = 0.0f; positions[1] = 1.0f-positions[0]; }else{ positions[0] = 0.0f; positions[1] = (maxCount/3)/currentCount; positions[2] = 1.0f-positions[0]*2; } positions[positions.length-1] = 1.0f; LinearGradient shader = new LinearGradient(3, 3, (mWidth-3)*section, mHeight-3, colors,null, Shader.TileMode.MIRROR); mPaint.setShader(shader); } canvas.drawRoundRect(rectProgressBg, round, round, mPaint); //canvas.drawBitmap(bitMap, rectProgressBg.right-20, rectProgressBg.top-4, null); } private int dipToPx(int dip) { float scale = getContext().getResources().getDisplayMetrics().density; return (int) (dip * scale + 0.5f * (dip >= 0 ? 1 : -1)); } public void setMaxCount(float maxCount) { this.maxCount = maxCount; } public void setCurrentCount(float currentCount) { this.currentCount = currentCount > maxCount ? maxCount : currentCount; invalidate(); } public float getMaxCount() { return maxCount; } public float getCurrentCount() { return currentCount; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); if (widthSpecMode == MeasureSpec.EXACTLY || widthSpecMode == MeasureSpec.AT_MOST) { mWidth = widthSpecSize; } else { mWidth = 0; } if (heightSpecMode == MeasureSpec.AT_MOST || heightSpecMode == MeasureSpec.UNSPECIFIED) { mHeight = dipToPx(15); } else { mHeight = heightSpecSize; } setMeasuredDimension(mWidth, mHeight); } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); Log.i("DEMO", "x:"+x + ",y:"+y); getParent().requestDisallowInterceptTouchEvent(true); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: moved(x, y); break; case MotionEvent.ACTION_MOVE: moved(x, y); break; case MotionEvent.ACTION_UP: moved(x, y); break; } return true; } private void moved(float x,float y){ if(x > mWidth){ return; } currentCount = maxCount * (x/mWidth); invalidate(); } }主要看一下onDraw方法:
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mPaint = new Paint(); mPaint.setAntiAlias(true); int round = mHeight/2; mPaint.setColor(Color.GRAY); RectF rectBg = new RectF(0, 0, mWidth, mHeight); canvas.drawRoundRect(rectBg, round, round, mPaint); mPaint.setColor(Color.WHITE); RectF rectBlackBg = new RectF(2, 2, mWidth-2, mHeight-2); canvas.drawRoundRect(rectBlackBg, round, round, mPaint); float section = currentCount/maxCount; RectF rectProgressBg = new RectF(3, 3, (mWidth-3)*section, mHeight-3); if(section <= 1.0f/3.0f){ if(section != 0.0f){ mPaint.setColor(SECTION_COLORS[0]); }else{ mPaint.setColor(Color.TRANSPARENT); } }else{ int count = (section <= 1.0f/3.0f*2 ) ? 2 : 3; int[] colors = new int[count]; System.arraycopy(SECTION_COLORS, 0, colors, 0, count); float[] positions = new float[count]; if(count == 2){ positions[0] = 0.0f; positions[1] = 1.0f-positions[0]; }else{ positions[0] = 0.0f; positions[1] = (maxCount/3)/currentCount; positions[2] = 1.0f-positions[0]*2; } positions[positions.length-1] = 1.0f; LinearGradient shader = new LinearGradient(3, 3, (mWidth-3)*section, mHeight-3, colors,null, Shader.TileMode.MIRROR); mPaint.setShader(shader); } canvas.drawRoundRect(rectProgressBg, round, round, mPaint); //canvas.drawBitmap(bitMap, rectProgressBg.right-20, rectProgressBg.top-4, null); }這裏首先繪製一個圓角矩形,繪製了兩個做爲SeekBar的背景圖片:
RectF rectBg = new RectF(0, 0, mWidth, mHeight); canvas.drawRoundRect(rectBg, round, round, mPaint); mPaint.setColor(Color.WHITE); RectF rectBlackBg = new RectF(2, 2, mWidth-2, mHeight-2); canvas.drawRoundRect(rectBlackBg, round, round, mPaint);
SeekBar最大的值:maxCount
SeekBar當前的值:currentCount
float section = currentCount/maxCount; RectF rectProgressBg = new RectF(3, 3, (mWidth-3)*section, mHeight-3); if(section <= 1.0f/3.0f){ if(section != 0.0f){ mPaint.setColor(SECTION_COLORS[0]); }else{ mPaint.setColor(Color.TRANSPARENT); } }else{ int count = (section <= 1.0f/3.0f*2 ) ? 2 : 3; int[] colors = new int[count]; System.arraycopy(SECTION_COLORS, 0, colors, 0, count); float[] positions = new float[count]; if(count == 2){ positions[0] = 0.0f; positions[1] = 1.0f-positions[0]; }else{ positions[0] = 0.0f; positions[1] = (maxCount/3)/currentCount; positions[2] = 1.0f-positions[0]*2; } positions[positions.length-1] = 1.0f; LinearGradient shader = new LinearGradient(3, 3, (mWidth-3)*section, mHeight-3, colors,null, Shader.TileMode.MIRROR); mPaint.setShader(shader); } canvas.drawRoundRect(rectProgressBg, round, round, mPaint);計算好了漸變色的距離,咱們在用drawRoundRect方法繪製一個漸變色的圓角矩形便可。
再看一下onTouchEvent方法:
@Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); Log.i("DEMO", "x:"+x + ",y:"+y); getParent().requestDisallowInterceptTouchEvent(true); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: moved(x, y); break; case MotionEvent.ACTION_MOVE: moved(x, y); break; case MotionEvent.ACTION_UP: moved(x, y); break; } return true; } private void moved(float x,float y){ if(x > mWidth){ return; } currentCount = maxCount * (x/mWidth); invalidate(); }這裏就會計算手指的座標,而後經過手指移動的x座標和SeekBar的寬度的比例在計算出當前的currentCount值,而後刷新一下界面便可。
在使用的時候咱們須要設置最大值和當前值:
SpringProgressView view = (SpringProgressView)findViewById(R.id.view); view.setMaxCount(100); view.setCurrentCount(30);
效果圖以下:
效果以下:
很少說,直接上代碼:
package com.example.drawpathdemo; import android.content.Context; import android.graphics.Canvas; import android.graphics.LinearGradient; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Shader; import android.util.AttributeSet; import android.widget.TextView; public class MyTextView extends TextView { private LinearGradient mLinearGradient; private Matrix mGradientMatrix; private Paint mPaint; private int mViewWidth = 0; private int mTranslate = 0; private boolean mAnimating = true; public MyTextView(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (mViewWidth == 0) { mViewWidth = getMeasuredWidth(); if (mViewWidth > 0) { mPaint = getPaint(); mLinearGradient = new LinearGradient(0,0,mViewWidth,0, new int[] { 0x33ffffff, 0xffffffff, 0x33ffffff },null,Shader.TileMode.CLAMP); mPaint.setShader(mLinearGradient); mGradientMatrix = new Matrix(); } } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mAnimating && mGradientMatrix != null) { mTranslate += mViewWidth / 10; if (mTranslate > 2 * mViewWidth) { mTranslate = -mViewWidth; } mGradientMatrix.setTranslate(mTranslate, 0); mLinearGradient.setLocalMatrix(mGradientMatrix); postInvalidateDelayed(50); } } }
這裏咱們詳細來看一下onSizeChanged方法:
這個方法在TextView大小發生改變的時候會調用,通常咱們在xml中已經把文本設置好了,只會調用一次。咱們在這裏面進行一次啊初始化操做。咱們首先用getPaint方法拿到TextView的畫筆,而後定義一個LinearGradient渲染器,可是咱們如今還須要一個功能就是那個白色漸變可以自動從左往右移,那麼就會使用到平移動畫。這裏就會使用到一個新的對象:Matrix
矩陣對象,咱們若是瞭解圖形學的話,會知道矩陣是重要的一個概念,他能夠實現圖片的轉化,各類動畫的實現等。
Android中咱們能夠給一個渲染器設置一個變化矩陣。對於矩陣咱們能夠設置平移,旋轉,縮放的動畫。那麼咱們這裏就是用他的平移動畫:
咱們就把LinearGradient這個比做一個長方形,如上圖是初始化的位置在手機屏幕的最左邊,要運動到屏幕的最右邊就須要2*width的長度。
咱們在onDraw方法中就開始計算移動的座標,而後調用postInvalidate方法延遲去刷新界面。
下面來看一下效果圖:
這個例子中咱們學習到了渲染器能夠有動畫的,咱們能夠對渲染器進行動畫操做,這個知識點,咱們在後面還會在用到。
咱們知道Office辦公軟件中有一個功能就是有一個顏色選擇器板:
那麼在Android中咱們,咱們就來實現如下這樣的效果。其實咱們主要介紹的是選擇器版面的View,由於這裏會用到Shader,上面這張圖片咱們知道可使用LinearGradient渲染器實現,可是咱們這裏定義一個圓形選擇器,效果圖以下:
那麼這裏咱們看到效果圖以後,知道應該使用掃描渲染器:SweepGradient
原理:咱們在onTouchEvent方法中獲取用戶點擊了那一塊顏色區域而後計算出它在哪一塊顏色區域便可。
代碼:
package com.example.drawpathdemo; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorMatrix; import android.graphics.Paint; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.SweepGradient; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; public class ColorPickerView extends View { private Paint mPaint;//漸變色環畫筆 private Paint mCenterPaint;//中間圓畫筆 private int[] mColors;//漸變色環顏色 private OnColorChangedListener mListener;//顏色改變回調 private static final int CENTER_X = 200; private static final int CENTER_Y = 200; private static final int CENTER_RADIUS = 32; public ColorPickerView(Context context){ super(context); } public ColorPickerView(Context context, AttributeSet attrs) { super(context, attrs); mColors = new int[] {//漸變色數組 0xFFFF0000, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF, 0xFF00FF00, 0xFFFFFF00, 0xFFFF0000 }; Shader s = new SweepGradient(0, 0, mColors, null); //初始化漸變色畫筆 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setShader(s); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(32); //初始化中心園畫筆 mCenterPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mCenterPaint.setColor(Color.RED); mCenterPaint.setStrokeWidth(15); } public ColorPickerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } private boolean mTrackingCenter; private boolean mHighlightCenter; @Override protected void onDraw(Canvas canvas) { float r = CENTER_X - mPaint.getStrokeWidth()*0.5f; //移動中心 canvas.translate(CENTER_X, CENTER_Y); //畫出色環和中心園 canvas.drawOval(new RectF(-r, -r, r, r), mPaint); canvas.drawCircle(0, 0, CENTER_RADIUS, mCenterPaint); if (mTrackingCenter) { int c = mCenterPaint.getColor(); mCenterPaint.setStyle(Paint.Style.STROKE); if (mHighlightCenter) { mCenterPaint.setAlpha(0xFF); } else { mCenterPaint.setAlpha(0x80); } canvas.drawCircle(0, 0, CENTER_RADIUS + mCenterPaint.getStrokeWidth(), mCenterPaint); mCenterPaint.setStyle(Paint.Style.FILL); mCenterPaint.setColor(c); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(CENTER_X*2, CENTER_Y*2); } private int ave(int s, int d, float p) { return s + java.lang.Math.round(p * (d - s)); } private int interpColor(int colors[], float unit) { if (unit <= 0) { return colors[0]; } if (unit >= 1) { return colors[colors.length - 1]; } /** * 1,2,3,4,5 * 0.6*4=2.4 * i=2,p=0.4 */ float p = unit * (colors.length - 1); int i = (int)p; p -= i; // now p is just the fractional part [0...1) and i is the index int c0 = colors[i]; int c1 = colors[i+1]; int a = ave(Color.alpha(c0), Color.alpha(c1), p); int r = ave(Color.red(c0), Color.red(c1), p); int g = ave(Color.green(c0), Color.green(c1), p); int b = ave(Color.blue(c0), Color.blue(c1), p); return Color.argb(a, r, g, b); } private static final float PI = 3.1415926f; @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX() - CENTER_X; float y = event.getY() - CENTER_Y; boolean inCenter = java.lang.Math.sqrt(x*x + y*y) <= CENTER_RADIUS; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mTrackingCenter = inCenter; if (inCenter) { mHighlightCenter = true; invalidate(); break; } case MotionEvent.ACTION_MOVE: if (mTrackingCenter) { if (mHighlightCenter != inCenter) { mHighlightCenter = inCenter; invalidate(); } } else { float angle = (float)java.lang.Math.atan2(y, x); // need to turn angle [-PI ... PI] into unit [0....1] float unit = angle/(2*PI); if (unit < 0) { unit += 1; } mCenterPaint.setColor(interpColor(mColors, unit)); invalidate(); } break; case MotionEvent.ACTION_UP: if (mTrackingCenter) { if (inCenter) { if(mListener != null) mListener.colorChanged(mCenterPaint.getColor()); } mTrackingCenter = false; invalidate(); } break; } return true; } public interface OnColorChangedListener{ public void colorChanged(int color); } }
public ColorPickerView(Context context, AttributeSet attrs) { super(context, attrs); mColors = new int[] {//漸變色數組 0xFFFF0000, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF, 0xFF00FF00, 0xFFFFFF00, 0xFFFF0000 }; Shader s = new SweepGradient(0, 0, mColors, null); //初始化漸變色畫筆 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setShader(s); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(32); //初始化中心園畫筆 mCenterPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mCenterPaint.setColor(Color.RED); mCenterPaint.setStrokeWidth(15); }定義一個渲染器,初始化兩個畫筆,一個是畫外邊的選擇器圓環,咱們這裏是經過設置輪廓的寬度來實現的,這樣作也是比較簡單的。
再來看一下onDraw方法:
@Override protected void onDraw(Canvas canvas) { float r = CENTER_X - mPaint.getStrokeWidth()*0.5f; //移動中心 canvas.translate(CENTER_X, CENTER_Y); //畫出色環和中心園 canvas.drawOval(new RectF(-r, -r, r, r), mPaint); canvas.drawCircle(0, 0, CENTER_RADIUS, mCenterPaint); if (mTrackingCenter) { int c = mCenterPaint.getColor(); mCenterPaint.setStyle(Paint.Style.STROKE); if (mHighlightCenter) { mCenterPaint.setAlpha(0xFF); } else { mCenterPaint.setAlpha(0x80); } canvas.drawCircle(0, 0, CENTER_RADIUS + mCenterPaint.getStrokeWidth(), mCenterPaint); mCenterPaint.setStyle(Paint.Style.FILL); mCenterPaint.setColor(c); } }這裏開始繪製圖形,調用了canvas.translate方法,將畫布移到圓環的中心,這樣作以後,咱們下面的在繪製圓形的時候,就不須要那麼複雜的計算圓心的座標了。
這裏有一個判斷,咱們在下面會說道,mTrackingCenter表示點擊了中間的圓形區域,mHightlightCenter表示從點擊了圓形以後,移動,這裏只是作了透明度的處理,說的有點抽象,能夠本身試驗一下就知道了。
在來看一下onTouchEvent方法:
@Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX() - CENTER_X; float y = event.getY() - CENTER_Y; boolean inCenter = java.lang.Math.sqrt(x*x + y*y) <= CENTER_RADIUS; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mTrackingCenter = inCenter; if (inCenter) { mHighlightCenter = true; invalidate(); break; } case MotionEvent.ACTION_MOVE: if (mTrackingCenter) { if (mHighlightCenter != inCenter) { mHighlightCenter = inCenter; invalidate(); } } else { float angle = (float)java.lang.Math.atan2(y, x); // need to turn angle [-PI ... PI] into unit [0....1] float unit = angle/(2*PI); if (unit < 0) { unit += 1; } mCenterPaint.setColor(interpColor(mColors, unit)); invalidate(); } break; case MotionEvent.ACTION_UP: if (mTrackingCenter) { if (inCenter) { if(mListener != null) mListener.colorChanged(mCenterPaint.getColor()); } mTrackingCenter = false; invalidate(); } break; } return true; }這裏開始的時候就先判斷觸發點所在的區域:
boolean inCenter = java.lang.Math.sqrt(x*x + y*y) <= CENTER_RADIUS;這個公式很簡單:就是判斷當前觸摸點的座標是否在半徑爲CENTER_RADIUS的圓中
而後當移動的時候,咱們須要計算觸發點所在的顏色區域:
float angle = (float)java.lang.Math.atan2(y, x); // need to turn angle [-PI ... PI] into unit [0....1] float unit = angle/(2*PI); if (unit < 0) { unit += 1; }這裏首先算出觸發點的到圓形的角度,而後interpColor(mColors, unit)方法算出顏色:
private int interpColor(int colors[], float unit) { if (unit <= 0) { return colors[0]; } if (unit >= 1) { return colors[colors.length - 1]; } /** * 1,2,3,4,5 * 0.6*4=2.4 * i=2,p=0.4 */ float p = unit * (colors.length - 1); int i = (int)p; p -= i; // now p is just the fractional part [0...1) and i is the index int c0 = colors[i]; int c1 = colors[i+1]; int a = ave(Color.alpha(c0), Color.alpha(c1), p); int r = ave(Color.red(c0), Color.red(c1), p); int g = ave(Color.green(c0), Color.green(c1), p); int b = ave(Color.blue(c0), Color.blue(c1), p); return Color.argb(a, r, g, b); }第一個參數是顏色數組,第二個是區域單元。
這個方法很簡單就是用一個區域值unit到顏色數組中找到其對應的色值區域
打個比方:在高中的時候咱們學習函數的時候,老是提到定義域和值域
這裏定義域就是unit,值域就是色值區域,這樣理解就簡單了
不過這裏用到了一個方法:Color.red/green/blue,獲取一個色值的三基色的值,而後在計算比例:
private int ave(int s, int d, float p) { return s + java.lang.Math.round(p * (d - s)); }這個方法也很簡單,算出兩個值之間的指定比例的值公式爲:
value = p1 + p(p2-p1)
其中p1是區域的開始值,p是比例,p2是區域的結束值
p的值爲[0,1],當p=0時,value=p1就是開始值,當p=1時,value=p2就是結束值
這樣原理就解釋清楚了吧,若是仍是不清楚的同窗,那我也沒辦法了。
效果:
這個例子和上面的長條漸變的SeekBar差很少的,只是此次是圓形的,效果圖以下:
那麼咱們就知道此次應該使用掃描梯度的渲染器:SweepGradient
這裏面咱們繪製圓環的時候,使用的是繪製兩個同心圓的方式的。
來看一下代碼:
onDraw方法:
@Override protected void onDraw(Canvas canvas) { float section = progress/maxProgress; /*dx = getXFromAngle(); dy = getYFromAngle();*/ /*LinearGradient shader = new LinearGradient(0,0,dx,dy, colors,null, Shader.TileMode.CLAMP); circleColor.setShader(shader);*/ SweepGradient shader = new SweepGradient(cx, cy, SECTION_COLORS, null); Matrix matrix = new Matrix(); matrix.setRotate(-90,cx,cy); shader.setLocalMatrix(matrix); circleColor.setShader(shader); canvas.drawCircle(cx, cy, outerRadius, circleRing); canvas.drawArc(rect, startAngle, angle, true, circleColor); canvas.drawCircle(cx, cy, innerRadius-40, innerColor); /*if(SHOW_SEEKBAR){ drawMarkerAtProgress(canvas); }*/ super.onDraw(canvas); }這裏,咱們同樣的有兩個變量:progress表示當前進度條的值,maxProgress表示進度條最大的值。而後定義一個梯度渲染器。而後開始繪製圓環,首先繪製外圍的圓形,而後繼續繪製一個扇形(用渲染器),最後在繪製內部的圓形。
這裏在操做的過程當中須要注意的是,將SweepGradient渲染器逆時針旋轉90度,否則結果是不正確的。
在來看一下onTouchEvent方法:
@Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); boolean up = false; this.getParent().requestDisallowInterceptTouchEvent(true); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: moved(x, y, up); break; case MotionEvent.ACTION_MOVE: moved(x, y, up); break; case MotionEvent.ACTION_UP: up = true; moved(x, y, up); break; } return true; } /** * Moved. * * @param x * the x * @param y * the y * @param up * the up */ private void moved(float x, float y, boolean up) { float distance = (float) Math.sqrt(Math.pow((x - cx), 2) + Math.pow((y - cy), 2)); if (distance < outerRadius + adjustmentFactor && distance > innerRadius - adjustmentFactor && !up) { IS_PRESSED = true; markPointX = (float) (cx + outerRadius * Math.cos(Math.atan2(x - cx, cy - y) - (Math.PI /2))); markPointY = (float) (cy + outerRadius * Math.sin(Math.atan2(x - cx, cy - y) - (Math.PI /2))); float degrees = (float) ((float) ((Math.toDegrees(Math.atan2(x - cx, cy - y)) + 360.0)) % 360.0); // and to make it count 0-360 if (degrees < 0) { degrees += 2 * Math.PI; } setAngle(Math.round(degrees)); invalidate(); } else { IS_PRESSED = false; invalidate(); } }這裏主要是計算扇形的角度。
還須要在代碼中設置一下當前進度和最大進度值:
CircularSeekBar view = (CircularSeekBar)findViewById(R.id.progressview); view.setMaxProgress(100); view.setProgress(20);
運行結果:
這裏面主要用到了Path對象,由於咱們繪製折線圖,用Path是最好不過了,而後在用LinearGradient渲染器,這裏須要注意的是,Path繪製的折線圖必定要是封閉的,否則這個渲染是沒有任何效果的,原理圖以下:
咱們繪製一個封閉的Path折線圖(至關於多邊形)
代碼以下:
onDraw方法
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawHLines(canvas); drawVLines(canvas); drawCurveLines(canvas); }
private void drawHLines(Canvas canvas) { for(int i = 0;i<=COLUM_VERTIAL_NUM;i++){ canvas.drawLine(0, mColumWidth *i, mColumWidth * (mDays), mColumWidth *i, mPaint); } }
private void drawVLines(Canvas canvas) { for (int i = 0; i < mDays; i++) { canvas.drawLine(mColumWidth * (i + 0.5f), 0, mColumWidth * (i + 0.5f), mColumWidth * COLUM_VERTIAL_NUM, mPaint); } }
private void drawCurveLines(Canvas canvas) { Path path = new Path(); for (int i = 0; i < mDays; i++) { List<Float> floats = CommonUtil.getRatioList(allDayTotalValueList); int flow; float ratio; try{ flow = allDayTotalValueList.get(i); ratio = floats.get(i); }catch (Exception e){ flow = 0; ratio = 0; } if (i == 0) { path.moveTo(mColumWidth * (i + 0.5f), getYByFlow((int)(ratio * mColumWidth * COLUM_VERTIAL_NUM))); } path.lineTo(mColumWidth * (i + 0.5f), getYByFlow((int)(ratio * mColumWidth * COLUM_VERTIAL_NUM))); canvas.drawCircle(mColumWidth * (i + 0.5f), getYByFlow((int)(ratio * mColumWidth * COLUM_VERTIAL_NUM)), RADIUS, mCirclePaint); if(CommonUtil.DAY_OF_MONTH() == (i+1)){ canvas.drawCircle(mColumWidth * (i + 0.5f), getYByFlow(0), RADIUS, mTodayCirclePaint); } drawFlowText(canvas,flow,ratio,i); drawDateText(canvas,i); } canvas.drawPath(path, mCurvePaint); drawGraintColor(canvas, path); }
private void drawFlowText(Canvas canvas,int value,float ratio,int position){ String text = CommonUtil.formatSize(value); float textHeight = mDatePaint.descent() - mDatePaint.ascent(); float textOffset = (textHeight / 2) - mDatePaint.descent(); float width = mDatePaint.measureText(text); canvas.drawText(text, mColumWidth * (position + 0.5f) - width / 2, getYByFlow((int)(ratio * mColumWidth * COLUM_VERTIAL_NUM)) - textHeight/2 + textOffset, mDatePaint); } private void drawDateText(Canvas canvas,int position){ String text = ""; if(CommonUtil.DAY_OF_MONTH() == (position+1)){ text = "今日"; }else{ text = mCurrentMonth+"/" +(position+1); } float textHeight = mDatePaint.descent() - mDatePaint.ascent(); float textOffset = (textHeight / 2) - mDatePaint.descent(); float width = mDatePaint.measureText(text); canvas.drawText(text, mColumWidth * (position + 0.5f) - width / 2, mColumWidth * COLUM_VERTIAL_NUM + textHeight/2+ textOffset, mDatePaint); } private void drawGraintColor(Canvas canvas,Path path){ path.lineTo(mColumWidth * (mDays - 0.5f), mColumWidth * COLUM_VERTIAL_NUM); path.lineTo(mColumWidth * 0.5f, mColumWidth * COLUM_VERTIAL_NUM); canvas.drawPath(path,mGraintPaint); }
layout.xml中:
<HorizontalScrollView android:layout_width="match_parent" android:paddingTop="5dp" android:paddingBottom="5dp" android:layout_height="wrap_content"> <com.example.drawpathdemo.CurveView android:id="@+id/curve_view" android:layout_width="match_parent" android:layout_height="match_parent" /> </HorizontalScrollView>
這裏使用水平的滾動視圖
CurveView curve = (CurveView)findViewById(R.id.curve_view); List<Integer> list = new ArrayList<Integer>(31); for(int i=0;i<31;i++){ list.add(new Random().nextInt(1000000)); } curve.setAllDayTotalValue(list);這裏初始化一個31天的天天消耗的流量大小值,採用隨機數的。在CommonUtil工具類中有數值的格式轉化。
在這31大小的list中找到最大值,而後和CurveView中折線圖的大小進行比例計算,否則折線圖的最高點會超出CurveView。
運行結果:
效果沒有360的好,可是差很少了,在詳細的調一下就能夠了。哈哈~~
到這裏咱們就介紹完了Android中自定義視圖的基礎知識了。這篇文章很長,原本想分開講解的,最後發現不少知識都是串聯在一塊兒的,因此就在一篇文章中進行講解了,其實沒什麼難度的。
在Android中咱們繪圖的時候其實有兩種方式,一種是咱們這篇文章主要講的繼承View以後,拿到畫布對象Canvas而後進行繪製,這種方式主要用在自定義視圖中,並且這種方式是在UI線程中進行的,那麼除了這種方式,咱們還可使用SurfaceView進行繪圖,這種方式效率高點,並且能夠在子線程中進行的,通常用於遊戲開發中,示例代碼:
//內部類的內部類 class MyThread implements Runnable{ @Override public void run() { Canvas canvas = holder.lockCanvas(null);//獲取畫布 Paint mPaint = new Paint(); mPaint.setColor(Color.BLUE); canvas.drawRect(new RectF(40,60,80,80), mPaint); holder.unlockCanvasAndPost(canvas);//解鎖畫布,提交畫好的圖像 } }這裏首先須要獲取畫布,其實就是給畫布加一個鎖,當畫完了以後,就釋放鎖。因此能夠在多線程中進行繪製。
這一篇文章是繪圖+自定義視圖的基礎,後續還會在開發過程當中遇到一些高級的自定義視圖,那時候我還會一一進行講解的。
(PS:這篇文章寫了我整整一個週末,累死了,因此必定要記得點贊呀~~)