Files
2025-05-26 21:59:42 +02:00

246 lines
7.2 KiB
Dart

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<MultiDragGrid> createState() => _MultiDragGridState();
}
class _MultiDragGridState extends State<MultiDragGrid> {
final double gridSize = 40;
final int widgetGridSize = 9;
late List<Offset> positions;
Map<Offset, int> cellCount = {};
Set<Offset> 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<Offset> _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<Offset, int> 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<DraggableWidget> createState() => _DraggableWidgetState();
}
class _DraggableWidgetState extends State<DraggableWidget> {
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;
}