如何使用 Flutter GestureDetector 和 InteractiveViewer 正确处理偏移

问题描述 投票:0回答:1

问题

我正在构建一个小部件,它可以在允许平移+缩放的位置绘制对象,同时还检测点击/单击何时与屏幕上绘制的对象重叠。

问题在于,平移或缩放后,点击会在初始帧本地注册,导致实际光标位置出现显着偏移,并且无法点击原始帧之外的任何内容。

调试截图

缩放前

在屏幕截图中,我在初始框架周围只有一个绿色框和一个正在注册光标点击的红点。请注意,在缩放之前,一切都按预期进行,并且检测到圆圈上的点击(您可以在终端输出中看到“点击节点”)。

Before Zoom (cursor aligned with tap position and circle)

缩放后

缩放后,点击位置看起来位于原始(缩小)帧的本地位置,完全偏离光标,并且光标下方的圆圈不会被检测为点击。事实上,它永远不会被击中,因为它位于代表原始并条机的绿色框之外。

After Zoom (Tap position relative to original frame and offset from cursor, no circle detected)

代码

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(),
  ));
}

flutter dart user-interface frontend
1个回答
0
投票

感谢 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(),
  ));
}

© www.soinside.com 2019 - 2024. All rights reserved.