新手學自定義View系列(一)之canvas繪制API

簡述:

  1. Android中自定義View開發的遇到的問題?

Android開發中自定義View可以說是常見的技術,不管是新手還是老司機都非常了解。但是目前Android自定義View的現狀是很多人對自定義View都能說得出來一些,但是實際上根據需求開發起來卻比較難。老實說這也是我之前的想法,我也一直在思考這個原因,個人覺得原因是:
第一 對自定義View中的API不熟悉;
第二 對自定義View沒有一個系統認識和深入了解;
第三 對掌握的自定義View的掌握沒有一個分類管理思想;

  1. canvas繪制的API的細節深入的學習能解決什么問題?
    此篇博客canvas的API的學習可以系統掌握canvas.drawxxx()系列的方法
    以及繪制原理。

  2. 本系列博客能給你帶來什么?
    個人認為Android中的自定義View API的學習主要分為幾個方面:
    第一 Canvas繪制系列的API的學習;
    第二 Paint 畫筆系列的API的學習;
    第三 輔助繪制(clipxxx系列方法)canvas的幾何變換(位移、旋轉、錯切)API的學習;
    第四 自定義View的測量、繪制(onMeasure、onSizeChange、onDraw);
    第五 自定義ViewGroup的測量、布局、繪制(onMeasure、onSizeChange、onLayout、onDraw);
    第六 事件分發、事件攔截、滑動沖突以及觸摸反饋的接口的回調設計;


1、canvas繪制顏色

一般用于在繪制之前設置底色,或者在繪制之后為界面設置半透明蒙版
canvas中繪制顏色主要幾種方法:

  • canvas.drawColor(int color)
  • canvas.drawRGB(int r, int g, int b)
  • canvas.drawARGB(int a, int r, int g, int b)
canvas.drawColor(Color.parseColor("#00bfa5"));//int color
canvas.drawRGB(100, 200, 100);//設置red值(0~255),green值(0~255),blue值(0~255)
canvas.drawARGB(100, 100, 200, 100);//設置alpha值(0~255),設置red值(0~255),green值(0~255),blue值(0~255)

2、canvas繪制形狀

  • 繪制圓形:
  • canvas.drawCircle(float dx, float dy, float radius, Paint paint)
mPaint.setStyle(Paint.Style.FILL);//設置填充style為FILL
mPaint.setStyle(Paint.Style.STROKE);//設置填充style為STROKE
canvas.drawCircle(mWidth / 2, mHeight / 2, 200, mPaint);//(dx:圓心橫坐標 dy:圓心縱坐標 radius:半徑 paint 畫筆) 

  • 繪制矩形:
    canvas中繪制矩形主要幾種方法:
    Rect和RectF的細微區別是Rect的繪制的單位類型是int,而RectF的繪制單位類型是float
  • canvas.drawRect(int left, int top, int right, int bottom, Paint paint);
  • canvas.drawRect(Rect rect, Paint paint);
  • canvas.drawRect(RectF rectF, Paint paint);
mPaint.setStyle(Paint.Style.FILL);//設置填充style為FILL
mPaint.setStyle(Paint.Style.STROKE);//設置填充style為STROKE
mPaint.setColor(Color.parseColor("#e91e63"));
canvas.drawRect(mWidth / 2 - 200, 200, mWidth / 2 + 200, 600, mPaint);//left:矩形的左邊離Y軸的距離;top:矩形的頂邊離X軸的距離;right的右邊離Y軸的距離;bottom:矩形的底部邊離X軸的距離

  • 繪制圓角矩形:
    canvas繪制圓角矩形主要有兩個方法:
    但是這兩個方法是等價的,繪制出來的效果都是一樣的。
  • drawRoundRect(RectF rect, float rx, float ry, Paint paint)
  • drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, Paint paint)
int rx = 80;//圓角矩形的圓角所處的橢圓的橫向半徑
int ry = 40;//圓角矩形的圓角所處的橢圓的縱向半徑
int left = mWidth / 2 - 200;//left:矩形的左邊離Y軸的距離;
int top = 200;//top:矩形的頂邊離X軸的距離;
int right = mWidth / 2 + 200;//right的右邊離Y軸的距離;
int bottom = 400;//bottom:矩形的底部邊離X軸的距離

mPaint.setStyle(Paint.Style.FILL);//設置填充style為FILL
mPaint.setStyle(Paint.Style.STROKE);//設置填充style為STROKE
mPaint.setStrokeWidth(5f);//設置stroke線形的寬度
mPaint.setColor(Color.parseColor("#e91e63"));

canvas.drawRoundRect(left, top, right, bottom, rx, ry, mPaint);//繪制圓角矩形

提出問題:圓角矩形繪制的原理是怎樣的?為什么會有rx,ry兩個半徑?

圓角矩形的四個圓角實際上對應著四個橢圓弧的一部分,注意是橢圓弧不是正圓弧。那么rx也就是橢圓弧的橫向半徑,ry是橢圓弧的縱向半徑,正好對應著橢圓的短半徑和長半徑。

這里寫圖片描述

(圖片來源于GcsSloop大神,地址:http://ww3.sinaimg.cn/large/005Xtdi2jw1f2748fjw2bj308c0dwmx8.jpg)
結論:

當rx等于ry時,此時的四個橢圓弧也就變成四個正圓弧,那么每個圓角即為正圓弧的1/4;

當rx等于矩形寬度的一半,ry等于矩形高度的一半時,此時四個圓角所處的橢圓的重合,正好為矩形的內接橢圓;

當rx大于矩形寬度的一半,ry大于矩形高度的一半時,此時四個圓角所處橢圓實際上是無法計算出圓弧的,所以drawRoundRect對大于該數值的參數進行了修改,如果大于rx大于矩形寬度一半,ry大于矩形高度一半的參數均按照一半來處理。

結論論證:
可以針對圓角矩形繪制四個半徑rx,ry橢圓來驗證,若橢圓中有一部分圓弧與之重合,正好就證實我們的原理。

int rx = 80;//圓角矩形的圓角所處的橢圓的橫向半徑
int ry = 40;//圓角矩形的圓角所處的橢圓的縱向半徑
int left = mWidth / 2 - 200;//left:矩形的左邊離Y軸的距離;
int top = 200;//top:矩形的頂邊離X軸的距離;
int right = mWidth / 2 + 200;//right的右邊離Y軸的距離;
int bottom = 400;//bottom:矩形的底部邊離X軸的距離

mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(5f);
mPaint.setColor(Color.parseColor("#e91e63"));

canvas.drawRoundRect(left, top, right, bottom, rx, ry, mPaint);//繪制圓角矩形

canvas.drawOval(left, top, left + 2 * rx, top + 2 * ry, mPaint);//繪制左上角圓角的所處的橢圓
canvas.drawOval(right - 2 * rx, top, right, top + 2 * ry, mPaint);//繪制右上角圓角的所處的橢圓
canvas.drawOval(left, bottom - 2 * ry, left + 2 * rx, bottom, mPaint);//繪制左下角圓角的所處的橢圓
canvas.drawOval(right - 2 * rx, bottom - 2 * ry, right, bottom, mPaint);//繪制右下角圓角的所處的橢圓

當rx等于ry時,此時的四個橢圓弧也就變成四個正圓弧,那么每個圓角即為正圓弧的1/4;

當rx接近矩形寬度的一半,ry接近矩形高度的一半時,此時四個圓角所處的橢圓接近重合

int rx = 195;//矩形寬度的一半為200
int ry = 95;//矩形高度的一半為100
int left = mWidth / 2 - 200;//left:矩形的左邊離Y軸的距離;
int top = 200;//top:矩形的頂邊離X軸的距離;
int right = mWidth / 2 + 200;//right的右邊離Y軸的距離;
int bottom = 400;//bottom:矩形的底部邊離X軸的距離

mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(5f);
mPaint.setColor(Color.parseColor("#e91e63"));

canvas.drawRoundRect(left, top, right, bottom, rx, ry, mPaint);//繪制圓角矩形

canvas.drawOval(left, top, left + 2 * rx, top + 2 * ry, mPaint);//繪制左上角圓角的所處的橢圓
canvas.drawOval(right - 2 * rx, top, right, top + 2 * ry, mPaint);//繪制右上角圓角的所處的橢圓
canvas.drawOval(left, bottom - 2 * ry, left + 2 * rx, bottom, mPaint);//繪制左下角圓角的所處的橢圓
canvas.drawOval(right - 2 * rx, bottom - 2 * ry, right, bottom, mPaint);//繪制右下角圓角的所處的橢圓
image.png

當rx等于矩形寬度的一半,ry等于矩形高度的一半時,此時四個圓角所處的橢圓的重合,正好為矩形的內接橢圓;

image.png

當rx大于矩形寬度的一半,ry大于矩形高度的一半時,此時四個圓角所處橢圓實際上是無法計算出圓弧的,所以drawRoundRect對大于該數值的參數進行了修改,如果大于rx大于矩形寬度一半,ry大于矩形高度一半的參數均按照一半來處理。(為了便于區別把橢圓的顏色繪制為grey)

image.png
  • 繪制點:
    canvas繪制點主要有幾種方法:
  • canvas.drawPoint(float x, float y, Paint paint)//繪制單個點
  • canvas.drawPoints(float[] pts, int offset, int count, Paint paint)//有選擇性繪制多個點
  • canvas.drawPoints(float[] pts, Paint paint)//繪制多個點
mPaint.setColor(Color.parseColor("#dd2c00"));
mPaint.setStrokeWidth(80f);
mPaint.setStrokeCap(Paint.Cap.ROUND);//繪制點Point,canvas.drawPoint默認繪制方形點,設置畫筆的setStrokeCap可以繪制圓點
canvas.drawPoint(mWidth / 2, mHeight / 2, mPaint);


//繪制多個點:drawPoints(float[] opts, Paint paint)
//opts 點的一對(x,y)坐標的數組。(相鄰的兩個數分為一個點的x,y)

mPaint.setColor(Color.parseColor("#304ffe"));
float[] points = {mWidth / 2 - 200, 200, mWidth / 2 + 200, 200, mWidth / 2 - 200, 600, mWidth / 2 + 200, 600};
canvas.drawPoints(points, mPaint);

//有選擇繪制多個點: drawPoints(float[] opts, offset, count, Paint paint)
//opts 點的一對(x,y)坐標的數組。(相鄰的兩個數分為一個點的x,y)
//offset:是相對于對points數組的元素下標做偏移
//count: 設置points數組從offset偏移算起元素的個數
//經過offset和count操作產生新的點的數組

mPaint.setColor(Color.RED);
canvas.drawPoints(points, 4, 4, mPaint);//在前兩個點后畫兩個點
image.png
  • 繪制橢圓:
    canvas繪制橢圓主要有兩種方法:
    但是這兩個方法是等價的,繪制出來的效果都是一樣的。
  • drawOval(RectF oval, Paint paint)
  • drawOval(float left, float top, float right, float bottom, Paint paint)
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(Color.parseColor("#FF9800"));

canvas.drawOval(mWidth / 2 - 400, 200, mWidth / 2 + 400, 600, mPaint);//外接矩形的left,top,right,bottom
image.png
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(5f);
mPaint.setColor(Color.parseColor("#0091EA"));
RectF rectF = new RectF(mWidth / 2 - 200, 0, mWidth / 2 + 200, 800);//外接矩形的left,top,right,bottom

canvas.drawOval(rectF, mPaint);
image.png
  • 繪制線段:
    canvas繪制線段主要有幾種方法:
  • drawLine(float startX, float startY, float stopX, float stopY, Paint paint)
  • drawLines(float[] pts, int offset, int count, Paint paint)
  • drawLines(float[] pts, Paint paint)
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(5f);
mPaint.setColor(Color.GREEN);

canvas.drawLine(mWidth / 2 - 200, 200, mWidth / 2 + 200, 600, mPaint);//startX線的起點X坐標,startY線的起點Y坐標,endX線的終點X坐標,endY線的終點Y坐標
image.png
float[] points2 = {
                mWidth / 2 - 200, 200,
                mWidth / 2 + 200, 600,
                mWidth / 2 + 200, 200,
                mWidth / 2 - 200, 600,
                mWidth / 2, 200,
                mWidth / 2, 600,
                mWidth / 2 - 200, 400,
                mWidth / 2 + 200, 400
        };//points2 點的一對(startX,startY,endX,endY)坐標的數組。(相鄰的四個數為一條線的起點和終點的x,y坐標)
        
canvas.drawLines(points2, mPaint);//繪制多條線段
image.png
float[] points2 = {
                mWidth / 2 - 200, 200,
                mWidth / 2 + 200, 600,
                mWidth / 2 + 200, 200,
                mWidth / 2 - 200, 600,
                mWidth / 2, 200,
                mWidth / 2, 600,
                mWidth / 2 - 200, 400,
                mWidth / 2 + 200, 400
        };//points2 點的一對(startX,startY,endX,endY)坐標的數組。(相鄰的四個數為一條線的起點和終點的x,y坐標)

//offset:原理和drawPonits一樣,是相對于對points2數組的元素下標做偏移
//count: 設置points2數組從offset偏移算起元素的個數
//經過offset和count產生新的點的數組

canvas.drawLines(points2, 4, 8, mPaint);
image.png
  • 繪制弧形或者扇形(是針對所在橢圓來繪制弧形和扇形):
    canvas繪制弧形或者扇形(是針對所在橢圓來繪制弧形和扇形)主要有兩種方法:但是這兩個方法是等價的,繪制出來的效果都是一樣的。
  • drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
  • drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
//canvas.drawArc(left,top,right,bottom,startAngle,sweepAngle,userCenter,paint)
//left,top,right,bottom: 固定出橢圓
//startAngle起始角度(0度為圓心橫坐標向左,正數為順時針,負數為逆時針)
//sweepAngle掃過的角度
//userCenter是否連接圓心(true:連接圓心,畫扇形;false:不連接圓心,畫弧形)

mPaint.setStyle(Paint.Style.STROKE);//設置橢圓填充style為STROKE
RectF rectF1 = new RectF(mWidth / 2 - 400, 200, mWidth / 2 + 400, 600);

mPaint.setStyle(Paint.Style.FILL);//設置扇形填充style為FILL
mPaint.setStyle(Paint.Style.STROKE);//設置扇形填充style為STROKE
canvas.drawOval(rectF1,mPaint);//繪制扇形所處的橢圓
canvas.drawArc(rectF1, 0, 120, true, mPaint);//扇形
image.png
image.png
//canvas.drawArc(left,top,right,bottom,startAngle,sweepAngle,userCenter,paint)
//left,top,right,bottom: 固定出橢圓
//startAngle起始角度(0度為圓心橫坐標向左,正數為順時針,負數為逆時針)
//sweepAngle掃過的角度
//userCenter是否連接圓心(true:連接圓心,畫扇形;false:不連接圓心,畫弧形)

mPaint.setStyle(Paint.Style.STROKE);//設置橢圓填充style為STROKE
RectF rectF1 = new RectF(mWidth / 2 - 400, 200, mWidth / 2 + 400, 600);

mPaint.setStyle(Paint.Style.FILL);//設置扇形填充style為FILL
mPaint.setStyle(Paint.Style.STROKE);//設置弧形填充style為STROKE
canvas.drawOval(rectF1,mPaint);//繪制弧形所處的橢圓(注意:繪制STROKE的弧形的時候不要繪制橢圓,否則會重合不便于觀察結果)
canvas.drawArc(rectF1, 0, 120, false, mPaint);//弧形
image.png
image.png
  • 繪制圖片Bitmap:
    canvas圖片Bitmap主要有幾種方法:
  • drawBitmap(Bitmap bitmap, float left, float top, Paint paint)
  • drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint)
  • drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)
canvas.translate(mWidth / 2, mHeight / 2);//移動坐標原點到View組件的正中間位置,這個操作會很方便我們繪制時算坐標。(涉及到canvas的幾何變換后期會說到)
float[] points3 = {-mWidth / 2, 0, mWidth / 2, 0, 0, -mHeight / 2, 0, mHeight / 2};
canvas.drawLines(points3, mPaint);//繪制出坐標系線
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.icon);
canvas.drawBitmap(bitmap, -bitmap.getWidth() / 2, -bitmap.getHeight() / 2, mPaint);//圖片左上角x,y坐標對應left,top
image.png
  • 繪制文字:
    canvas中涉及到文本的繪制的細節有很多,后期會專門說下關于文本的繪制,這里先了解下。
  • canvas.drawText(char[] text, int index, int count, float x, float y, Paint paint)
  • canvas.drawText(String text, float x, float y, Paint paint)
  • canvas.drawText(String text, int start, int end, float x, float y, Paint paint)
  • canvas.drawText(CharSequence text, int start, int end, float x, float y, Paint paint)
canvas.translate(mWidth / 2, mHeight / 2);//移動坐標原點到View組件的正中間位置
mPaint.setTextSize(50);
mPaint.setColor(Color.GREEN);
mPaint.setFakeBoldText(true);
String text = "今天好像是情人節,單身狗還是擼擼代碼吧";
mPaint.getTextBounds(text, 0, text.length(), mTextBound);//通過傳入mTextBound(Rect對象),將文字的尺寸固定住,作用相當于測量文本尺寸。

canvas.drawText(text, -mTextBound.width() / 2, mTextBound.height() / 2, mPaint);//x,y指的是文本繪制起點坐標,注意文本的繪制起點是第一個字左下角還要向外偏移一點,至于為什么是這樣后期會說到.
image.png
  • 繪制Path類型自定義圖型:
    Path的知識在Canvas中繪制很重要,合理使用Path可以做出很多炫酷的組件以及提高繪制的效率。所以這里先給出個例子,下一期博客將會深入講解canvas繪制中的Path。情人節了,這張圖或許適合你,哈哈哈
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(Color.RED);
mPath.arcTo(200, 200, 400, 400, -225, 225, true);
mPath.arcTo(400, 200, 600, 400, -180, 225, false);
mPath.lineTo(400, 542);

canvas.drawPath(mPath, mPaint);
image.png

結束:
到這里,這期博客就結束了,該博客旨在新手學習沉淀。感謝GcsSloop和扔物線大神對自定義View的博客的無私奉獻。本系列博客均為自己學大神們自定義View總結。

小例子:本來是扔物線大神第一期的直方圖的例子,在其基礎上擴充幾點:第一就是點擊每個直方圖會有監聽回調的觸摸返回,并把當前點擊的直方圖數據信息顯示出來;第二就是在直方圖基礎上繪制了折線圖。
拋出一個思考的問題:對于一個自定義View如何給某個繪制區域設置監聽的點擊事件觸摸回調?

image.png
image.png
image.png

例子源碼下篇博客給出。

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

推薦閱讀更多精彩內容