Flutter拖拽控件Draggable

3,131 阅读4分钟

Draggable

最近做了一个Flutter项目,其中有一个需求是做出三个可以互相拖拽的任务列表,平时在做Android项目时,拖动的控件基本上都是自定义View来实现的,想看看在Fluter上大家都是怎么实现的,没想到flutter提供了一个非常方便的拖拽控件Draggable。

Draggable的构造函数

我个人在Flutter开发时,遇到没有见过的控件时,点开源码观察它的构造函数一定是了解它的功能的最优解,我们看Draggable的构造函数:

  const Draggable({
    Key key,
    @required this.child, // child不用解释了吧
    @required this.feedback, // 拖动时显示的组件
    this.data, // 控件携带的数据(一般提供给DragTarget,后面会讲)
    this.axis, // 对滑动的方向做限制(横向或者纵向)
    this.childWhenDragging, //多点触控拖动时显示的组件
    this.feedbackOffset = Offset.zero, // 拖动后显示的位置,默认(00this.dragAnchor = DragAnchor.child, // 开始拖动时feedback显示的位置(原child位置还是触摸的位置)
    this.affinity, // 让Draggable可以共享垂直或水平方向上的滑动事件(例如拖动同时滚动Scrollable)
    this.maxSimultaneousDrags, // 多点触控时最大响应数
    this.onDragStarted, // 开始拖动时的回调
    this.onDraggableCanceled, // 未拖动到DragTarget控件上时回调
    this.onDragEnd, // 拖动结束时的回调
    this.onDragCompleted, // 拖动到DragTarget控件上时回调
    this.ignoringFeedbackSemantics = true, // 控制是否显示feedback
  }) : assert(child != null),
       assert(feedback != null),
       assert(ignoringFeedbackSemantics != null),
       assert(maxSimultaneousDrags == null || maxSimultaneousDrags >= 0),
       super(key: key);

可以看到Draggable还是很强大的,把拖动时显示的weiget也给封装好了,我们只用传入一个weiget就能自动显示,对拖拽事件的回调也很丰富,其中的DragTarget是另一个跟Draggable搭配的强大控件,下面先实现一个简单的Draggable:

Draggable的实现

Container(
        alignment: Alignment.center,
        child: Draggable(
          child: Text("我可以被拖动!"),
          feedback: Text("我正在被拖动!"),
        ),
      ),

是不是很简单,我们可以加一点其他的属性,比如对水平拖动进行限制

Container(
        alignment: Alignment.center,
        child: Draggable(
          axis: Axis.vertical,
          child: Text("我可以被拖动!"),
          feedback: Text("我正在被拖动!"),
        ),
      ),

DragTarget

但是一般我们的拖动逻辑都是将一个控件挪到指定的位置(或者控件)上,以往我们都是通过拖动后的坐标等方法判断,然而观察Draggable的回调发现还有一个与Draggable配套的控件DragTarget,用DragTarget就可以把拖动到指定位置上的判断交给系统判断,话不多说,放码过来

DragTarget的构造函数

  const DragTarget({
    Key key,
    @required this.builder, // 构造器
    this.onWillAccept, // 判断该数据是否符合要求
    this.onAccept, // 接收 Data 数据的回调
    this.onLeave, // 
  }) : super(key: key);

DragTarget的构造函数就简单许多了:

builder:构造器;

其中参数包括三个属性,分别为 context 上下文环境,candidateData 为 onWillAccept 回调为 true 时可接收的数据列表,rejectedData 为 onWillAccept 回调为 false 时拒绝时的数据列表;builder的返回值就是DragTarget的child

typedef DragTargetBuilder<T> = Widget Function(BuildContext context, List<T> candidateData, List<dynamic> rejectedData);

onWillAccept:Draggable拖拽到 DragTarget 时的回调

用于判断该数据是否符合要求,返回true 时会将 Data 数据添加到 candidateData 列表中,且会调用onAccept;false 时会将 Data 数据添加到 rejectedData 列表中,且不会调用onAccept;

onAccept:用于接收Draggable中Data数据的回调;

onLeave:为Draggable离开时的回调;

DragTarget的实现

举个栗子

  String data = "默认数据";

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.only(top: 200),
      color: Colors.white,
      child: Column(
        children: [
          Draggable<String>(
            data: "Draggable数据",,
            child: Text("我可以被拖动!"),
            feedback: Text("我正在被拖动!"),
          ),
          DragTarget<String>(
            builder: (BuildContext context, List<dynamic> accepted, List<dynamic> rejected,) {
              return Text(data);
            },
            onAccept: (data) {
              setState(() {
                this.data = data;
              });
            },
          )
        ],
      ),
    );
  }

同理,我们可以应用到更复杂的画面中,还记得我一开始的需求么:做出三个可以互相拖拽的任务列表,于是我们可以将DragTarget和Draggable结合起来,将list的item做为Draggable,将每个list作为DragTarget,加上亿点点细节:

  List<String> list1 = ["list1_1", "list1_2", "list1_3"];
  List<String> list2 = ["list2_1", "list2_2"];
  List<String> list3 = ["list3_1", "list3_2"];

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.only(top: 200),
      color: Colors.white,
      child: Column(
        children: [
          _createListView(list1),
          _createListView(list2),
          _createListView(list3),
        ],
      ),
    );
  }

  Widget _createListView(List<String> _items) {
    return DragTarget<String>(
      builder: (
        BuildContext context,
        List<dynamic> accepted,
        List<dynamic> rejected,
      ) {
        return ListView.builder(
          itemCount: _items.length,
          shrinkWrap: true,
          padding: EdgeInsets.all(10),
          itemBuilder: (context, index) {
            return Draggable<String>(
              onDragCompleted: () {
                // 在拖动到DragTarget后删除数据
                setState(() {
                  _items.removeAt(index);
                });
              },
              feedback: Material(
                child: Container(
                  height: 60,
                  width: 200,
                  color: Colors.blueAccent,
                  alignment: Alignment.center,
                  child: Text(
                    _items[index],
                    style: TextStyle(color: Colors.white),
                  ),
                ),
              ),
              data: _items[index],
              child: Container(
                height: 50,
                width: 200,
                color: Colors.blueAccent,
                alignment: Alignment.center,
                child: Text(
                  _items[index],
                  style: TextStyle(color: Colors.white, fontSize: 20),
                ),
              ),
            );
          },
        );
      },
      onAccept: (String data) {
        setState(() {
          // 添加Draggable数据到list
          _items.add(data);
        });
      },
    );
  }

搞定!