Skip to content

Instantly share code, notes, and snippets.

@pskink
Last active July 12, 2024 17:08
Show Gist options
  • Save pskink/468b0ed8859a85f94f0040c552e3a488 to your computer and use it in GitHub Desktop.
Save pskink/468b0ed8859a85f94f0040c552e3a488 to your computer and use it in GitHub Desktop.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
void main() => runApp(MaterialApp(home: Scaffold(body: Foo())));
class Foo extends StatefulWidget {
@override
State<Foo> createState() => _FooState();
}
class _FooState extends State<Foo> {
static const kGrid = 64.0;
static final kNormalizedRect = Rect.fromCircle(center: Offset.zero, radius: 1);
late final container = const Rect.fromLTWH(0, 0, 5, 6);
late final items = [
(const Rect.fromLTWH(0, 0, 2, 2), Colors.indigo, 'indigo'),
(const Rect.fromLTWH(2, 0, 2, 1), Colors.orange.shade800, 'orange'),
(const Rect.fromLTWH(4, 0, 1, 3), Colors.pink, 'pink'),
(const Rect.fromLTWH(0, 2, 3, 1), Colors.deepPurple, 'deep purple'),
(const Rect.fromLTWH(3, 1, 1, 2), Colors.red.shade800, 'red'),
(const Rect.fromLTWH(2, 1, 1, 1), Colors.teal, 'teal'),
].map((r) => Item(initialRect: r.$1, color: r.$2, debugName: r.$3, container: container)).toList();
final sizerData = [
(Alignment.centerLeft, (Offset d) => EdgeInsets.only(left: -d.dx)),
(Alignment.topCenter, (Offset d) => EdgeInsets.only(top: -d.dy)),
(Alignment.centerRight, (Offset d) => EdgeInsets.only(right: d.dx)),
(Alignment.bottomCenter, (Offset d) => EdgeInsets.only(bottom: d.dy)),
];
Item? _activeItem;
bool layoutAll = false;
bool clamp = true;
@override
Widget build(BuildContext context) {
return Center(
child: SingleChildScrollView(
child: SizedBox(
width: container.width * kGrid,
child: Column(
children: [
Container(
color: Colors.green.shade200,
child: const Padding(padding: EdgeInsets.all(6), child: Text('''move or/and resize any item below\nyou can control the item's layout algorithm and clamping inside the grid by using the checkboxes below''')),
),
CheckboxListTile(
value: layoutAll,
onChanged: (v) => setState(() => layoutAll = v!),
title: const Text('layout all items'),
),
CheckboxListTile(
value: clamp,
onChanged: (v) => setState(() => clamp = v!),
title: const Text('clamp items inside container'),
),
SizedBox.fromSize(
size: container.size * kGrid,
child: Stack(
children: [
Positioned.fill(
child: GridPaper(
subdivisions: 1,
interval: kGrid,
color: Colors.black38,
child: GestureDetector(
onTap: () => setState(() => _activeItem = null),
),
),
),
...items.map(_itemBuilder),
],
),
),
],
),
),
),
);
}
Widget _itemBuilder(Item item) {
final isActive = item == _activeItem;
return AnimatedPositioned.fromRect(
key: ObjectKey(item),
duration: Durations.short4,
rect: item.rect * kGrid,
child: Stack(
children: [
// item itself
Positioned.fill(
child: AnimatedContainer(
duration: Durations.medium4,
foregroundDecoration: BoxDecoration(
color: isActive? Colors.grey.withOpacity(0.8) : null,
),
decoration: BoxDecoration(
boxShadow: isActive? const [BoxShadow(blurRadius: 4, offset: Offset(3, 3))] : null,
),
child: GestureDetector(
onTap: () => setState(() {
_restack(item);
_activeItem = isActive? null : item;
}),
onPanStart: (d) => setState(() {
_restack(item);
_activeItem = item;
}),
onPanUpdate: (d) {
if (item.moveBy(d.delta / kGrid, clamp)) {
debugPrint('"${item.debugName}" moved to ${item.location(false)}');
_relayout(container, item, items, layoutAll);
setState(() {});
}
},
onPanEnd: (d) => item.settle(),
child: item.build(isActive),
),
),
),
// four sizers
...sizerData.map((sizerRecord) {
final (alignment, insetBuilder) = sizerRecord;
return ClipRect(
child: Align(
alignment: alignment,
child: SizedBox.fromSize(
size: const Size.square(kGrid / sqrt2),
child: AnimatedSlide(
duration: Durations.medium4,
offset: alignment.withinRect(kNormalizedRect) * (isActive? 0.5 : 1),
curve: Curves.easeOut,
child: DecoratedBox(
decoration: BoxDecoration(
color: item.color.withOpacity(0.5),
border: Border.all(color: Colors.black38, width: 1),
shape: BoxShape.circle,
),
child: GestureDetector(
onPanUpdate: (d) {
if(item.inflateBy(insetBuilder(d.delta / kGrid), clamp)) {
debugPrint('"${item.debugName}" resized to ${item.location(true)}');
_relayout(container, item, items, layoutAll);
setState(() {});
}
},
onPanEnd: (d) => item.settle(),
),
),
),
),
),
);
}),
],
),
);
}
_relayout(Rect container, Item fixedItem, List<Item> items, bool layoutAll) {
final fixedItemRect = fixedItem.rect;
final remainingItems = items.where((i) => i != fixedItem).toList();
final itemsToLayout = layoutAll? remainingItems : remainingItems.where((i) => i._rect.overlaps(fixedItemRect));
final itemsNotToLayout = layoutAll? <Item>[] : remainingItems.where((i) => !i._rect.overlaps(fixedItemRect));
int byLongestSide(Item a, Item b) => -a._rect.longestSide.compareTo(b._rect.longestSide);
// int byDiagonal(Item a, Item b) => -a._rect.size.bottomRight(Offset.zero).distance.compareTo(b._rect.size.bottomRight(Offset.zero).distance);
// int byHeight(Item a, Item b) => -a._rect.height.compareTo(b._rect.height);
final sortedItems = itemsToLayout.sorted(byLongestSide);
// debugPrint('sortedItems: ${sortedItems.map((item) => item.debugName)}}');
final placedItems = <Item, Rect>{
fixedItem: fixedItemRect,
for (final item in itemsNotToLayout)
item: item._rect,
};
itemLoop:
for (final item in sortedItems) {
for (double t = container.top; t < container.bottom - item._rect.height + 1; t++) {
for (double l = container.left; l < container.right - item._rect.width + 1; l++) {
final r = Rect.fromLTWH(l, t, item._rect.width, item._rect.height);
final goodPlaceToLay = placedItems.values.every((i) => !i.overlaps(r));
if (goodPlaceToLay) {
// debugPrint('good place to lay for ${item.debugName} is $r');
item._rect = r;
placedItems[item] = r;
continue itemLoop;
}
}
}
debugPrint('no space for ${item.debugName}');
}
}
void _restack(Item item) {
items
..remove(item)
..add(item);
debugPrint('new order: ${items.map((item) => item.debugName)}');
}
}
extension RectUtilExtension on Rect {
Rect snap(double grid) {
final l = (left / grid).roundToDouble() * grid;
final t = (top / grid).roundToDouble() * grid;
final r = (right / grid).roundToDouble() * grid;
final b = (bottom / grid).roundToDouble() * grid;
return Rect.fromLTRB(l, t, r, b);
}
Rect clamp(Rect container) {
// assert(width <= container.width && height <= container.height);
double l = max(left, container.left);
double t = max(top, container.top);
if (right > container.right) l -= right - container.right;
if (bottom > container.bottom) t -= bottom - container.bottom;
// l == left && t == top means that no clamping was done so return 'this' Rect
return l == left && t == top? this : Rect.fromLTWH(l, t, width, height);
}
Rect operator *(double operand) => Rect.fromLTRB(left * operand, top * operand, right * operand, bottom * operand);
}
class Item {
Item({
required Rect initialRect,
required this.color,
required this.debugName,
required this.container,
}) : _rect = initialRect;
Rect _rect;
final Color color;
final String debugName;
final Rect container;
// snapped Rect
Rect get rect => _rect.snap(1);
bool moveBy(Offset delta, bool clamp) {
final old = rect;
_rect = _rect.shift(delta);
if (clamp) _clamp(old, false);
return old != rect;
}
bool inflateBy(EdgeInsets insets, bool clamp) {
final old = rect;
_rect = insets.inflateRect(_rect);
if (clamp) _clamp(old, true);
return old != rect;
}
_clamp(Rect old, bool resize) {
if (old == rect) return;
if (resize) {
if (_rect.expandToInclude(container) != container) {
_rect = old;
}
} else {
_rect = _rect.clamp(container);
}
}
settle() => _rect = rect;
Widget build(bool isActive) => Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
gradient: RadialGradient(
colors: [Color.alphaBlend(Colors.white30, color), color],
radius: 1,
),
),
child: FittedBox(child: Text(debugName, style: const TextStyle(color: Colors.white70))),
);
String location(bool reportSize) {
final r = rect;
final position = r.topLeft;
final size = r.size;
final positionStr = '(${position.dx.toInt()},${position.dy.toInt()})';
return switch (reportSize) {
true => '${size.width.toInt()}x${size.height.toInt()} at $positionStr',
false => positionStr,
};
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment