Flutter【绘制】制作一个掘金Logo组件

掘金logo

掘金官方使用的logo从网页上看到是个svg文件,官方掘金logo,点击去可以看到logo和文字都是些path标签。

svg的原理也是通过路径绘制出来的图形,和Flutter路径绘制原理相似,同样可以绘制出任何平面图形,了解svg相关知识,可以看看张老师的svg解析:【Flutter 绘制番外】svg 文件与绘制 (上)

为了巩固下Flutter绘制的相关知识,今天我们就用Flutter路径从头开始制作封装一个掘金的logo组件, 掘金的logo看起来很简单,但是其中还是涉及到了很多绘制以及三角函数的知识的。

绘制菱形

首先我们可以看到掘金最上面是一个菱形,通过量角器测得掘金logo的角度大约为100°,那么菱形的上下角度也就为100°。

为了封装的通用性,我们设菱形的边长为side,菱形上方一半的角度为angle= 50°,根据三角函数就可以得到菱形的四个坐标点,通过path路径进行链接。
代码:

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);

就可以得到以下效果,设置透明度为了下面计算效果可以看的更加直观。

绘制折线

接下来绘制菱形下方的折线,折线我们使用非填充画笔来实现,首先掘金的logo整体关于y轴对称,角度一致,关键要计算折线之间与菱形的距离,首先我们知道菱形四个点的坐标,那么最下面的坐标就是(0, side * cos(angle));, 根据掘金logo的设计,折线的宽度大约为菱形边长的0.7倍,所以这里我们暂设画笔的宽度为double paintWidth = side * 0.7;,y轴折线中心点距离菱形底部的距离为下图红线部分,这个距离大约为菱形边长的1.5倍

左右连接

代码:

Path path2 = Path();
// 原点距离下方折线中心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);

接下来绘制最下面的折线,这里为了让两条折线之间距离一致,我们需要计算出下图c点坐标,下图中b是中点,那么ab=bc,求出ab的长度也就知道c点的坐标了,过a点做bd垂直线交点设为g,那么已知ag等于线宽的1/2,角abg= angle°;,就能得出ab的长度 ab = paintWidth / 2 / sin(angle);,那么也就得到c点坐标=(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);

效果:

裁剪

上方大致画出来了效果,接下来需要进行对画布进行裁剪成以下阴影效果,主要就是计算b点和d点的坐标,涉及到两条直线的交点和三角函数。

首先a点的值可以通过两条相交直线求交点公式可以得出,然后过b点做红线的中垂线先计算出ab的值,已知bd = paintWidth / 2,角bad = 180°-100°=80°,那么就可以得出ab = paintWidth / 2 / sin(pi - angle * 2),然后再分别过a点和b点做垂直三角形,就能得出b点坐标为(a.x - paintWidth / 2 / sin(pi - angle * 2) * sin(angle), a.y + paintWidth / 2 / sin(pi - angle * 2) * cos(angle));

同理d点坐标也可得出。
计算代码:

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);

效果:

原始 移到画布中间

上面我们是通过菱形边长去求的各个坐标点,现在我们为了使用方便需求是已知组件宽高,求菱形的边长,这样组件使用起来才会比较方便精准的控制组件大小,这里就是一些的繁琐的倒推计算,假设我们的高度是我们设定的height,那么宽度其实是也就确定了,因为角度一旦确立,宽度自然也就确定了,所以这里我们向外暴露两个属性,一个组件高度,一个菱形角度即可。

完整源码:

/// 掘金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;// 折线线宽相对菱形边长倍数
    double n = 1.5;// 折线之间线宽相对菱形边长倍数
    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 {
  /// 两点求直线方程
  static double towPointKb(Point<double> p1, Point<double> p2,
      {bool isK = true}) {
    /// 求得两点斜率
    double k = 0;
    double b = 0;
    // 防止除数 = 0 出现的计算错误 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);
  }
}

使用
使用也是非常的方便,直接设置组件高度即可。

Widget build(BuildContext context) {
  return Container(
    color: Colors.white,
    child: JueJinLogo(
      height: 200,
    ),
  );
}

效果:

因为这里暴露了角度,所以也可以自定义角度.

当然这里还可以暴露一些颜色、渐变色等一些属性,就不一一展示了。掌握原理即可。

总结

通过制作掘金logo这个组件,又巩固了Flutter绘制的的相关知识和一些基础的三角函数计算知识,同时对封装组件所需注意的事项也有了加深的理解,那这篇文章就到这里,希望对你在Flutter绘制以及封装组件方面有所帮助,如有帮助,欢迎点赞,如有疑问,欢迎指正~

作者:老李code
链接:https://juejin.cn/post/7165139559875870750

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容