掘金logo
掘金官方使用的logo從網(wǎng)頁上看到是個(gè)svg文件,官方掘金logo,點(diǎn)擊去可以看到logo和文字都是些path標(biāo)簽。

svg的原理也是通過路徑繪制出來的圖形,和Flutter路徑繪制原理相似,同樣可以繪制出任何平面圖形,了解svg相關(guān)知識,可以看看張老師的svg解析:【Flutter 繪制番外】svg 文件與繪制 (上)。
為了鞏固下Flutter繪制的相關(guān)知識,今天我們就用Flutter路徑從頭開始制作封裝一個(gè)掘金的logo組件, 掘金的logo看起來很簡單,但是其中還是涉及到了很多繪制以及三角函數(shù)的知識的。
繪制菱形
首先我們可以看到掘金最上面是一個(gè)菱形,通過量角器測得掘金logo的角度大約為100°,那么菱形的上下角度也就為100°。

為了封裝的通用性,我們設(shè)菱形的邊長為side,菱形上方一半的角度為angle= 50°,根據(jù)三角函數(shù)就可以得到菱形的四個(gè)坐標(biāo)點(diǎn),通過path路徑進(jìn)行鏈接。
代碼:
double angle = pi / 18* 5;
// 菱形邊長
double side = 50;
Paint paint = Paint()
..style = PaintingStyle.fill
..isAntiAlias = true
..strokeJoin= StrokeJoin.miter
..color = Color(0xff1E80FF).withOpacity(0.7);
// 頂部菱形
Path path = Path();
path.moveTo(-side * sin(angle), 0);
path.lineTo(0, -side * cos(angle));
path.lineTo(side * sin(angle), 0);
path.lineTo(0, side * cos(angle));
path.close();
canvas.drawPath(path, paint);
就可以得到以下效果,設(shè)置透明度為了下面計(jì)算效果可以看的更加直觀。

繪制折線
接下來繪制菱形下方的折線,折線我們使用非填充畫筆來實(shí)現(xiàn),首先掘金的logo整體關(guān)于y軸對稱,角度一致,關(guān)鍵要計(jì)算折線之間與菱形的距離,首先我們知道菱形四個(gè)點(diǎn)的坐標(biāo),那么最下面的坐標(biāo)就是(0, side * cos(angle));, 根據(jù)掘金logo的設(shè)計(jì),折線的寬度大約為菱形邊長的0.7倍,所以這里我們暫設(shè)畫筆的寬度為double paintWidth = side * 0.7;,y軸折線中心點(diǎn)距離菱形底部的距離為下圖紅線部分,這個(gè)距離大約為菱形邊長的1.5倍。
| 左 | 左右連接 |
|---|---|
![]() |
![]() |
代碼:
Path path2 = Path();
// 原點(diǎn)距離下方折線中心y軸距離
double h1 = side * cos(angle) + side * 1.5;
path2.moveTo(-h1 * tan(angle), 0);
path2.lineTo(0, h1);
path2.lineTo(h1 * tan(angle), 0);
canvas.drawPath(path2, paint);
接下來繪制最下面的折線,這里為了讓兩條折線之間距離一致,我們需要計(jì)算出下圖c點(diǎn)坐標(biāo),下圖中b是中點(diǎn),那么ab=bc,求出ab的長度也就知道c點(diǎn)的坐標(biāo)了,過a點(diǎn)做bd垂直線交點(diǎn)設(shè)為g,那么已知ag等于線寬的1/2,角abg= angle°;,就能得出ab的長度 ab = paintWidth / 2 / sin(angle);,那么也就得到c點(diǎn)坐標(biāo)=(0, h1+ab);

那么折線之間的距離也就可以算出來了。
代碼:
Path path3 = Path();
double h2 = h1 +
(paintWidth / 2 / sin(angle) + side * 1.5);
path3.moveTo(-h2 * tan(angle), 0);
path3.lineTo(0, h2);
path3.lineTo(h2 * tan(angle), 0);
效果:

裁剪
上方大致畫出來了效果,接下來需要進(jìn)行對畫布進(jìn)行裁剪成以下陰影效果,主要就是計(jì)算b點(diǎn)和d點(diǎn)的坐標(biāo),涉及到兩條直線的交點(diǎn)和三角函數(shù)。

首先a點(diǎn)的值可以通過兩條相交直線求交點(diǎn)公式可以得出,然后過b點(diǎn)做紅線的中垂線先計(jì)算出ab的值,已知bd = paintWidth / 2,角bad = 180°-100°=80°,那么就可以得出ab = paintWidth / 2 / sin(pi - angle * 2),然后再分別過a點(diǎn)和b點(diǎn)做垂直三角形,就能得出b點(diǎn)坐標(biāo)為(a.x - paintWidth / 2 / sin(pi - angle * 2) * sin(angle), a.y + paintWidth / 2 / sin(pi - angle * 2) * cos(angle));

同理d點(diǎn)坐標(biāo)也可得出。
計(jì)算代碼:
Point left = toTwoPoint(Point(-side * sin(angle), 0),
Point(0, -side * cos(angle)), Point(-h2 * tan(angle), 0), Point(0, h2));
Point right = toTwoPoint(Point(side * sin(angle), 0),
Point(0, -side * cos(angle)), Point(h2 * tan(angle), 0), Point(0, h2));
Path pathBg = Path();
pathBg.moveTo(0, -side * cos(angle));
pathBg.lineTo(
left.x.toDouble() - paintWidth / 2 / sin(pi - angle * 2) * sin(angle),
left.y.toDouble() + paintWidth / 2 / sin(pi - angle * 2) * cos(angle));
pathBg.lineTo(left.x.toDouble(), h2 + (paintWidth / 2 / sin(pi - angle * 2) / sin(angle)));
pathBg.lineTo(right.x.toDouble(), h2 + (paintWidth / 2 / sin(pi - angle * 2)/ sin(angle)));
pathBg.lineTo(right.x.toDouble() + paintWidth / 2 / sin(pi - angle * 2) * sin(angle),
right.y.toDouble() + paintWidth / 2 * cos(angle));
pathBg.close();
// 通過裁剪畫布得到最終效果
canvas.clipPath(pathBg);
效果:
| 原始 | 移到畫布中間 |
|---|---|
![]() |
![]() |
上面我們是通過菱形邊長去求的各個(gè)坐標(biāo)點(diǎn),現(xiàn)在我們?yōu)榱耸褂梅奖阈枨笫且阎M件寬高,求菱形的邊長,這樣組件使用起來才會比較方便精準(zhǔn)的控制組件大小,這里就是一些的繁瑣的倒推計(jì)算,假設(shè)我們的高度是我們設(shè)定的height,那么寬度其實(shí)是也就確定了,因?yàn)榻嵌纫坏┐_立,寬度自然也就確定了,所以這里我們向外暴露兩個(gè)屬性,一個(gè)組件高度,一個(gè)菱形角度即可。
完整源碼:
/// 掘金logo組件
class JueJinLogo extends StatelessWidget {
final double height; // 組件高度
final double angle; // 菱形上下角度1/2
const JueJinLogo({Key? key, this.height = 140, this.angle = pi / 18 * 5})
: super(key: key);
@override
Widget build(BuildContext context) {
double m = 0.7;// 折線線寬相對菱形邊長倍數(shù)
double n = 1.5;// 折線之間線寬相對菱形邊長倍數(shù)
var a = (2 * cos(angle) + m * 0.5 / sin(angle) + 3);
double side = height / (a + m * 0.5 / sin(pi - angle * 2) / sin(angle));
double paintWidth = m * side;
double h2 = side * cos(angle) +
side * n +
(paintWidth / 2 / sin(angle) + side * n);
Point right = PointUtil.toTwoPoint(Point(side * sin(angle), 0),
Point(0, -side * cos(angle)), Point(h2 * tan(angle), 0), Point(0, h2));
double width = (right.x.toDouble() +
paintWidth / 2 / sin(pi - angle * 2) * sin(angle)) *
2;
return CustomPaint(
size: Size(width, height),
painter: _JueJinLogoPaint(side, angle),
);
}
}
class _JueJinLogoPaint extends CustomPainter {
double side;
double angle;
_JueJinLogoPaint(this.side, this.angle);
@override
void paint(Canvas canvas, Size size) {
canvas.translate(size.width / 2, size.height / 2);
double paintWidth = side * 0.7;
Paint paint = Paint()
..strokeWidth = paintWidth
..style = PaintingStyle.fill
..isAntiAlias = true
..strokeJoin = StrokeJoin.miter
..color = Color(0xff1E80FF);
canvas.save();
Path path = Path();
path.moveTo(-side * sin(angle), 0);
path.lineTo(0, -side * cos(angle));
path.lineTo(side * sin(angle), 0);
path.lineTo(0, side * cos(angle));
path.close();
Path path2 = Path();
double h1 = side * cos(angle) + side * 1.5;
path2.moveTo(-h1 * tan(angle), 0);
path2.lineTo(0, h1);
path2.lineTo(h1 * tan(angle), 0);
Path path3 = Path();
double h2 = h1 + (paintWidth / 2 / sin(angle) + side * 1.5);
path3.moveTo(-h2 * tan(angle), 0);
path3.lineTo(0, h2);
path3.lineTo(h2 * tan(angle), 0);
// 平移組件到畫布中心
canvas.translate(
0,
side * cos(angle) -
(h2 + (paintWidth / 2 / sin(angle)) + side * cos(angle)) / 2);
Point left = PointUtil.toTwoPoint(Point(-side * sin(angle), 0),
Point(0, -side * cos(angle)), Point(-h2 * tan(angle), 0), Point(0, h2));
Point right = PointUtil.toTwoPoint(Point(side * sin(angle), 0),
Point(0, -side * cos(angle)), Point(h2 * tan(angle), 0), Point(0, h2));
Path pathBg = Path();
pathBg.moveTo(0, -side * cos(angle));
pathBg.lineTo(
left.x.toDouble() - paintWidth / 2 / sin(pi - angle * 2) * sin(angle),
left.y.toDouble() + paintWidth / 2 / sin(pi - angle * 2) * cos(angle));
pathBg.lineTo(left.x.toDouble(),
h2 + (paintWidth / 2 / sin(pi - angle * 2) / sin(angle)));
pathBg.lineTo(right.x.toDouble(),
h2 + (paintWidth / 2 / sin(pi - angle * 2) / sin(angle)));
pathBg.lineTo(right.x.toDouble() + paintWidth / 2 * sin(angle),
right.y.toDouble() + paintWidth / 2 * cos(angle));
pathBg.close();
// 裁剪畫布
canvas.clipPath(pathBg);
// 繪制菱形以及折線
canvas.drawPath(path, paint);
canvas.drawPath(path2, paint..style = PaintingStyle.stroke);
canvas.drawPath(path3, paint..style = PaintingStyle.stroke);
canvas.restore();
}
@override
bool shouldRepaint(covariant _JueJinLogoPaint oldDelegate) {
return false;
}
}
class PointUtil {
/// 兩點(diǎn)求直線方程
static double towPointKb(Point<double> p1, Point<double> p2,
{bool isK = true}) {
/// 求得兩點(diǎn)斜率
double k = 0;
double b = 0;
// 防止除數(shù) = 0 出現(xiàn)的計(jì)算錯(cuò)誤 a e x軸重合
if (p1.x == p2.x) {
k = (p1.y - p2.y) / (p1.x - p2.x - 1);
} else {
k = (p1.y - p2.y) / (p1.x - p2.x);
}
b = p1.y - k * p1.x;
if (isK)
return k;
else
return b;
}
static Point<double> toTwoPoint(
Point<double> a, Point<double> b, Point<double> m, Point<double> n) {
double k1 = towPointKb(a, b);
double b1 = towPointKb(a, b, isK: false);
double k2 = towPointKb(m, n);
double b2 = towPointKb(m, n, isK: false);
return Point((b2 - b1) / (k1 - k2), (b2 - b1) / (k1 - k2) * k1 + b1);
}
}
使用
使用也是非常的方便,直接設(shè)置組件高度即可。
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: JueJinLogo(
height: 200,
),
);
}
效果:

因?yàn)檫@里暴露了角度,所以也可以自定義角度.
當(dāng)然這里還可以暴露一些顏色、漸變色等一些屬性,就不一一展示了。掌握原理即可。
總結(jié)
通過制作掘金logo這個(gè)組件,又鞏固了Flutter繪制的的相關(guān)知識和一些基礎(chǔ)的三角函數(shù)計(jì)算知識,同時(shí)對封裝組件所需注意的事項(xiàng)也有了加深的理解,那這篇文章就到這里,希望對你在Flutter繪制以及封裝組件方面有所幫助,如有幫助,歡迎點(diǎn)贊,如有疑問,歡迎指正~
作者:老李code
鏈接:https://juejin.cn/post/7165139559875870750



