import 'package:flutter/material.dart'; import 'dart:math'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( home: MultiDragGrid(widgetCount: 6), ); } } class MultiDragGrid extends StatefulWidget { final int widgetCount; const MultiDragGrid({super.key, required this.widgetCount}); @override State createState() => _MultiDragGridState(); } class _MultiDragGridState extends State { final double gridSize = 40; final int widgetGridSize = 9; late List positions; Map cellCount = {}; Set overlappingCells = {}; @override void initState() { super.initState(); positions = List.generate( widget.widgetCount, (i) => Offset(0.0 + i * 40.0, 0.0), ); _recalculateCellCounts(); } Offset _snapToGrid(Offset pos) { final double x = (pos.dx / gridSize).round() * gridSize; final double y = (pos.dy / gridSize).round() * gridSize; return Offset(x, y); } List _getGridCells(Offset snappedPos) { return [ for (int dx = 0; dx < widgetGridSize; dx++) for (int dy = 0; dy < widgetGridSize; dy++) Offset(snappedPos.dx + dx * gridSize, snappedPos.dy + dy * gridSize), ]; } void _recalculateCellCounts() { final Map count = {}; for (var pos in positions) { final snapped = _snapToGrid(pos); for (var cell in _getGridCells(snapped)) { count[cell] = (count[cell] ?? 0) + 1; } } overlappingCells = count.entries.where((e) => e.value > 1).map((e) => e.key).toSet(); cellCount = count; } void _updatePosition(int index, Offset newPos) { setState(() { positions[index] = _snapToGrid(newPos); _recalculateCellCounts(); }); } void _updateDraggingPosition(int index, Offset newPos) { setState(() { positions[index] = newPos; _recalculateCellCounts(); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Drag ${widget.widgetCount} Widgets + Overlap Detection')), body: SingleChildScrollView( scrollDirection: Axis.horizontal, child: SingleChildScrollView( scrollDirection: Axis.vertical, child: SizedBox( width: 5000, height: 5000, child: InteractiveViewer( boundaryMargin: const EdgeInsets.all(double.infinity), minScale: 0.2, maxScale: 5.0, child: Stack( clipBehavior: Clip.none, children: [ ...positions.asMap().entries.expand((entry) { final snapped = _snapToGrid(entry.value); return _getGridCells(snapped).map((cellOffset) { final highlight = overlappingCells.contains(cellOffset); return Positioned( left: cellOffset.dx, top: cellOffset.dy, child: Container( width: gridSize, height: gridSize, decoration: BoxDecoration( color: highlight ? Colors.yellow.withOpacity(0.6) : Colors.transparent, border: Border.all(color: Colors.black12), ), ), ); }); }), ...positions.asMap().entries.map((entry) { final index = entry.key; final position = entry.value; return DraggableWidget( index: index, initialPosition: position, gridSize: gridSize, widgetGridSize: widgetGridSize, onDragEnd: (newPos) => _updatePosition(index, newPos), onDragUpdate: (newPos) => _updateDraggingPosition(index, newPos), ); }), ], ), ), ), ), ), ); } } class DraggableWidget extends StatefulWidget { final int index; final Offset initialPosition; final double gridSize; final int widgetGridSize; final void Function(Offset) onDragEnd; final void Function(Offset) onDragUpdate; const DraggableWidget({ required this.index, required this.initialPosition, required this.gridSize, required this.widgetGridSize, required this.onDragEnd, required this.onDragUpdate, super.key, }); @override State createState() => _DraggableWidgetState(); } class _DraggableWidgetState extends State { late Offset position; bool isDragging = false; Offset _snapToGrid(Offset pos) { final double x = (pos.dx / widget.gridSize).round() * widget.gridSize; final double y = (pos.dy / widget.gridSize).round() * widget.gridSize; return Offset(x, y); } @override void initState() { super.initState(); position = _snapToGrid(widget.initialPosition); } @override Widget build(BuildContext context) { return Positioned( left: position.dx, top: position.dy, child: GestureDetector( onPanStart: (_) { setState(() => isDragging = true); }, onPanUpdate: (details) { setState(() { position += details.delta; widget.onDragUpdate(position); }); }, onPanEnd: (details) { final snapped = _snapToGrid(position); widget.onDragEnd(snapped); setState(() { position = snapped; isDragging = false; }); }, child: Opacity( opacity: isDragging ? 0.7 : 1.0, child: CustomPaint( size: Size(widget.gridSize * widget.widgetGridSize, widget.gridSize * widget.widgetGridSize), painter: GridPainter(gridSize: widget.gridSize), child: Container( width: widget.gridSize * widget.widgetGridSize, height: widget.gridSize * widget.widgetGridSize, decoration: BoxDecoration( color: Colors.transparent, border: Border.all(color: Colors.blueAccent, width: 4), ), ), ), ), ), ); } } class GridPainter extends CustomPainter { final double gridSize; GridPainter({required this.gridSize}); @override void paint(Canvas canvas, Size size) { final Paint finePaint = Paint() ..color = Colors.grey[400]! ..strokeWidth = 1; final Paint boldPaint = Paint() ..color = Colors.grey[700]! ..strokeWidth = 2; for (double x = 0; x <= size.width; x += gridSize) { final paint = ((x / gridSize) % 3 == 0) ? boldPaint : finePaint; canvas.drawLine(Offset(x, 0), Offset(x, size.height), paint); } for (double y = 0; y <= size.height; y += gridSize) { final paint = ((y / gridSize) % 3 == 0) ? boldPaint : finePaint; canvas.drawLine(Offset(0, y), Offset(size.width, y), paint); } } @override bool shouldRepaint(CustomPainter oldDelegate) => false; }