我怎样才能采取任意定位的矩形,并将其置于视图的中心,并仅使用 Canvas 变换操作对其进行缩放?

How can I take an rect of arbitrary positioning, and center it in the view, and scale it using only Canvas transform operations?

我有一个可以定位在二维坐标 space 中任意位置的矩形。这个矩形最终将代表由许多点组成的精心设计的路径(地图)的边界。当我绘制路径时,我不希望笔画随地图坐标缩放,所以我打算使用 Path.transform 缩放和平移而不影响笔画。因此,我希望能够仅使用 canvas(或变换矩阵)上的变换操作来做到这一点。

为了简化问题,我在当前示例中只使用了一个矩形。

我想对矩形做两件事。

  1. 按比例值缩放它。
  2. 使矩形在视图中居中(基于在 paint 方法中传递的大小)。

我认为我需要在翻译时补偿比例,但我不确定如何。

这是我完整的应用程序代码,我有评论希望澄清我的意图。

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key}) : super(key: key);
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text("Custom Painter Test"),
      ),
      body: Container(
        width: double.infinity,
        height: double.infinity,
        child: CustomPaint(
          painter: MyCustomPainter(),
        ),
      ),
    );
  }
}

class MyCustomPainter extends CustomPainter {
  MyCustomPainter() : super();
  @override
  void paint(Canvas canvas, Size size) {
    var rect = Rect.fromLTRB(-14, -20 , 200.0, 80.0);
    var centerX = size.width / 2;
    var centerY = size.height / 2;
    var centeringXOffset = centerX - rect.center.dx;
    var centeringYOffset = centerY - rect.center.dy;
    var scale = 2.0;

    canvas.save();

    canvas.translate(-rect.center.dx, -rect.center.dy); // translate the rect so it is at 0,0 to scale from center of rect
    canvas.scale(scale);
    canvas.translate(rect.center.dx, rect.center.dy); // translate the rect to original position
    canvas.translate(centeringXOffset, centeringYOffset); // translate to center the rect

    // draw the rect with border
    canvas.drawRect(rect, Paint()..color = Colors.red);
    var innerRect = rect.deflate(3);
    canvas.drawRect(innerRect, Paint()..color = Colors.white.withOpacity(0.8));

    // draw the rect center point
    canvas.drawCircle(Offset(rect.center.dx,rect.center.dy) , 3.0 , Paint()..color = Colors.red);

    // draw the origincal center point of the view
    canvas.restore();
    canvas.drawCircle(Offset(centerX , centerY) , 4.0 , Paint()..color = Colors.black.withOpacity(0.32));
  }

  @override
  bool shouldRepaint(MyCustomPainter oldDelegate) => true;
}

当使用 Matrix4 api 时,您可以应用两种方法:第一种是通用矩阵乘法,因此您可以结合平移、缩放和旋转矩阵或直接矩阵缩放和平移,这可以是有点棘手(正如您在缩放和翻译您发布的代码中的 Canvas 时发现的那样)

下面的代码使用了这两种方法:红色形状使用矩阵乘法,橙色使用直接矩阵缩放和平移(请注意,对于您的特定情况,您也可以使用 rectToRect2() 简化方法)

// your widget code:
  final dragDetails = ValueNotifier(DragUpdateDetails(globalPosition: Offset.zero, primaryDelta: 0.0));

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onVerticalDragUpdate: (d) => dragDetails.value = d,
      child: CustomPaint(
        painter: FooPainter(dragDetails),
        child: Center(child: Text('move your finger up and down', textScaleFactor: 2)),
      ),
    );
  }

// custom painter code    
class FooPainter extends CustomPainter {
  final ValueNotifier<DragUpdateDetails> dragDetails;
  FooPainter(this.dragDetails) : super(repaint: dragDetails);

  double scale = 4.0;
  @override
  void paint(Canvas canvas, Size size) {
    final painterRect = Offset.zero & size;
    canvas.drawRect(painterRect, Paint()..color = Colors.black26);
    final center1 = Offset(size.width / 2, size.height / 3);
    final center2 = Offset(size.width / 2, size.height * 2 / 3);
    canvas.drawPoints(PointMode.lines, [
      Offset(0, center1.dy), Offset(size.width, center1.dy),
      Offset(0, center2.dy), Offset(size.width, center2.dy),
      painterRect.topCenter, painterRect.bottomCenter
    ], Paint()..color = Colors.black38);

    final scaleFactor = pow(2, -dragDetails.value.primaryDelta / 128);
    scale *= scaleFactor;
    final rect = Rect.fromLTWH(14, 20, 20, 28);
    final path = Path()
      ..fillType = PathFillType.evenOdd
      ..addOval(Rect.fromCircle(center: rect.bottomLeft, radius: 8))
      ..addRRect(RRect.fromRectAndRadius(rect, Radius.circular(3)))
      ..addOval(Rect.fromCircle(center: rect.center, radius: 3));
    Matrix4 matrix;
    Path transformedPath;

    // first solution
    final tranlationMatrix = _translate(center1 - rect.center);
    final scaleMatrix = _scale(scale, center1);
    matrix = scaleMatrix * tranlationMatrix;
    transformedPath = path.transform(matrix.storage);
    canvas.drawPath(transformedPath, Paint()..color = Colors.red);
    canvas.drawPath(transformedPath, Paint()..style = PaintingStyle.stroke ..strokeWidth = 4 ..color = Colors.white);

    // second solution
    // you can also use simplified rectToRect2() that "fills" src rect into dst one
    matrix = rectToRect(rect, Rect.fromCenter(center: center2, width: scale * rect.width, height: scale * rect.height));
    transformedPath = path.transform(matrix.storage);
    canvas.drawPath(transformedPath, Paint()..color = Colors.orange);
    canvas.drawPath(transformedPath, Paint()..style = PaintingStyle.stroke ..strokeWidth = 4 ..color = Colors.white);

    canvas.drawPath(path, Paint()..color = Colors.deepPurple);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

Matrix4 _translate(Offset translation) {
  var dx = translation.dx;
  var dy = translation.dy;
  return Matrix4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, dx, dy, 0, 1);
}

Matrix4 _scale(double scale, Offset focalPoint) {
  var dx = (1 - scale) * focalPoint.dx;
  var dy = (1 - scale) * focalPoint.dy;
  return Matrix4(scale, 0, 0, 0, 0, scale, 0, 0, 0, 0, 1, 0, dx, dy, 0, 1);
}

/// Return a scaled and translated [Matrix4] that maps [src] to [dst] for given [fit]
/// aligned by [alignment] within [dst]
///
/// For example, if you have a [CustomPainter] with size 300 x 200 logical pixels and
/// you want to draw an expanded, centered image with size 80 x 100 you can do the following:
/// 
/// ```dart
///  canvas.save();
///  var matrix = sizeToRect(imageSize, Offset.zero & customPainterSize);
///  canvas.transform(matrix.storage);
///  canvas.drawImage(image, Offset.zero, Paint());
///  canvas.restore();
/// ```
/// 
///  and your image will be drawn inside a rect Rect.fromLTRB(70, 0, 230, 200)
Matrix4 sizeToRect(Size src, Rect dst, {BoxFit fit = BoxFit.contain, Alignment alignment = Alignment.center}) {
  FittedSizes fs = applyBoxFit(fit, src, dst.size);
  double scaleX = fs.destination.width / fs.source.width;
  double scaleY = fs.destination.height / fs.source.height;
  Size fittedSrc = Size(src.width * scaleX, src.height * scaleY);
  Rect out = alignment.inscribe(fittedSrc, dst);

  return Matrix4.identity()
    ..translate(out.left, out.top)
    ..scale(scaleX, scaleY);
}

/// Like [sizeToRect] but accepting a [Rect] as [src]
Matrix4 rectToRect(Rect src, Rect dst, {BoxFit fit = BoxFit.contain, Alignment alignment = Alignment.center}) {
  return sizeToRect(src.size, dst, fit: fit, alignment: alignment)
    ..translate(-src.left, -src.top);
}

Matrix4 rectToRect2(Rect src, Rect dst) {
  final scaleX = dst.width / src.width;
  final scaleY = dst.height / src.height;
  return Matrix4.identity()
    ..translate(dst.left, dst.top)
    ..scale(scaleX, scaleY)
    ..translate(-src.left, -src.top);
}

编辑

您可以进一步简化 rectToRect2 方法为:

Matrix4 pointToPoint(double scale, Offset srcFocalPoint, Offset dstFocalPoint) {
  return Matrix4.identity()
    ..translate(dstFocalPoint.dx, dstFocalPoint.dy)
    ..scale(scale)
    ..translate(-srcFocalPoint.dx, -srcFocalPoint.dy);
}

并像这样使用它:

matrix = pointToPoint(scale, rect.center, center2);