import 'package:flutter/material.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/widget/draggable.dart' as my; import 'package:np_codegen/np_codegen.dart'; import 'package:to_string/to_string.dart'; part 'draggable_item_list.g.dart'; /// Describe an item in a [DraggableItemList] /// /// Derived classes should implement [operator ==] in order for the list to /// correctly map the items after changing the list content abstract class DraggableItemMetadata { bool get isDraggable; } /// A list where some/all of the items can be dragged to rearrange them class DraggableItemList extends StatefulWidget { const DraggableItemList({ super.key, required this.items, required this.maxCrossAxisExtent, required this.itemBuilder, required this.itemDragFeedbackBuilder, required this.staggeredTileBuilder, this.onDragResult, this.onDraggingChanged, }); @override State createState() => _DraggableItemListState(); final List items; final double maxCrossAxisExtent; final Widget Function(BuildContext context, int index, T metadata) itemBuilder; final Widget? Function(BuildContext context, int index, T metadata)? itemDragFeedbackBuilder; final StaggeredTile? Function(int index, T metadata) staggeredTileBuilder; /// Called when an item is dropped to a new place /// /// [results] contains the rearranged items final void Function(List results)? onDragResult; /// Called when user started (true) or ended (false) dragging final ValueChanged? onDraggingChanged; } @npLog class _DraggableItemListState extends State> { @override Widget build(BuildContext context) { return SliverStaggeredGrid.extentBuilder( key: ObjectKey(widget.maxCrossAxisExtent), maxCrossAxisExtent: widget.maxCrossAxisExtent, itemCount: widget.items.length, itemBuilder: (context, i) { final meta = widget.items[i]; if (meta.isDraggable) { return my.Draggable<_DraggableData>( data: _DraggableData(i, meta), feedback: widget.itemDragFeedbackBuilder?.call(context, i, meta), onDropBefore: (data) => _onMoved(data.index, i, true), onDropAfter: (data) => _onMoved(data.index, i, false), onDragStarted: () { widget.onDraggingChanged?.call(true); }, onDragEndedAny: () { widget.onDraggingChanged?.call(false); }, feedbackSize: Size(widget.maxCrossAxisExtent * .65, widget.maxCrossAxisExtent * .65), child: widget.itemBuilder(context, i, meta), ); } else { return widget.itemBuilder(context, i, meta); } }, staggeredTileBuilder: (i) => widget.staggeredTileBuilder(i, widget.items[i]), ); } void _onMoved(int fromIndex, int toIndex, bool isBefore) { if (fromIndex == toIndex) { return; } final newItems = widget.items.toList(); final moved = newItems.removeAt(fromIndex); final newIndex = toIndex + (isBefore ? 0 : 1) + (fromIndex < toIndex ? -1 : 0); newItems.insert(newIndex, moved); widget.onDragResult?.call(newItems); } } @toString class _DraggableData { const _DraggableData(this.index, this.meta); @override String toString() => _$toString(); final int index; final DraggableItemMetadata meta; }