詳解利用Flutter中的Canvas繪制有趣的圖形
簡(jiǎn)介
上一篇我們介紹了使用 Flutter 的 Canvas 繪制基本圖形的示例,簡(jiǎn)單的示例沒什么好玩的,今天這一篇我們來點(diǎn)有趣的,我們會(huì)完成如下圖形的繪制:
- 發(fā)現(xiàn)數(shù)學(xué)重復(fù)之美:使用等邊三角形組合成彩虹傘面。
- 繪制彩虹。
- 繪制評(píng)分用的五角星。
通過這一篇,我們可以知道自定義形狀繪制的基本原理,然后可以在這個(gè)基礎(chǔ)上繪制你自己想要繪制的圖形。
等邊三角形構(gòu)建重復(fù)之美
首先我們來繪制等邊三角形,其實(shí)上一篇我們也有繪制等邊三角形,只是那是將三個(gè)頂點(diǎn)手動(dòng)計(jì)算出來的,這一篇我們封裝一個(gè)繪制等邊三角形的通用方法。老規(guī)矩,先定義方法的輸入?yún)?shù),如下所示:
canvas
:Canvas
畫布color
:繪制顏色startVertex
:三角形的第一個(gè)頂點(diǎn)位置,這里我們其他邊都是相對(duì)這個(gè)點(diǎn)旋轉(zhuǎn)的length
:邊長(zhǎng)startAngle
:第一條邊相對(duì)水平方向旋轉(zhuǎn)的夾角,這樣我們可以改變夾角來更改三角形的繪制位置。clockwise
:順時(shí)針繪制,如果是順時(shí)針,則繪制的偏移夾角往順時(shí)針方向開始,否則逆時(shí)針。filled
:是否填充圖形。
void drawEquilateralTriangle( Canvas canvas, { required Color color, required Offset startVertex, required double length, double startAngle = 0, clockwise = true, filled = true, })
等邊三角形基于一個(gè)頂點(diǎn),一條邊和起始角度后就可以計(jì)算其他兩個(gè)頂點(diǎn)的位置,具體推到通過三角函數(shù)就可以了。
具體計(jì)算三角形的三個(gè)頂點(diǎn)的方法如下,這里逆時(shí)針方向和順時(shí)針方向的計(jì)算方式有點(diǎn)不同,需要區(qū)分一下。
static List<Offset> getEquilateralTriangleVertexes( Offset startVertex, double length, {double startAngle = 0, bool clockwise = true}) { double point2X, point2Y, point3X, point3Y; point2X = startVertex.dx + length * cos(startAngle); point2Y = startVertex.dy - length * sin(startAngle); if (clockwise) { point3X = startVertex.dx + length * cos(pi / 3 + startAngle); point3Y = startVertex.dy - length * sin(pi / 3 + startAngle); } else { point3X = startVertex.dx + length * cos(pi / 3 - startAngle); point3Y = startVertex.dy + length * sin(pi / 3 - startAngle); } return [startVertex, Offset(point2X, point2Y), Offset(point3X, point3Y)]; }
有了頂點(diǎn)我們就可以使用 Path 將頂點(diǎn)連起來就完成等邊三角形的繪制了,繪制三角形的實(shí)現(xiàn)方法如下:
void drawEquilateralTriangle( Canvas canvas, { required Color color, required Offset startVertex, required double length, double startAngle = 0, clockwise = true, filled = true, }) { assert(length > 0); Path trianglePath = Path(); List<Offset> vertexes = ShapesUtil.getEquilateralTriangleVertexes( startVertex, length, clockwise: clockwise, startAngle: startAngle, ); trianglePath.moveTo(vertexes[0].dx, vertexes[0].dy); for (int i = 1; i < vertexes.length; i++) { trianglePath.lineTo(vertexes[i].dx, vertexes[i].dy); } trianglePath.close(); Paint paint = Paint(); paint.color = color; if (!filled) { paint.style = PaintingStyle.stroke; } canvas.drawPath(trianglePath, paint); } }
單獨(dú)一個(gè)三角形沒啥意思,我們通過畫6個(gè)等邊三角形,每個(gè)三角形旋轉(zhuǎn)60度,空心繪制看看怎么樣?
一個(gè) 完美的六邊形出來了,再試試12個(gè)怎么樣。
形狀越多,會(huì)越接近圓形,你會(huì)充分發(fā)現(xiàn)對(duì)稱之美。下面是我們用24個(gè)三角形,填充不同顏色后的效果。有點(diǎn)像一把彩虹傘的傘面了,感覺是不是很美?
上面圖形的實(shí)現(xiàn)代碼如下,其中顏色是通過一個(gè)顏色數(shù)組完成的。
int number = 24; for (int i = 0; i < number; ++i) { drawEquilateralTriangle( canvas, color: colors[i], startVertex: Offset(center.width, center.height), length: 120, startAngle: i * 2 * pi / number, clockwise: true, filled: true, ); }
繪制彩虹
有了上面的彩虹傘一樣的啟發(fā),我們決定來繪制彩虹。彩虹其實(shí)比較簡(jiǎn)單,繪制7條不同顏色的弧線即可。這里講一下弧線的繪制約束。如下圖所示,實(shí)際上弧線是通過矩形的內(nèi)接橢圓限制的(這里用正方形,內(nèi)接為圓形示例)。外面的矩形限制了橢圓位置和尺寸,而通過 startAngle
(起始角度)和 sweepAngle
(弧線覆蓋的角度范圍)就能夠確定弧線的起點(diǎn)和終點(diǎn),從而得到一段弧線。注意的是,數(shù)學(xué)里我們是逆時(shí)針角度為正,但是在 Flutter 默認(rèn)是順時(shí)針為正,因此如果你要從逆時(shí)針方向開始角度就要設(shè)置為負(fù)數(shù)。
下面是弧線繪制的示例代碼:
Path path1 = Path(); Rect rect1 = Rect.fromLTWH(startPoint.dx + (width - innerWidth) / 2, startPoint.dy + (width - innerWidth) / 2, innerWidth, innerWidth); path1.arcTo(rect1, -pi / 6, -2 * pi / 3, true); paint.color = colors[i]; canvas.drawPath(path1, paint);
有了這個(gè)基礎(chǔ),我們通過循環(huán) ,繪制7條弧線,保證每條弧線挨著就行。而弧線的線條粗細(xì)可以用畫筆的寬度來搞定,代碼如下。我們這里每條弧線的中心、起始角度和覆蓋角度是一樣的,通過改變不同弧線的正方形邊長(zhǎng)實(shí)現(xiàn)彩虹弧線的位置不同,然后畫筆粗細(xì)保持為每條彩虹的高度的一半就可以保證每條彩虹是挨著的了。
void drawRainbow( Canvas canvas, { required Offset startPoint, required double width, }) { assert(width > 0); var paint = Paint(); double rowHeight = 12; paint.strokeWidth = rowHeight / 2; List<Color> colors = [ Color(0xFFE05100), Color(0xFFF0A060), Color(0xFFE0E000), Color(0xFF10F020), Color(0xFF2080F5), Color(0xFF104FF0), Color(0xFFA040E5), ]; paint.style = PaintingStyle.stroke; for (var i = 0; i < 7; i++) { double innerWidth = width - i * rowHeight; Path path1 = Path(); Rect rect1 = Rect.fromLTWH(startPoint.dx + (width - innerWidth) / 2, startPoint.dy + (width - innerWidth) / 2, innerWidth, innerWidth); path1.arcTo(rect1, -pi / 6, -2 * pi / 3, true); paint.color = colors[i]; canvas.drawPath(path1, paint); } }
最終效果如下圖所示。
繪制五角星
五角星相對(duì)來說會(huì)復(fù)雜一些,主要是要知道通過中心點(diǎn)確定10個(gè)頂點(diǎn)的坐標(biāo),這里就需要利用二維坐標(biāo)的旋轉(zhuǎn)公式了,具體可以查閱相關(guān)資料,結(jié)論是一個(gè)點(diǎn)(x2, y2)圍繞另一個(gè)點(diǎn)(x1, y1)旋轉(zhuǎn)某個(gè)角度(α)后得到的新坐標(biāo)(x, y)計(jì)算方式如下:
x=x1+(x2-x1)*cos(α)-(y2-y1)*sin(α)
y=y1+(y2-y1)*cos(α)+(x2-x1)*sin(α)
有了這個(gè)基礎(chǔ),我們就可以基于五角星的中心點(diǎn),第一個(gè)頂點(diǎn),邊長(zhǎng)(間隔一個(gè)點(diǎn)連線的線段長(zhǎng)度)來通過旋轉(zhuǎn)計(jì)算其他頂點(diǎn)了。其中外面5頂點(diǎn)一組計(jì)算,內(nèi)部5個(gè)頂點(diǎn)一組計(jì)算。最終獲取5個(gè)頂點(diǎn)的代碼如下:
static List<Offset> getStarVertexes(Offset center, double length) { assert(length > 0); // 外接圓半徑計(jì)算(五角星銳角為36度) double radius = length / 2 / cos(18 / 180 * pi); // 內(nèi)部頂點(diǎn)的半徑 double innerRadius = radius / (cos(36 / 180 * pi) + sin(36 / 180 * pi) / sin(18 / 180 * pi)); List<Offset> vertexes = []; Offset outerStartVertex = Offset(center.dx, center.dy - radius); Offset innerStartVertex = Offset( center.dx - innerRadius * sin(36 / 180 * pi), center.dy - innerRadius * cos(36 / 180 * pi), ); vertexes.add(outerStartVertex); vertexes.add(innerStartVertex); // 計(jì)算方式為以第一個(gè)頂點(diǎn)圍繞五角星中心點(diǎn)坐標(biāo)旋轉(zhuǎn)得到 const double rotateAngle = 72 / 180 * pi; for (int i = 1; i < 5; ++i) { vertexes.add(Offset( center.dx + (outerStartVertex.dx - center.dx) * cos(-i * rotateAngle) - (outerStartVertex.dy - center.dy) * sin(-i * rotateAngle), center.dy + (outerStartVertex.dy - center.dy) * cos(-i * rotateAngle) + (outerStartVertex.dx - center.dx) * sin(-i * rotateAngle), )); vertexes.add(Offset( center.dx + (innerStartVertex.dx - center.dx) * cos(-i * rotateAngle) - (innerStartVertex.dy - center.dy) * sin(-i * rotateAngle), center.dy + (innerStartVertex.dy - center.dy) * cos(-i * rotateAngle) + (innerStartVertex.dx - center.dx) * sin(-i * rotateAngle), )); } return vertexes; }
有了頂點(diǎn),繪制方式就和三角形一樣了,將頂點(diǎn)連起來就好了。下面是我們繪制了一個(gè)常見的五星評(píng)分的圖形。
總結(jié)
本篇介紹了基于 Flutter 的 CustomPaint
繪制定制化圖形的示例,可以看到,其實(shí)只要 UI 小姐姐給出的圖形能夠用數(shù)學(xué)表達(dá)式表示出來,都可以用 CustomPaint
的 Canvas
來實(shí)現(xiàn)。
到此這篇關(guān)于詳解利用Flutter中的Canvas繪制有趣的圖形的文章就介紹到這了,更多相關(guān)Flutter Canvas圖形內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android 實(shí)現(xiàn)定時(shí)器的四種方式總結(jié)及實(shí)現(xiàn)實(shí)例
這篇文章主要介紹了Android 實(shí)現(xiàn)定時(shí)器的四種方式總結(jié)及實(shí)現(xiàn)實(shí)例的相關(guān)資料,這里對(duì)定時(shí)器進(jìn)行詳解,并附實(shí)例代碼,需要的朋友可以參考下2016-12-12淺析AndroidStudio3.0最新 Android Profiler分析器(cpu memory network
Android Profiler分為三大模塊: cpu、內(nèi)存 、網(wǎng)絡(luò)。本文給大家介紹AndroidStudio3.0最新 Android Profiler分析器(cpu memory network 分析器)的相關(guān)知識(shí),他們的基本使用方法,在文中都給大家提到,具體內(nèi)容詳情大家通過本文一起學(xué)習(xí)吧2017-12-12android 手機(jī)截取長(zhǎng)屏實(shí)例代碼
本篇文章主要介紹了android 手機(jī)截取長(zhǎng)屏實(shí)例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-06-06Android實(shí)現(xiàn)圖片在屏幕內(nèi)縮放和移動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了Android控制圖片在屏幕內(nèi)縮放和移動(dòng)效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02安卓(android)怎么實(shí)現(xiàn)下拉刷新
這里我們將采取的方案是使用組合View的方式,先自定義一個(gè)布局繼承自LinearLayout,然后在這個(gè)布局中加入下拉頭和ListView這兩個(gè)子元素,并讓這兩個(gè)子元素縱向排列。對(duì)安卓(android)怎么實(shí)現(xiàn)下拉刷新的相關(guān)知識(shí)感興趣的朋友一起學(xué)習(xí)吧2016-04-04Android開發(fā)中獲取View視圖寬與高的常用方法小結(jié)
這篇文章主要介紹了Android開發(fā)中獲取View視圖寬與高的常用方法,結(jié)合實(shí)例形式總結(jié)分析了Android獲取View視圖寬與高的三種常用方法及使用場(chǎng)景,需要的朋友可以參考下2017-10-10Android仿QQ空間動(dòng)態(tài)界面分享功能
這篇文章主要介紹了Android仿QQ空間動(dòng)態(tài)界面分享功能,本文圖文并茂給大家介紹的非常詳細(xì),需要的朋友可以參考下2017-04-04Android自定義控件實(shí)現(xiàn)雷達(dá)圖效果
這篇文章主要為大家詳細(xì)介紹了Android自定義控件實(shí)現(xiàn)雷達(dá)圖效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-09-09Android 中 Fragment 嵌套 Fragment使用存在的bug附完美解決方案
本文從兩個(gè)方面淺析Android 中 Fragment 嵌套 Fragment使用存在的bug問題,原因找到就可以完美的解決了,對(duì)fragment嵌套fragment使用相關(guān)知識(shí)感興趣的朋友一起看看吧2016-08-08