huhuang03 / reorderable_grid_view

MIT License
46 stars 33 forks source link

Expose onDragUpdate callback #56

Closed aroningruber closed 1 year ago

aroningruber commented 1 year ago

What changed?

ReorderableGridView, ReorderableSliverGridView and ReorderableWrapperWidget now expose an additional callback onDragUpdate (similarly to the already existing onDragStart callback). The callback is invoked by the _onDragUpdate method of the ReorderableGridWidgetMixin. It receives the index of the dragged item, the current position of the pointer in the global coordinate system and the relative offset since the last invocation of the callback.

Why is this change useful?

By exposing the onDragUpdate callback, it becomes possible to react to position changes of the dragged widget and to track the pointer position while dragging.

Will this change break any existing code?

No. This change introduces an additional non-required nullable callback attribute to ReorderableGridView, ReorderableSliverGridView and ReorderableWrapperWidget. For any existing code, the new callback will automatically be set to null, which causes the callback not to be called inside ReorderableGridWidgetMixin._onDragUpdate and therefore does not change the existing behavior.

Example

An example use of the new callback is to detect if the pointer position of a dragged widget is within a predefined drop region. This allows us to mimic some of the behavior of Flutter's built-in DropTarget class.

In the example below, a grid item can be dropped onto a predefined drop region to delete it from the grid. To provide visual feedback, if the item will be deleted if dropped, it is necessary to check, if the pointer is inside the drop region while the item is being dragged. The new callback allows us to perform this check every time the position of the item being dragged changes.

https://user-images.githubusercontent.com/37301927/209568774-8546497a-3c06-4c09-b06b-eec694e09f84.mp4

class OnDragUpdateExample extends StatefulWidget {
  const OnDragUpdateExample({super.key});

  @override
  State<OnDragUpdateExample> createState() => _OnDragUpdateExampleState();
}

class _OnDragUpdateExampleState extends State<OnDragUpdateExample> {
  final _data = List.generate(15, (index) => index + 1, growable: true);
  final _deleteWidgetKey = GlobalKey();

  /// Indicates if the dragged item is above the delete drop target
  bool _aboveDeleteDropTarget = false;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        Expanded(
          child: ReorderableGridView.count(
            crossAxisCount: 5,
            mainAxisSpacing: 8.0,
            crossAxisSpacing: 8.0,
            onReorder: _onReorder,
            onDragUpdate: _onDragUpdate,
            dragWidgetBuilder: _dragWidgetBuilder,
            children: _data.map(_buildItem).toList(),
          ),
        ),
        Expanded(
          child: Center(
            child: AnimatedContainer(
              key: _deleteWidgetKey,
              duration: const Duration(milliseconds: 200),
              alignment: Alignment.center,
              height: 128,
              width: 128,
              color: Colors.red.withAlpha(_aboveDeleteDropTarget ? 200 : 50),
              child: const Text(
                'Drop here\n\nto delete item',
                textAlign: TextAlign.center,
              ),
            ),
          ),
        ),
      ],
    );
  }

  Widget _buildItem(int value) {
    return InkWell(
      key: ValueKey(value),
      onTap: () {/* Placeholder for InkWell animation */},
      child: Container(
        color: Colors.blue.withAlpha(50),
        child: Center(child: Text('$value')),
      ),
    );
  }

  Widget _dragWidgetBuilder(int index, Widget child) {
    return Material(
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 200),
        color: Colors.blue.withAlpha(150),
        child: Center(child: Text('${_data[index]}')),
      ),
    );
  }

  void _onReorder(int oldIndex, int newIndex) {
    if (_aboveDeleteDropTarget) {
      // Item was dropped above delete drop target
      // => delete item
      setState(() {
        _data.removeAt(oldIndex);
      });
    } else {
      // Reorder items: Insert dragged item at new index
      setState(() {
        _data.insert(newIndex, _data.removeAt(oldIndex));
      });
    }
    // Dragging is completed
    // => reset state
    setState(() {
      _aboveDeleteDropTarget = false;
    });
  }

  void _onDragUpdate(int dragIndex, Offset position, Offset delta) {
    final deleteRect = _tryGetRect(_deleteWidgetKey);
    // Check if the current pointer position is inside the delete drop target.
    final cursorInsideDeleteDropTarget =
        deleteRect?.contains(position) ?? false;

    setState(() {
      _aboveDeleteDropTarget = cursorInsideDeleteDropTarget;
    });
  }
}

/// Tries to retrieve the [Rect] of the widget that matches this [globalKey].
///
/// If there is no widget in the tree that matches this [globalKey],
/// `null` is returned.
Rect? _tryGetRect(GlobalKey globalKey) {
  final renderBox = globalKey.currentContext?.findRenderObject() as RenderBox?;

  if (renderBox == null) {
    return null;
  }

  final size = renderBox.size;
  final position = renderBox.localToGlobal(Offset.zero);

  return Rect.fromLTWH(position.dx, position.dy, size.width, size.height);
}
huhuang03 commented 1 year ago

Thank you for your commit!

Released in version 2.2.6-alpha.5