用Draggable和DragTarget在Flutter中拖放UI元素

1,483 阅读8分钟

自从我们大多数人开始使用电脑以来,拖放功能就一直存在。我们使用Gmail应用程序中的拖放功能,在收件人和抄送人之间移动地址。大多数图片托管服务提供了类似的功能来上传图片。一些送餐应用程序允许你使用拖放功能定制你的订单。等等。

简单地说,当用户选择一个项目,将其拖到屏幕上的另一点,然后释放它时,就发生了拖放互动。它是为了模仿我们在现实世界中拿起和移动东西的方式。

在本教程中,我们将演示如何在Flutter应用中使用DraggableDragTargetLongPressDraggable ,建立拖放交互。我们还将通过一些实际的例子,向您展示这些小部件如何协同工作,为您的用户产生引人入胜的拖放体验。

我们将详细介绍以下内容。

如果您是一个视觉学习者,请查看这个快速视频教程。

使用Draggable 小部件

[Draggable](https://api.flutter.dev/flutter/widgets/Draggable-class.html)是一个可以拖动或移动的Flutter小部件。只要用户点击并开始拖动Draggable widget,就会出现一个新的反馈widget并跟随用户的手指或鼠标指针。当用户抬起手指或鼠标指针时,反馈小部件就会消失。

让我们来看看如何创建一个Draggable widget。完成后的产品将看起来像这样。

Draggable Widget Final

图片来源:https://www.vecteezy.com/

下面是让它工作的代码。

Scaffold(
    body: Container(
  child: Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Draggable<String>(
          // Data is the value this Draggable stores.
          data: 'red',
          child: Container(
            height: 120.0,
            width: 120.0,
            child: Center(
              child: Image.asset('assets/images/tomato.png'),
            ),
          ),
          feedback: Container(
            height: 120.0,
            width: 120.0,
            child: Center(
              child: Image.asset('assets/images/tomato.png'),
            ),
          ),
        ),
      ],
    ),
  ),
))

让我们更深入地了解一下代码。

  • child :把你的小部件包在Draggable 小部件里面,并把它放在子参数中。在这里,我们有Container ,上面有一个西红柿图片
  • data: 每个Draggable 应该持有一些数据。这些数据将被DragTarget (我们将在后面看到)使用。在上面的代码中,我们给出的字符串数据为red
  • feedback: 你可以在这里写任何你希望出现在用户的手指或鼠标指针下面的小部件。为了获得更好的用户体验,你应该把同一个部件作为一个子部件。这给用户一种实际拖动项目的感觉。你可以稍微改变一下这个小部件的大小或形状,以增强拖动的体验,就像这样。

Dragging Copy

下面是代码是如何转化为设计的。

Code Translated to Design

替换拖动时的图像

在这一点上,我们能够拖动图片。但如果你注意到,你会发现当图像被拖动时,实际的图像保持原样。如果我们不改变或删除它,用户可能会感到困惑。

让我们来改变图像。

Draggable<String>(
  // Data is the value this Draggable stores.
  data: _color,
  child: Container(
    height: 120.0,
    width: 120.0,
    child: Center(
      child: Image.asset('assets/images/tomato.png'),
    ),
  ),
  feedback: Container(
    height: 120.0,
    width: 120.0,
    child: Center(
      child: Image.asset('assets/images/tomato.png'),
    ),
  ),
  //New
  childWhenDragging: Container(
    height: 120.0,
    width: 120.0,
    child: Center(
      child: Image.asset('assets/images/tomato_greyed.png'),
    ),
  ),
)

childWhenDragging 属性中添加一个小部件就可以解决这个问题。在上面的代码中,我们正在显示灰色背景的番茄图像。它看起来像这样。

Tomato With Grey Background

你也可以通过编写空容器来完全删除图像,看起来像这样。

childWhenDragging: Container(),

Removing Image

在单一方向上拖动

你可能想在一个固定的方向上拖动项目,无论是垂直还是水平方向。设置轴参数将允许项目在你选择的轴上拖动。

axis: Axis.vertical

上述代码将产生以下输出。

Dragging Single Direction

倾听拖动事件

Draggable 小组件使您能够监听拖动事件。您可以使用这些事件来触发一些行动,如移除项目本身或通知用户。

以下是您可以监听的事件。

  • onDragStarted: 一旦用户开始移动项目,您将得到一个回调。
  • onDragEnd: 当物品被丢在屏幕上的任何地方时,这个事件会被立即调用。它给出了物品的额外细节,无论它是否成功地丢在了丢弃区。
  • onDraggableCanceled :当物品没有成功掉落或用户抬起手指或鼠标指针时,这个功能被调用。
  • onDragCompleted: 当物品成功地被丢弃在丢弃区时,你将得到一个回调。

下面是你如何添加onDragStarted

onDragStarted: () {
  showSnackBarGlobal(context, 'Drag started');
},

Drag Started

DragTarget widget上丢弃一个项目

拖动一个项目是很酷的,但是如果我们不能把它放到某个地方,它就没有用。让我们试着把我们的项目放在 [DragTarget](https://api.flutter.dev/flutter/widgets/DragTarget-class.html)小组件。

DragTarget 接收Draggable widget;更确切地说,它接收由Draggable widget携带的数据。DragTarget 有方法根据数据来决定是否接受Draggable widget。

让我们设计一个Draggable widget,它看起来像这样。

Draggable Widget

图片来源:https://www.vecteezy.com/

下面是你如何做的。

Scaffold(
    body: Container(
  child: Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Draggable<String>(
          ...
         ),

        DragTarget<String>(
          builder: (
            BuildContext context,
            List<dynamic> accepted,
            List<dynamic> rejected,
          ) {
            return Container(
              height: 300,
              width: 300,
              child: Center(
                child: Image.asset(_isDropped
                    ? 'assets/images/bowl_full.png'
                    : 'assets/images/bowl.png'),
              ),
            );
          },
        ),
      ],
    ),
  ),
))

简单地将你的小部件包裹在DragTarget 。在这里,我们显示一个碗的图像作为番茄图像的拖动目标。

丢弃一个项目

在这一点上,我们仍然无法在DragTarget 中投放物品。因此,让我们看看如何能让项目下降。

DragTarget<String>(
  builder: (
    BuildContext context,
    List<dynamic> accepted,
    List<dynamic> rejected,
  ) {
    ...
  },
  onWillAccept: (data) {
    return data == 'red';
  },
  onAccept: (data) {
    setState(() {
      showSnackBarGlobal(context, 'Dropped successfully!');
      _isDropped = true;
    });
  },
),

添加两个名为onWillAcceptonAccept 的方法。

  • 每当项目在DragTarget 上被丢弃时,onWillAccept 被调用。我们可以使用这个方法来检索Draggable widget所携带的数据,并决定是否接受该项目。在上面的代码中,如果西红柿图片携带了该字符串,我们就接受它。red
  • onAccept 是一个回调,一旦该项目被DragTarget 接受,我们就应该收到这个回调。我们正在显示成功信息并更新_isDropped_isDropped ,用来改变碗的图像,以显示碗内的西红柿图像。

下面是它现在的样子。

Dropped Successfully

如果你想在物品离开时通知用户,而不将其放入可拖动区域,只需再添加一个名为onLeave 的方法。

onLeave: (data) {
  showSnackBarGlobal(context, 'Missed');
},

Missed Drop

使UI元素可以在长按时拖动LongPressDraggable

[LongPressDraggable](https://api.flutter.dev/flutter/widgets/LongPressDraggable-class.html)是另一个可拖动的小部件。LongPressDraggableDraggable 之间的唯一区别是,LongPressDraggable 允许你在长按上拖动项目,而Draggable 可以即时拖动。

当你想拖动的项目在一个列表内时,LongPressDraggable 很有用。例如,当你想把一张照片从画廊移到其他地方时,你应该使用LongPressDraggable 而不是Draggable ,像这样。

Dragging Square

图片来源:https://www.vecteezy.com/

正如你在上面的GIF中看到的那样,红色的、方形的项目已经准备好被拖动了,但只有当用户在它上面做长按时才可以。

这里是代码。

Scaffold(
    body: Container(
  child: Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        LongPressDraggable<String>(
          // Data is the value this Draggable stores.
          data: _color,
          child: Container(
            height: 150.0,
            width: 150.0,
            color: Colors.redAccent,
            child: const Center(
              child: Text(
                'Drag me',
                textScaleFactor: 2,
              ),
            ),
          ),
          feedback: Material(
            child: Container(
              height: 170.0,
              width: 170.0,
              decoration: BoxDecoration(
                color: Colors.redAccent,
              ),
              child: const Center(
                child: Text(
                  'Dragging',
                  textScaleFactor: 2,
                ),
              ),
            ),
          ),
          childWhenDragging: Container(
            height: 150.0,
            width: 150.0,
            color: Colors.grey,
            child: const Center(
              child: Text(
                'I was here',
                textScaleFactor: 2,
              ),
            ),
          ),
        ),
        SizedBox(
          height: MediaQuery.of(context).size.height * 0.15,
        ),
        DragTarget<String>(
          builder: (
            BuildContext context,
            List<dynamic> accepted,
            List<dynamic> rejected,
          ) {
            return DottedBorder(
              borderType: BorderType.RRect,
              radius: Radius.circular(12),
              padding: EdgeInsets.all(6),
              color: Colors.white,
              strokeWidth: 2,
              dashPattern: [8],
              child: ClipRRect(
                borderRadius: BorderRadius.all(Radius.circular(12)),
                child: Container(
                  height: 200,
                  width: 200,
                  color: _isDropped ? Colors.redAccent : null,
                  child: Center(
                      child: Text(
                    !_isDropped ? 'Drop here' : 'Dropped',
                    textScaleFactor: 2,
                  )),
                ),
              ),
            );
          },
          onAccept: (data) {
            debugPrint('hi $data');
            setState(() {
              showSnackBarGlobal(context, 'Dropped successfully!');
              _isDropped = true;
            });
          },
          onWillAccept: (data) {
            return data == _color;
          },
          onLeave: (data) {
            showSnackBarGlobal(context, 'Missed');
          },
        ),
      ],
    ),
  ),
))

大部分代码与我们之前讨论的相同;只是将Draggable widget替换为LongPressDraggable

Flutter拖放示例。构建一个七巧板

现在你知道了如何在Flutter中实现拖放互动,你应该能够自己构建任何东西。为了测试我们的技能,让我们尝试建立一个非常基本的拼图。

下面是我们要搭建的东西。

Dragging Puzzle Piece

图片来源:https://www.vecteezy.com/

首先,强制该应用程序只在横向模式下打开。

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  SystemChrome.setPreferredOrientations(
      [DeviceOrientation.landscapeRight, DeviceOrientation.landscapeLeft]).then(
    (_) => runApp(MyApp()),
  );
}

接下来,定义保存拼图块状态的变量(它们是否被成功放置)。

bool _isBlueDropped = false;
bool _isRedDropped = false;
bool _isYelloDropped = false;
bool _isGreenDropped = false;
String _blue = 'blue';
String _red = 'red';
String _yellow = 'yellow';
String _green = 'green';

创建四个DragTarget widgets的Stack 。在Position widget的帮助下排列,使其看起来像一个2×2的拼图。

Stack(
  children: [
    Positioned(
      top: 0,
      left: 0,
      child: DragTarget<String>(),
    ),
    Positioned(
      top: 0,
      right: 0,
      child: DragTarget<String>(),
    ),
    Positioned(
      bottom: 0,
      left: 0,
      child: DragTarget<String>(),
    ),
    Positioned(
      bottom: 0,
      right: 0,
      child: DragTarget<String>(),
    ),
  ],
)

现在创建一个拼图片的列表。每个拼图片都是一个Draggable widget。

SingleChildScrollView(
  child: Column(
    children: [
      Visibility(
        visible: !_isRedDropped,
        child: Draggable<String>(),
      ),
      Visibility(
        visible: !_isGreenDropped,
        child: Draggable<String>(),
      ),
      Visibility(
        visible: !_isBlueDropped,
        child: Draggable<String>(),
      ),
      Visibility(
        visible: !_isYelloDropped,
        child: Draggable<String>(),
      ),
    ],
  ),
),

列表中的拼图块一旦被正确放置,就会被隐藏。可见性是用我们前面定义的变量来管理的。

就这样了!完整的源代码可以在GitHub上找到。

总结

在本教程中,我们学习了如何在Flutter中建立一个拖放式交互。我们通过实际例子学习了如何使用各种小部件,如DraggableDragTargetLongPressDraggable 。最后,我们演示了如何使用本教程中描述的部件和技能来开发一个简单的拼图。

The postDrag and drop UI elements in Flutter with Draggable and DragTargetappeared first onLogRocket Blog.