Preface:

This is the 30th day of my participation in the August Challenge. In preparation for the Nuggets’ August challenge, I’m going to pick 31 components this month that I haven’t covered before for a full analysis and attribute presentation. These articles will serve as important material for future compilation of Flutter components. I hope I can stick to it, your support will be my biggest motivation ~

This series Component articles The list of
1.NotificationListener 2.Dismissible 3.Switch
4.Scrollbar 5.ClipPath 6.CupertinoActivityIndicator
7.Opacity 8.FadeTransition 9. AnimatedOpacity
10. FadeInImage 11. Offstage 12. TickerMode
13. Visibility 14. Padding 15. AnimatedContainer
16.CircleAvatar 17.PhysicalShape 18.Divider
Flexible, Expanded, and Spacer 20.Card 21.SizedBox
22.ConstrainedBox 23.Stack 24.Positioned
25.OverflowBox 26.SizedOverflowBox 27. DecoratedBox
28. BackdropFilter 29. ImageFiltered and ColorFiltered 30. Draggable and DragTarget

First, know the Draggable component

Draggable, as the name suggests, is a dragable component that inherits from StatefulWidget and accepts a generic type. The constructor has many inputs, of which the child and feedback components must be passed in.

final Widget child;
final Widget feedback;
Copy the code

1. Drag direction: Axis

First, let’s take a look at Draggable through a small case: Here are three Draggable components, where Child is a small blue circle and feedback is a small red circle. The difference between the three components lies in axis attributes. The left axis is null, indicating that the axis is not limited and can be dragged freely. The middle axis is vertical, which can only be dragged in the vertical direction. The middle axis is horizontal and can only be dragged in the horizontal direction.

class CustomDraggable extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    List<Axis? > axis = [null, Axis.vertical, Axis.horizontal];
    return Wrap(
        spacing: 30,
        children: axis
            .map((Axis? axis) => Draggable(
                  axis: axis,
                  child: buildContent(),
                  feedback: buildFeedback(),
                ))
            .toList());
  }

  Widget buildContent() {
    return Container(
      width: 30,
      height: 30,
      alignment: Alignment.center,
      decoration: BoxDecoration(
        color: Colors.blue,
        shape: BoxShape.circle,
      ),
    );
  }

  Widget buildFeedback() {
    return Container(
      width: 30,
      height: 30, decoration: BoxDecoration( color: Colors.red, shape: BoxShape.circle, ), ); }}Copy the code

2. Original position component when dragging: childWhenDragging

The Draggable can specify the component in its original position during the drag process through the childWhenDragging attribute. Below, drag the original position as a small orange circle and remove the icon.

Draggable(
  axis: axis,
  childWhenDragging: buildWhenDragging(),
  child: buildContent(),
  feedback: buildFeedback(),
))
  
Widget buildWhenDragging() {
  return Container(
    width: 30,
    height: 30,
    decoration: BoxDecoration(
      color: Colors.orange,
      shape: BoxShape.circle,
    ),
    child: Icon(
      Icons.delete_outline,
      size: 20,
      color: Colors.white,
    ),
  );
}
Copy the code

2. Draggable and DragTarget are used together

1. Comprehensive test cases

Let’s test the combination of Draggable and DragTarget with an example. Here, the top ball is Draggable and the bottom area is DragTarget. You can drag the ball to color the DragTarget and display information about the current action.

Draggable can listen for five callbacks:

  • onDragStarted: Callback when dragging begins, no callback data.
  • onDragEnd: Can be retrieved when the drag endsDraggableDetailsThe data.
  • onDragUpdate: Can be obtained by dragging the update callbackDraggableDetailsThe data.
  • onDragCompleted: Drag into target area and release callback when completed, no callback data.
  • onDraggableCanceled: Not in the target area, drag and drop to cancel the callbackVelocityOffsetThe data.
List<Widget> _buildDraggable() {
  return colors.map(
        (Color color) => Draggable<Color>(
            onDragStarted: _onDragStarted,
            onDragEnd: _onDragEnd,
            onDragUpdate: _onDragUpdate,
            onDragCompleted: _onDragCompleted,
            onDraggableCanceled: _onDraggableCanceled,
            childWhenDragging: childWhenDragging(colors.indexOf(color).toString()),
            child: buildContent(color),
            data: color,
            feedback: buildFeedback(color)),
      ).toList();
}

void _onDragUpdate(DragUpdateDetails details) {
  print('coordinates:
      '(${details.localPosition.dx.toStringAsFixed(1)}, '
      '${details.localPosition.dy.toStringAsFixed(1)}) ');
}

void _onDraggableCanceled(Velocity velocity, Offset offset) {
  _info = 'Drag cancel';
}

void _onDragCompleted() {
  _info = 'Drag done';
}

void _onDragEnd(DraggableDetails details) {
  setState(() => _info = 'End drag');
}

void _onDragStarted() {
     setState(() => _info = 'Start dragging');
}
Copy the code

The DragTarget builds the component through the builder callback, which calls back the candidateData and rejectedData lists containing the accepted and rejectedData. Since Draggable supports multiple simultaneous drags, use lists of data.

DragTarget<Color>(
  onLeave: _onLeave,
  onAccept: _onAccept,
  onWillAccept: _onWillAccept,
  builder: _buildTarget,
)
  
Widget _buildTarget(BuildContext context, List<Color? > candidateData,List rejectedData) {
  return Container(
      width: 150.0,
      height: 50.0,
      color: _color,
      child: Center(
        child: Text(
          _info,
          style: TextStyle(color: Colors.white),
        ),
      ));
}

void _onLeave(Color? data) {
  print("onLeave: data = $data ");
  setState(() => _info = 'onLeave');
}

void _onAccept(Color data) {
  print("onAccept: data = $data ");
  setState(() => _color = data);
}

bool _onWillAccept(Color? data) {
  print("onWillAccept: data = $data ");
  setState(() => _info = 'onWillAccept');
  returndata ! =null;
}
Copy the code

OnWillAccept is one of the more important callbacks in DragTarget. OnWillAccept is triggered when the dragged component reaches the target region. It can be seen from the following source code that _candidateAvatars and _rejectedAvatars are related to the return value of onWillAccept. If onWillAccept returns false, the data will be simplified to _rejectedAvatars.

The callback parameters candidateData and rejectedData in Builder are calculated from the above two lists.


2. Drag and drop to delete the case

As shown in the following example, it is easy to remove by extending the component target to the specified location through the Draggable and DragTarget union.

The code implementation is as follows: The Draggable of different colors is generated by the color array colors, and has an int generic. The value passed is the index of the dragable component, so that the dragable index data can be obtained in the onAccept of the DragTarget, thus achieving the deletion function.

class DeleteDraggable extends StatefulWidget {
  @override
  _DeleteDraggableState createState() => _DeleteDraggableState();
}

class _DeleteDraggableState extends State<DeleteDraggable> {
  List<Color> colors = [
    Colors.red, Colors.yellow, Colors.blue, Colors.green,
    Colors.orange, Colors.purple, Colors.cyanAccent];

  @override
  Widget build(BuildContext context) {
    return  Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Wrap(
            children: _buildDraggable(),
            spacing: 10,
          ),
          SizedBox(
            height: 20,
          ),
          DragTarget<int>( onAccept: _onAccept, onWillAccept: (data) => data ! =null,
              builder: buildTarget
          )
        ],
    );
  }

  Widget buildTarget(context, candidateData, rejectedData) => Container(
      width: 40.0,
      height: 40.0,
      decoration: BoxDecoration(color: Colors.red, shape: BoxShape.circle),
      child: Center(
        child: Icon(Icons.delete_sweep, color: Colors.white),
      ));

  List<Widget> _buildDraggable() => colors
      .map((Color color) => Draggable<int>(
            child: buildContent(color),
            data: colors.indexOf(color),
            childWhenDragging: buildWhenDragging(),
            feedback: buildFeedback(color)),
      ).toList();

  Widget buildContent(Color color) {
    return Container(
      width: 30,
      height: 30,
      alignment: Alignment.center,
      child: Text(
        colors.indexOf(color).toString(),
        style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
      ),
      decoration: BoxDecoration(color: color, shape: BoxShape.circle),
    );
  }

  Widget buildFeedback(Color color) {
    return Container(
      width: 25,
      height: 25,
      decoration:
          BoxDecoration(color: color.withAlpha(100), shape: BoxShape.circle),
    );
  }

  Widget buildWhenDragging() {
    return Container(
      width: 30,
      height: 30,
      decoration: BoxDecoration(color: Colors.red, shape: BoxShape.circle),
      child: Icon(Icons.delete_outline, size: 20, color: Colors.white,
      ),
    );
  }

  void _onAccept(intdata) { setState(() { colors.removeAt(data); }); }}Copy the code

Through the combined use of Draggable and DragTarget, we do not need to implement the drag logic by ourselves, which can easily solve many target drag problems. That’s the end of this article. Thanks for watching and see you tomorrow