187 lines
5.0 KiB
Dart
187 lines
5.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import 'drawing_canvas.dart';
|
|
import 'drawing_controller.dart';
|
|
|
|
/// A drawing board that overlays a child widget with a drawing canvas.
|
|
///
|
|
/// Supports:
|
|
/// - Drawing with normalized coordinates that scale correctly
|
|
/// - Zooming and panning via InteractiveViewer
|
|
/// - Toggle between view-only and drawing modes
|
|
class DrawingBoard extends StatefulWidget {
|
|
/// The widget to display behind the drawing (e.g., PDF page)
|
|
final Widget child;
|
|
|
|
/// Size of the drawing area (should match child size)
|
|
final Size boardSize;
|
|
|
|
/// Controller for managing drawing state
|
|
final DrawingController controller;
|
|
|
|
/// Whether drawing is enabled (false = view only)
|
|
final bool drawingEnabled;
|
|
|
|
/// Minimum zoom scale
|
|
final double minScale;
|
|
|
|
/// Maximum zoom scale
|
|
final double maxScale;
|
|
|
|
/// Alignment of the board within available space
|
|
final Alignment alignment;
|
|
|
|
const DrawingBoard({
|
|
super.key,
|
|
required this.child,
|
|
required this.boardSize,
|
|
required this.controller,
|
|
this.drawingEnabled = true,
|
|
this.minScale = 1.0,
|
|
this.maxScale = 3.0,
|
|
this.alignment = Alignment.topCenter,
|
|
});
|
|
|
|
@override
|
|
State<DrawingBoard> createState() => _DrawingBoardState();
|
|
}
|
|
|
|
class _DrawingBoardState extends State<DrawingBoard> {
|
|
final TransformationController _transformationController =
|
|
TransformationController();
|
|
|
|
/// Tracks whether we're currently in a drawing gesture
|
|
bool _isDrawing = false;
|
|
|
|
/// Tracks the number of active pointers for gesture disambiguation
|
|
int _pointerCount = 0;
|
|
|
|
@override
|
|
void dispose() {
|
|
_transformationController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// The content that will be transformed (zoomed/panned)
|
|
final content = SizedBox(
|
|
width: widget.boardSize.width,
|
|
height: widget.boardSize.height,
|
|
child: Stack(
|
|
children: [
|
|
// Background child (e.g., PDF)
|
|
Positioned.fill(child: widget.child),
|
|
|
|
// Drawing overlay
|
|
Positioned.fill(
|
|
child: widget.drawingEnabled
|
|
? _buildDrawingLayer()
|
|
: DrawingOverlay(
|
|
controller: widget.controller,
|
|
canvasSize: widget.boardSize,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (!widget.drawingEnabled) {
|
|
// View-only mode: just show the content (no zoom/pan here,
|
|
// let parent handle navigation)
|
|
return Align(
|
|
alignment: widget.alignment,
|
|
child: content,
|
|
);
|
|
}
|
|
|
|
// Drawing mode: wrap with InteractiveViewer for zoom/pan
|
|
return Align(
|
|
alignment: widget.alignment,
|
|
child: InteractiveViewer(
|
|
transformationController: _transformationController,
|
|
minScale: widget.minScale,
|
|
maxScale: widget.maxScale,
|
|
boundaryMargin: EdgeInsets.zero,
|
|
constrained: true,
|
|
panEnabled: !_isDrawing,
|
|
scaleEnabled: !_isDrawing,
|
|
child: content,
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Builds the drawing layer with gesture handling.
|
|
///
|
|
/// Uses Listener for drawing and distinguishes between:
|
|
/// - Single finger: Draw
|
|
/// - Two+ fingers: Pan/Zoom (handled by InteractiveViewer)
|
|
Widget _buildDrawingLayer() {
|
|
return Listener(
|
|
onPointerDown: _onPointerDown,
|
|
onPointerMove: _onPointerMove,
|
|
onPointerUp: _onPointerUp,
|
|
onPointerCancel: _onPointerCancel,
|
|
behavior: HitTestBehavior.translucent,
|
|
child: DrawingOverlay(
|
|
controller: widget.controller,
|
|
canvasSize: widget.boardSize,
|
|
),
|
|
);
|
|
}
|
|
|
|
void _onPointerDown(PointerDownEvent event) {
|
|
_pointerCount++;
|
|
|
|
// Only start drawing with single finger touch
|
|
if (_pointerCount == 1) {
|
|
_isDrawing = true;
|
|
final normalized = _toNormalized(event.localPosition);
|
|
widget.controller.startLine(normalized);
|
|
setState(() {}); // Update to disable pan/scale
|
|
} else {
|
|
// Multiple fingers: cancel drawing, enable pan/zoom
|
|
if (_isDrawing) {
|
|
widget.controller.endLine();
|
|
_isDrawing = false;
|
|
setState(() {});
|
|
}
|
|
}
|
|
}
|
|
|
|
void _onPointerMove(PointerMoveEvent event) {
|
|
if (_isDrawing && _pointerCount == 1) {
|
|
final normalized = _toNormalized(event.localPosition);
|
|
widget.controller.addPoint(normalized);
|
|
}
|
|
}
|
|
|
|
void _onPointerUp(PointerUpEvent event) {
|
|
_pointerCount = (_pointerCount - 1).clamp(0, 10);
|
|
|
|
if (_isDrawing) {
|
|
widget.controller.endLine();
|
|
_isDrawing = false;
|
|
setState(() {}); // Re-enable pan/scale
|
|
}
|
|
}
|
|
|
|
void _onPointerCancel(PointerCancelEvent event) {
|
|
_pointerCount = (_pointerCount - 1).clamp(0, 10);
|
|
|
|
if (_isDrawing) {
|
|
widget.controller.endLine();
|
|
_isDrawing = false;
|
|
setState(() {});
|
|
}
|
|
}
|
|
|
|
/// Converts local position to normalized coordinates (0-1).
|
|
Offset _toNormalized(Offset localPosition) {
|
|
return Offset(
|
|
(localPosition.dx / widget.boardSize.width).clamp(0.0, 1.0),
|
|
(localPosition.dy / widget.boardSize.height).clamp(0.0, 1.0),
|
|
);
|
|
}
|
|
}
|