我怎样才能采取任意定位的矩形,并将其置于视图的中心,并仅使用 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(或变换矩阵)上的变换操作来做到这一点。
为了简化问题,我在当前示例中只使用了一个矩形。
我想对矩形做两件事。
- 按比例值缩放它。
- 使矩形在视图中居中(基于在 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);
我有一个可以定位在二维坐标 space 中任意位置的矩形。这个矩形最终将代表由许多点组成的精心设计的路径(地图)的边界。当我绘制路径时,我不希望笔画随地图坐标缩放,所以我打算使用 Path.transform 缩放和平移而不影响笔画。因此,我希望能够仅使用 canvas(或变换矩阵)上的变换操作来做到这一点。
为了简化问题,我在当前示例中只使用了一个矩形。
我想对矩形做两件事。
- 按比例值缩放它。
- 使矩形在视图中居中(基于在 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);