一、认识 Draggable 组件

Draggable 顾名思义,是可拖动的组件,它继承自 StatefulWidget ,且可接受一个泛型。 构造方法有非常多的入参,其中必须传入的是 childfeedback 两个组件。

img

final Widget child;
final Widget feedback;
复制代码

1. 拖动的方向: axis

下面先通过一个小案例认识一下 Draggable:下面是三个 Draggable 组件,其中 child 是蓝色小圆,feedback 是红色小圆,三者的区别在于 axis 属性不同。左边 axisnull ,表示不限定轴向,可以自由拖动;中间 axisvertical ,只能在竖直方向拖动;中间 axishorizontal ,只能在水平方向拖动。

img

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,
      ),
    );
  }
}
复制代码

2.拖动时原位置组件: childWhenDragging

Draggable 可以通过 childWhenDragging 属性指定在拖拽过程中原来位置的组件。如下,拖动时原来的位置显示为 橙色小圆 和 删除图标。

img

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,
    ),
  );
}
复制代码

二、Draggable 与 DragTarget 联合使用

1. 综合测试案例

下面通过一个示例测试一下 DraggableDragTarget 的联合使用。如下,上面的小球是 Draggable ,下面的区域是 DragTarget 。可以拖动小球来为 DragTarget 着色,并且显示当前操作的信息。

img

Draggable 可以监听五个回调:

  • onDragStarted :开始拖动时回调,无回调数据。
  • onDragEnd:结束拖动时回调,可以获取 DraggableDetails 数据。
  • onDragUpdate:拖动更新时回调,可以获取 DraggableDetails 数据。
  • onDragCompleted : 拖入目标区域,并松手完成时回调,无回调数据。
  • onDraggableCanceled:未在目标区域,拖拽取消回调,可以获取 VelocityOffset数据。
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('坐标:'
      '(${details.localPosition.dx.toStringAsFixed(1)},'
      '${details.localPosition.dy.toStringAsFixed(1)})');
}

void _onDraggableCanceled(Velocity velocity, Offset offset) {
  _info = '拖拽取消';
}

void _onDragCompleted() {
  _info = '拖拽完成';
}

void _onDragEnd(DraggableDetails details) {
  setState(() => _info = '结束拖拽');
}

void _onDragStarted() {
     setState(() => _info = '开始拖拽');
}
复制代码

DragTarget 会通过 builder 回调来构建组件,其中会回调 candidateDatarejectedData 两个列表,其中包含接受拒绝 的数据。由于 Draggable 支持多个同时拖动,使用是数据列表。

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');
  return data != null;
}
复制代码

onWillAcceptDragTarget 中比较重要的一个回调,当拖动的组件到达目标区域后,onWillAccept 会触发。从下面源码中可以看出 _candidateAvatars_rejectedAvatarsonWillAccept 的返回值有关。如果 onWillAccept 返回 false ,则数据会被简入到 _rejectedAvatars

img

builder 中的回调入参 candidateDatarejectedData 就是根据上面两个列表计算的。

img


2.拖拽删除案例

如下示例,通过拓展组件目标到指定位置进行移除,通过 Draggable 和 DragTarget 联合就很容易实现。

img

代码实现如下,通过颜色数组 colors 生成不同颜色的 Draggable ,并拥有 int 泛型,传递的数值为可拖拽组件的索引,这样在 DragTargetonAccept 中可以获取拖入进的索引数据,从而实现删除功能。

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(int data) {
    setState(() {
      colors.removeAt(data);
    });
  }
}
复制代码

通过Draggable 和 DragTarget 的联合使用,不需要我们自己去实现拖拽逻辑,可以很轻松解决很多目标拖拽的问题。那本文到这里就结束了,谢谢观看,明天见~