Flutter小工具之如何使用画布

44 阅读6分钟

小工具是非常棒的!而且Flutter为我们提供了大量的小工具。而且Flutter为我们提供了很多开箱即用的小工具。

然而,有些时候,我们希望对我们在屏幕上绘制的内容有更多的控制。

为此,我们可以用CustomPainter ,直接在画布上绘制。

所以在这篇文章中,我们将看到如何在屏幕上(画)出一张快乐的脸。

由于这个教程不需要任何第三方软件包,我们可以用Dartpad来构建这个教程。

用CustomPainter在屏幕上画快乐的脸

初始设置

我们可以从一个MaterialApp 开始,包含一个Scaffold ,一个外部的白色Container ,和一个内部的黄色Container

import 'package:flutter/material.dart';

final Color darkBlue = Color.fromARGB(255, 18, 32, 47);

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
      home: Scaffold(
        // Outer white container with padding 
        body: Container(
          padding: EdgeInsets.symmetric(horizontal: 40, vertical: 80),
          color: Colors.white,
          // Inner yellow container
          child: Container(
            color: Colors.yellow,
          ),
        ),
      ),
    );
  }
}

如果我们在Dartpad上运行这段代码,我们会得到这样的结果。

带有两个容器的Dartpad预览

接下来,我们可以添加一个 CustomPaintwidget作为内部容器的一个子节点。

Container(
  color: Colors.yellow,
  child: CustomPaint(painter: FaceOutlinePainter()),
),

CustomPaint 需要一个 类型的参数painter CustomPainter的参数,是Flutter APIs中的一个抽象类。

为了绘制东西,我们需要子类CustomPainter

class FaceOutlinePainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // TODO: draw something with canvas
  }

  @override
  bool shouldRepaint(FaceOutlinePainter oldDelegate) => false;
}

我们稍后会看一下这个问题--但首先--我们有一个问题要解决。

如果我们再次运行这个应用程序,有些事情就不对了,因为我们的黄色Container 已经不可见了。

Dartpad预览中缺少黄色的容器

发生这种情况是因为CustomPainter 并不总是能与试图限制其大小的父级小部件友好相处。

为了解决这个问题,我们可以指定父级Container 的宽度和高度。

child: Container(
  width: 300,
  height: 300,
  color: Colors.yellow,
  child: CustomPaint(painter: FaceOutlinePainter()),
),

如果我们再次运行代码,我们会得到。

带有固定尺寸黄色容器的Dartpad预览

这就好些了,但仍然不理想。我们希望容器占据UI窗口的全部可用空间。

如果有一个小部件可以告诉我们父窗口的大小,我们就可以用它来调整widthheight

这个小部件是存在的,它被称为LayoutBuilder 。让我们来使用它。

LayoutBuilder(
  // Inner yellow container
  builder: (_, constraints) => Container(
    width: constraints.widthConstraints().maxWidth,
    height: constraints.heightConstraints().maxHeight,
    color: Colors.yellow,
    child: CustomPaint(painter: FaceOutlinePainter()),
  ),
),

在这里,我们把LayoutBuilder'的最大宽度和高度约束传递给Container 中的相应参数。

如果我们再次运行该代码,我们可以看到容器现在占据了所有的可用空间。

带有可调整大小的黄色容器的Dartpad预览

当我们在Dartpad中调整UI窗口的大小时,这甚至也能发挥作用。

2020年1月28日更新。有人指出,LayoutBuilder ,在这种情况下是没有必要的。相反,把width: double.infinity, height: double.infinity 作为参数传给黄色的Container 就可以了。


我们都很好,我们可以回到我们的FaceOutlinePainter

class FaceOutlinePainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // TODO: draw something with canvas
  }

  @override
  bool shouldRepaint(FaceOutlinePainter oldDelegate) => false;
}

你好,我是一个画家 👨🎨

我们可以在paint() 方法中用Canvas 对象来画东西,我们可以用shouldRepaint() 来指定我们的画师何时应该重画。由于我们的绘图器没有任何可变的状态,在这个例子中,shouldRepaint() 可以返回false

Canvas 是一个大类,有很多方法用于绘制各种图形。

这些方法都有一个共同的参数:一个Paint 对象。

想要一个带有靛蓝笔触的油漆对象,4pt厚?那就来吧。

final paint = Paint()
  ..style = PaintingStyle.stroke
  ..strokeWidth = 4.0
  ..color = Colors.indigo;

要一个红色的填充物吗?很简单。

final paint = Paint()
  ..style = PaintingStyle.fill
  ..color = Colors.red;

一旦我们有了一个Paint 对象,我们就可以用它来画东西。

canvas.drawRect(
  Rect.fromLTWH(20, 40, 100, 100),
  paint,
);

paint() ,我们可以随意调用画布上的绘画方法

尊重边界

边界在Flutter中很重要,就像在现实生活中一样😉。

paint() 方法也给了我们一个Size 参数。

我们的CustomPaint 的边界是一个大小的函数:(0, 0, size.width, size.height)

让我们重新审视一下我们的设置代码。

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        // Outer white container with padding
        body: Container(
          color: Colors.white,
          padding: EdgeInsets.symmetric(horizontal: 40, vertical: 80),
          child: LayoutBuilder(
            // Inner yellow container
            builder: (_, constraints) => Container(
              width: constraints.widthConstraints().maxWidth,
              height: constraints.heightConstraints().maxHeight,
              color: Colors.yellow,
              child: CustomPaint(painter: FaceOutlinePainter()),
            ),
          ),
        ),
      ),
    );
  }
}

FaceOutlinePainter 的大小与黄色Container 的边界相匹配,而黄色 又受到其父Container 的padding的影响。

在这种设置下,paint() 方法的参数size 将相当于屏幕尺寸,减去padding。

因此,如果我们想保持在边界内,我们的绘制坐标应该是正数,并且不超过 size.widthsize.height


让我们把东西放在一起,画一个快乐的脸。

class FaceOutlinePainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // Define a paint object
    final paint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = 4.0
      ..color = Colors.indigo;

    // Left eye
    canvas.drawRRect(
      RRect.fromRectAndRadius(Rect.fromLTWH(20, 40, 100, 100), Radius.circular(20)),
      paint,
    );
    // Right eye
    canvas.drawOval(
      Rect.fromLTWH(size.width - 120, 40, 100, 100),
      paint,
    );
    // Mouth
    final mouth = Path();
    mouth.moveTo(size.width * 0.8, size.height * 0.6);
    mouth.arcToPoint(
      Offset(size.width * 0.2, size.height * 0.6),
      radius: Radius.circular(150),
    );
    mouth.arcToPoint(
      Offset(size.width * 0.8, size.height * 0.6),
      radius: Radius.circular(200),
      clockwise: false,
    );

    canvas.drawPath(mouth, paint);
  }

  @override
  bool shouldRepaint(FaceOutlinePainter oldDelegate) => false;
}

上面的大部分代码设置了一些坐标,相对于画布的大小。绘画是通过调用各种画布绘画方法来完成的。

注意:嘴巴的坐标是size.widthsize.height 的一个函数。这确保了嘴的大小与父部件成正比

这就是最终的结果,在Dartpad中更新。

使用CustomPainter在屏幕上的快乐表情

这里是Dartpad上的完整例子。你可以玩一玩,看看当你调整UI窗口的大小时,绘画器是如何更新的。

总结

让我们对我们所学到的东西做一个总结。

  • 我们可以使用一个CustomPaint widget来做自定义绘画。
  • 这需要一个类型为CustomPainter 的画师对象。
  • 我们可以编写我们自己的CustomPainter 子类,并重写paint()shouldRepaint() 方法。
  • 我们可以使用Canvas 对象来绘制不同的形状。
  • 我们可以使用具有各种填充描边属性的Paint 对象,来配置我们的形状的外观。

最后,我们必须记住,当我们使用一个CustomPainter ,我们的部件层次结构需要一些额外的注意。换句话说,我们需要指定父widthheightContainer 。如果我们想让它根据窗口动态地调整大小,我们可以使用来自LayoutBuilder

值得注意的是,如果我们不使用Scaffold ,就没有必要使用LayoutBuilder 。在这种情况下,如果我们在MaterialApphome 参数中传递一个Container ,那么一切都可以正常工作。

来吧,我的朋友,画一幅艺术作品吧。🎨

总结

我们已经学会了如何在Flutter中绘画,通过在CustomPainter 子类中绘制自定义形状。

当现有的widget API不够用时,您可以用它来定义自定义形状。

你知道还有什么是很酷的吗?

你可以使用Firebase ML Vision摄像头API来做实时的特征检测**(条形码**、人脸标签文本)。并使用画师来实时绘制覆盖图。

买嘿,也许这是一个未来教程的主题。😎

绘画愉快!🙂