我正在构建一个小部件,它可以在允许平移+缩放的位置绘制对象,同时还检测点击/单击何时与屏幕上绘制的对象重叠。
问题在于,平移或缩放后,点击会在初始帧本地注册,导致实际光标位置出现显着偏移,并且无法点击原始帧之外的任何内容。
在屏幕截图中,我在初始框架周围只有一个绿色框和一个正在注册光标点击的红点。请注意,在缩放之前,一切都按预期进行,并且检测到圆圈上的点击(您可以在终端输出中看到“点击节点”)。
缩放后,点击位置看起来位于原始(缩小)帧的本地位置,完全偏离光标,并且光标下方的圆圈不会被检测为点击。事实上,它永远不会被击中,因为它位于代表原始并条机的绿色框之外。
import 'package:flutter/material.dart';
import 'dart:math';
class GraphWidget extends StatefulWidget {
@override
_GraphWidgetState createState() => _GraphWidgetState();
}
class _GraphWidgetState extends State<GraphWidget> {
List<Offset> nodes = [];
EdgeInsets boundaryMargin = EdgeInsets.all(20.0);
Offset? tapPosition;
@override
void initState() {
super.initState();
_generateRandomNodes();
}
void _generateRandomNodes() {
final random = Random();
nodes = List.generate(10, (index) {
return Offset(
random.nextDouble() * 2000,
random.nextDouble() * 2000,
);
});
setState(() {
_calculateBoundaryMargin();
});
}
void _calculateBoundaryMargin() {
if (nodes.isEmpty) {
return;
}
double minX = nodes.map((node) => node.dx).reduce((a, b) => a < b ? a : b);
double maxX = nodes.map((node) => node.dx).reduce((a, b) => a > b ? a : b);
double minY = nodes.map((node) => node.dy).reduce((a, b) => a < b ? a : b);
double maxY = nodes.map((node) => node.dy).reduce((a, b) => a > b ? a : b);
double marginX = (maxX - minX) / 2;
double marginY = (maxY - minY) / 2;
setState(() {
boundaryMargin = EdgeInsets.only(
left: -minX + marginX,
right: maxX + marginX,
top: -minY + marginY,
bottom: maxY + marginY,
);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Graph'),
),
body: LayoutBuilder(
builder: (context, constraints) {
return GestureDetector(
onTapUp: (details) {
final RenderBox box = context.findRenderObject() as RenderBox;
final Offset localOffset = box.globalToLocal(details.globalPosition);
setState(() {
tapPosition = localOffset;
});
_onCanvasTap(localOffset);
},
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.red),
),
child: InteractiveViewer(
clipBehavior: Clip.none,
boundaryMargin: boundaryMargin,
minScale: 0.1,
maxScale: 4.0,
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.green),
),
width: constraints.maxWidth * 2,
height: constraints.maxHeight * 2,
child: CustomPaint(
size: Size(constraints.maxWidth * 2, constraints.maxHeight * 2),
painter: GraphPainter(
nodes,
tapPosition: tapPosition,
),
),
),
),
),
);
},
),
);
}
void _onCanvasTap(Offset position) {
print(position);
for (final node in nodes) {
if ((position - node).distance <= 20) {
print('Node tapped at $node');
break;
}
}
}
}
class GraphPainter extends CustomPainter {
final List<Offset> nodes;
final Offset? tapPosition;
GraphPainter(this.nodes, {this.tapPosition});
@override
void paint(Canvas canvas, Size size) {
final nodePaint = Paint()
..color = Colors.green
..style = PaintingStyle.fill;
for (final node in nodes) {
canvas.drawCircle(node, 100, nodePaint);
}
if (tapPosition != null) {
final tapPaint = Paint()
..color = Colors.red
..style = PaintingStyle.fill;
canvas.drawCircle(tapPosition!, 15, tapPaint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
void main() {
runApp(MaterialApp(
home: GraphWidget(),
));
}
感谢 pskink 评论我需要的工具,那就是
TransformationController.toScene(tapPosition)
。
这里是更新的代码,即使在平移/缩放后也可以注册对圆圈的点击。相关更改是添加
_transformationController
并使用其 toScene
方法的行。
import 'package:flutter/material.dart';
import 'dart:math';
class GraphWidget extends StatefulWidget {
@override
_GraphWidgetState createState() => _GraphWidgetState();
}
class _GraphWidgetState extends State<GraphWidget> {
List<Offset> nodes = [];
EdgeInsets boundaryMargin = EdgeInsets.all(20.0);
Offset? tapPosition;
final TransformationController _transformationController = TransformationController();
@override
void initState() {
super.initState();
_generateRandomNodes();
}
void _generateRandomNodes() {
final random = Random();
nodes = List.generate(10, (index) {
return Offset(
random.nextDouble() * 2000,
random.nextDouble() * 2000,
);
});
setState(() {
_calculateBoundaryMargin();
});
}
void _calculateBoundaryMargin() {
if (nodes.isEmpty) {
return;
}
double minX = nodes.map((node) => node.dx).reduce((a, b) => a < b ? a : b);
double maxX = nodes.map((node) => node.dx).reduce((a, b) => a > b ? a : b);
double minY = nodes.map((node) => node.dy).reduce((a, b) => a < b ? a : b);
double maxY = nodes.map((node) => node.dy).reduce((a, b) => a > b ? a : b);
double marginX = (maxX - minX) / 2;
double marginY = (maxY - minY) / 2;
setState(() {
boundaryMargin = EdgeInsets.only(
left: -minX + marginX,
right: maxX + marginX,
top: -minY + marginY,
bottom: maxY + marginY,
);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Graph'),
),
body: LayoutBuilder(
builder: (context, constraints) {
return GestureDetector(
onTapUp: (details) {
final RenderBox box = context.findRenderObject() as RenderBox;
final Offset localOffset = box.globalToLocal(details.globalPosition);
setState(() {
tapPosition = localOffset;
});
_onCanvasTap(localOffset);
},
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.red),
),
child: InteractiveViewer(
clipBehavior: Clip.none,
boundaryMargin: boundaryMargin,
transformationController: _transformationController,
minScale: 0.1,
maxScale: 4.0,
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.green),
),
width: constraints.maxWidth * 2,
height: constraints.maxHeight * 2,
child: CustomPaint(
size: Size(constraints.maxWidth * 2, constraints.maxHeight * 2),
painter: GraphPainter(
_transformationController,
nodes,
tapPosition: tapPosition,
),
),
),
),
),
);
},
),
);
}
void _onCanvasTap(Offset position) {
for (final node in nodes) {
if ((_transformationController.toScene(position) - node).distance <= 100) {
print('Node tapped at $node');
break;
}
}
}
}
class GraphPainter extends CustomPainter {
final List<Offset> nodes;
final Offset? tapPosition;
final TransformationController _transformationController;
GraphPainter(this._transformationController, this.nodes, {this.tapPosition});
@override
void paint(Canvas canvas, Size size) {
final nodePaint = Paint()
..color = Colors.green
..style = PaintingStyle.fill;
for (final node in nodes) {
canvas.drawCircle(node, 100, nodePaint);
final textSpan = TextSpan(
text: '${node}\n${_transformationController.toScene(node)}',
style: TextStyle(color: Colors.white, fontSize: 10),
);
final textPainter = TextPainter(
text: textSpan,
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
node,//Offset(node.x - textPainter.width / 2, node.y - textPainter.height / 2),
);
}
if (tapPosition != null) {
final tapPaint = Paint()
..color = Colors.red
..style = PaintingStyle.fill;
canvas.drawCircle(tapPosition!, 15, tapPaint);
canvas.drawCircle(_transformationController.toScene(tapPosition!), 15, tapPaint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
void main() {
runApp(MaterialApp(
home: GraphWidget(),
));
}