29、Flutter中画布与自定义绘制的深入探索

Flutter中画布与自定义绘制的深入探索

1. 平移与变换

在Flutter里,我们可以借助 Transform 类来对组件进行平移操作。例如,使用 Transform.translate() 构造函数,将 Transform 组件作为要移动组件的父组件:

Transform.translate(
  offset: Offset(100, 300),
  child: RaisedButton(
    child: Text("translated to bottom"),
    onPressed: () {},
  ),
);

也可以使用默认构造函数搭配 Matrix4 来指定平移:

Transform(
  transform: Matrix4.translationValues(100, 300, 0),
  child: RaisedButton(
    child: Text("translated to bottom"),
    onPressed: () {},
  ),
);

当需要对组件应用多个变换时,有两种方式。一种是添加多个 Transform 组件:

Transform.translate(
  offset: Offset(70, 200),
  child: Transform.rotate(
    angle: -45 * (math.pi / 180.0),
    child: Transform.scale(
      scale: 2.0,
      child: RaisedButton(
        child: Text("multiple transformations"),
        onPressed: () {},
      ),
    ),
  ),
);

这种方式虽易读,但会增加组件树的深度。另一种是使用默认的 Transform 构造函数和 Matrix4 对象:

Transform(
  alignment: Alignment.center,
  transform: Matrix4.translationValues(70, 200, 0)
    ..rotateZ(-45 * (math.pi / 180.0))
    ..scale(2.0, 2.0),
  child: RaisedButton(
    child: Text("multiple transformations"),
    onPressed: () {},
  ),
);

这种方式避免了嵌套组件,使组件树更简洁。

2. 自定义绘制与画布

Flutter为开发者提供了丰富的工具来构建应用界面。除了使用各种组件,还能借助 CustomPaint CustomPainter Canvas 这三个类来创建具有独特外观和行为的组件。

2.1 Canvas类

Canvas 可看作是我们进行绘图的空间,能绘制线条、圆形和矩形等形状。不过,Flutter的 Canvas 并非真正的画布,它只是一个用于记录图形操作的接口,这些操作将在下一渲染帧绘制。

2.2 Canvas变换与裁剪

Canvas 上的所有操作都基于一个坐标系,其原点默认由拥有 Canvas CustomPaint 组件定义。 Canvas 的当前变换会影响所有操作,我们可以随时对其进行变换。此外, Canvas 有当前裁剪区域,默认裁剪区域是无限的。

2.3 Canvas常用方法
方法 描述
drawArc() 用于绘制闭合弧线或圆弧段
drawCircle() 用于绘制指定半径的圆
drawImage() 用于在 Canvas 上绘制图像
drawLine() 用于在 Canvas 上绘制线条
drawRect() 用于在 Canvas 上绘制矩形
rotate() 为当前 Canvas 变换添加旋转变换
scale() 为当前 Canvas 变换添加缩放变换
translate() 为当前 Canvas 变换添加平移变换

更多方法和详细信息可查看文档:https://docs.flutter.io/flutter/dart-ui/Canvas-class.html。

2.4 Paint对象

Paint 对象描述了在 Canvas 上绘制时的样式,可定义颜色和笔触宽度等。所有 Canvas 的绘制方法都将 Paint 对象作为参数,且同一个 Paint 实例可在多次绘制调用中复用。

3. CustomPaint组件

在Flutter中,若要手动绘制图形,需使用 CustomPaint 组件。其主要作用是提供一个 Canvas 对象供我们操作,并委托一个 CustomPainter 对象负责在 Canvas 上绘制。

CustomPaint 组件的构造函数如下:

const CustomPaint({
    Key key,
    CustomPainter painter,
    CustomPainter foregroundPainter,
    Size size: Size.zero,
    bool isComplex: false,
    bool willChange: false,
    Widget child
})

各属性说明如下:
- painter :在 Canvas 上绘制内容的绘制器实现。
- foregroundPainter :在子组件绘制后,在 Canvas 上绘制内容的绘制器实现。
- size :若 child 属性不为空,则使用子组件的大小,此值将被忽略;否则,指定绘制所需的大小。
- isComplex willChange :为合成器的光栅缓存提供提示,有助于分析渲染成本。
- child :组件树中位于下方的子组件。

绘制顺序为:先执行 painter 的操作,然后绘制 child ,最后(若有) foregroundPainter 在子组件前面绘制。

4. CustomPainter对象

若要创建自己的绘制逻辑,需继承 CustomPainter 类并重写 paint() shouldRepaint() 两个方法。

4.1 paint方法

paint() 方法是 CustomPainter 的核心,每当组件需要重绘时会被调用:

void paint (
    Canvas canvas,
    Size size
)

该方法接收两个参数: canvas 用于实际绘制, size 定义了绘制的边界。绘制操作应在给定区域内进行。

4.2 shouldRepaint方法

shouldRepaint() 方法对Flutter引擎很重要:

bool shouldRepaint (
    covariant CustomPainter oldDelegate
)

它接收 oldDelegate 参数,代表上一个负责在 CustomPaint 上绘制的委托。若返回 false paint 调用可能会被优化(但不意味着不会调用)。我们应比较新旧委托,判断与绘制相关的数据是否有变化,有变化则返回 true

5. 实际示例 - 饼图组件

下面我们通过一个实际示例,展示如何使用 Canvas CustomPaint 组件创建自定义绘制的组件,这里以饼图和径向图为例。

5.1 定义组件

首先定义 PieChart 组件,它继承自 StatelessWidget

class PieChart extends StatelessWidget {
  final List<int> values;
  final List<Color> colors;
  ...
}

values 列表表示每个扇形的值, colors 列表包含用于绘制每个扇形的颜色。其 build() 方法如下:

@override
Widget build(BuildContext context) {
  return Row(
    children: <Widget>[
      Expanded(
        child: CustomPaint(
          painter: PieChartPainter(
            values,
            colors
          ),
        ),
      ),
    ],
  );
}

由于 CustomPaint 组件需要大小,这里将其放在 Row 组件中,并使用 Expanded 组件填充可用的水平空间。

5.2 定义CustomPainter

定义 PieChartPainter 类,重写 shouldRepaint() paint() 方法。

graph TD;
    A[开始] --> B[定义PieChartPainter类];
    B --> C[重写shouldRepaint方法];
    B --> D[重写paint方法];
    C --> E{判断values或colors是否改变};
    E -- 是 --> F[返回true];
    E -- 否 --> G[返回false];
    D --> H[计算圆心和半径];
    H --> I[创建Rect实例];
    I --> J[计算总数值];
    J --> K[调用_paintCircle方法];
    K --> L[绘制扇形];
    L --> M[结束];

shouldRepaint() 方法:

@override
bool shouldRepaint(PieChartPainter oldDelegate) {
    return !ListEquality().equals(oldDelegate.values, values) ||
     !ListEquality().equals(oldDelegate.colors, colors);
}

paint() 方法:

@override
void paint(Canvas canvas, Size size) {
  var center = Offset(size.width / 2, size.height / 2);
  var radius = (size.width * 0.75) / 2;
  Rect chartRect = Rect.fromCircle(
    center: center,
    radius: radius,
  );
  int total = values.reduce((a, b) => a + b);
  _paintCircle(canvas, total, chartRect);
}

_paintCircle() 方法:

void _paintCircle(Canvas canvas, int total, Rect chartRect) {
  Paint sectionPaint = Paint()..style = PaintingStyle.fill;
  double startAngle = -90;
  for (var i = 0; i < values.length; i++) {
    final value = values[i];
    final color = colors[i];
    double sweepAngle = ((value * 360.0) / total);
    sectionPaint.color = color;
    canvas.drawArc(
      chartRect,
      startAngle * _toRadians,
      sweepAngle * _toRadians,
      true,
      sectionPaint,
    );
    startAngle += sweepAngle;
  }
}

通过以上步骤,我们就完成了饼图组件的绘制。在绘制过程中,我们首先计算圆心和半径,创建矩形区域,然后计算每个扇形的角度,最后使用 drawArc() 方法绘制扇形。同时,要注意将角度值转换为弧度后再传递给 drawArc() 方法。

Flutter中画布与自定义绘制的深入探索

6. 径向图组件

为了进一步展示 CustomPaint 组件的潜力,我们来创建一个径向图组件。

6.1 定义组件

RadialChart 组件与前面定义的 PieChart 组件非常相似,具有相同的参数和基本目标。其 build() 方法如下:

// part of radial_chart.dart RadialChart widget
@override
Widget build(BuildContext context) {
  return Row(
    children: <Widget>[
      Expanded(
        child: CustomPaint(
          painter: RadialChartPainter(
            values,
            colors,
            Theme.of(context).textTheme.display1,
            Directionality.of(context),
          ),
        ),
      ),
    ],
  );
}

PieChart 组件的区别在于, CustomPaint 组件的 painter 属性传递了一个新的 RadialChartPainter 类实例,并且除了 values colors ,还传递了 TextStyle TextDirection 两个额外参数。

6.2 定义CustomPainter

RadialChartPainter 类与 PieChartPainter 类在某些特定部分有所不同。其 paint() 方法如下:

// part of radial_chart.dart RadialChartPainter class
@override
void paint(Canvas canvas, Size size) {
  var center = Offset(size.width / 2, size.height / 2);
  var radius = size.width * 0.75 / 2;
  Rect chartRect = Rect.fromCircle(
    center: center,
    radius: radius,
  );
  int total = values.reduce((a, b) => a + b);
  _paintTotal(canvas, total, chartRect);
  _paintCircle(canvas, total, chartRect);
}

PieChartPainter paint() 方法相比,多了一个 _paintTotal(canvas, total, chartRect) 调用。

_paintCircle() 方法:

// part of radial_chart.dart RadialChartPainter class
void _paintCircle(Canvas canvas, int total, Rect chartRect) {
  Paint sectionPaint = Paint()
    ..style = PaintingStyle.stroke
    ..strokeWidth = 30.0;
  double startAngle = -90;
  for (var i = 0; i < values.length; i++) {
    final value = values[i];
    final color = colors[i];
    double sweepAngle = ((value * 360.0) / total);
    sectionPaint.color = color;

    canvas.drawArc(
      chartRect,
      (startAngle + 2) * _toRadians,
      (sweepAngle - 2)* _toRadians,
      false,
      sectionPaint,
    );
    startAngle += sweepAngle;
  }
}

这里的变化如下:
- 将 sectionPaint 的样式改为 PaintingStyle.stroke ,只绘制形状的轮廓,并设置了 strokeWidth 属性。
- 在将角度值传递给 drawArc 函数之前,对 startAngle 加2°,对 sweepAngle 减2°,使扇形之间留出一点空间,以获得更好的视觉效果。
- 将 useCenter 参数设置为 false ,绘制圆弧段而不是填充的扇形。

_paintTotal() 方法用于在图表中心绘制总数值标签:

void _paintTotal(Canvas canvas, int total, Rect chartRect) {
  final totalPainter = TextPainter(
    maxLines: 1,
    text: TextSpan(
      style: textStyle,
      text: "$total",
    ),
    textDirection: textDirection,
  );
  totalPainter.layout(maxWidth: chartRect.width);
  totalPainter.paint(
    canvas,
    chartRect.center.translate(
      -totalPainter.width / 2.0,
      -totalPainter.height / 2.0,
    ),
  );
}

绘制文本的步骤如下:
1. 实例化一个 TextPainter 对象,定义文本的外观和方向。
2. 调用 layout() 函数,计算文本的视觉位置。
3. 根据文本的大小,将其定位在图表的中心。

7. 总结与拓展

通过以上饼图和径向图的示例,我们可以看到使用 CustomPaint CustomPainter Canvas 类可以创建出具有独特外观和行为的组件。虽然这两个图表看起来很相似,但主要区别在于定义的绘制器。我们可以将这些功能抽象到一个单一的组件中,通过获取所需的图表类型并更改传递给 CustomPaint 组件的绘制器,实现不同类型图表的绘制。

以下是创建自定义绘制组件的一般步骤总结:
1. 定义组件类,继承自 StatelessWidget StatefulWidget ,并确定组件所需的属性。
2. 在组件的 build() 方法中,使用 CustomPaint 组件,并传递自定义的 CustomPainter 实例。
3. 定义 CustomPainter 类,继承自 CustomPainter ,并重写 paint() shouldRepaint() 方法。
4. 在 paint() 方法中,使用 Canvas 的各种绘制方法进行实际绘制。
5. 在 shouldRepaint() 方法中,判断是否需要重绘。

graph LR;
    A[定义组件类] --> B[确定属性];
    B --> C[编写build方法];
    C --> D[使用CustomPaint组件];
    D --> E[传递CustomPainter实例];
    E --> F[定义CustomPainter类];
    F --> G[重写paint方法];
    F --> H[重写shouldRepaint方法];
    G --> I[使用Canvas绘制];
    H --> J{判断是否重绘};
    J -- 是 --> K[重绘];
    J -- 否 --> L[不重绘];

在实际开发中,我们可以根据需求进一步拓展这些功能,例如添加动画效果、交互功能等,以创建更加丰富和吸引人的用户界面。同时,要注意性能优化,合理使用 shouldRepaint() 方法,避免不必要的重绘。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值