Custom drawing implementation
This commit is contained in:
186
lib/features/sheet_viewer/drawing/drawing_board.dart
Normal file
186
lib/features/sheet_viewer/drawing/drawing_board.dart
Normal file
@@ -0,0 +1,186 @@
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user