Help us understand the problem. What is going on with this article?

【Flutter】ドラッグ&ドロップの実装

はじめに

以前AndroidでViewをドラッグ&ドロップする方法について紹介しました。
(AndroidでViewをドラッグ&ドロップする方法はこちらをご覧ください。)
今回はFlutterでどのようにWidgetをドラッグ&ドロップするのか調査したのでまとめます。
ちなみに今回はこちらの英文記事を参考にさせていただきました。
ソースコードはGithubで公開しています。

Draggableの実装

Flutterでドラッグ可能なWidgetを作成するには、Draggableを使います。
DraggableのchildにドラッグしたいWidgetを指定することで、そのWidgetをドラッグ可能にします。
また、Draggableはfeedbackというプロパティが必須となっています。
これはドラッグ中に表示されるWidgetのことです。
必須ではありませんが、childWhenDraggingを指定するとドラッグ中にchildの表示を変更できます。

実装は下記の通りです。

main.dart
class _HomeState extends State<Home> {

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(
          title: new Text("Drag & Drop"),
        ),
        body: Center(child: buildDraggable("Draggable", Icons.filter_1)));
  }

  Draggable buildDraggable(String name, IconData childIcon) =>
      new Draggable(
        child: Icon(
          childIcon,
          size: 90,
        ),
        feedback: Icon(
          Icons.android,
          size: 90,
        ),
        childWhenDragging: Icon(
          Icons.flag,
          size: 90,
        ),
      );
}

output.gif
1のアイコンがchildでドロイド君のアイコンがfeedback、そしてフラッグのアイコンがchildWhenDraggingです。

Draggableのイベントハンドリング

Draggableでハンドリングできるイベントは全部で4つあります。

  • onDragStarted
    • ドラッグが開始された際に呼ばれます。
    • 引数・戻り値なしのFuncitonです。
  • onDraggableCanceled
    • 後述するDragTarget以外のWidgetにドロップされた際に呼ばれます。
    • またはDragTargetに受け入れられていない状態でそのDragTargetにドロップした際に呼ばれます。
    • 引数にVelocity, Offsetを持つ戻り値なしのFunctionです。
      • Velocityはユーザー操作の速度を表すクラスです。pixel/secondで表されます。
      • Offsetは平面上のx座標・y座標を表すクラスです。
  • onDragCompleted
    • 後述するDragTargetに受け入れられた状態でそのDragTargetにドロップされた際に呼ばれます。
    • 引数・戻り値なしのFuncitonです。
  • onDragEnd
    • ドロップされた際に呼ばれます。
    • DraggableDetailsを引数に持つ戻り値なしのFunctionです。
      • DraggableDetailsはDragTargetの受け入れ状態とVelocity、そしてOffsetを持つクラスです。

実装は下記の通りです。

main.dart
  Draggable buildDraggable(String name, IconData childIcon) =>
      new Draggable(
        onDragStarted: () {
          print("Draggable.onDragStarted:");
        },
        onDraggableCanceled: (velocity, offset) {
          print("Draggable.onDraggableCanceled: velocity: $velocity, offset: $offset");
        },
        onDragCompleted: () {
          print("Draggable.onDragCompleted:");
        },
        onDragEnd: (details) {
          print("Draggable.onDragEnd: details: $details");
        },
        child: Icon(
          childIcon,
          size: 90,
        ),
        feedback: Icon(
          Icons.android,
          size: 90,
        ),
        childWhenDragging: Icon(
          Icons.flag,
          size: 90,
        ),
      );

上記GIFと同じように操作した際の呼び出し順を確認してみます。

各イベントの発生順
Draggable.onDragStarted:
Draggable.onDragEnd: details: Instance of 'DraggableDetails'
Draggable.onDraggableCanceled: velocity: Velocity(0.0, 0.0), offset: Offset(72.0, 232.7)

onDragCompletedはDragTargetが存在して尚且つ受け入れ可能な状態になっている場合にしか呼び出されないため、今回は呼び出されていません。

データを持たせる

Draggableにはデータを持たせることができます。
このデータは後述するDragTargetで受け取ることができます。

実装は下記の通りです。

main.dart
  Draggable buildDraggable(String name, IconData childIcon) => new Draggable(
        data: "I'm $name!", // add this line
        onDragStarted: () {
          print("DEBUG: Draggable.onDragStarted:");
        },
        onDraggableCanceled: (velocity, offset) {
          print(
              "DEBUG: Draggable.onDraggableCanceled: velocity: $velocity, offset: $offset");
        },
        onDragCompleted: () {
          print("DEBUG: Draggable.onDragCompleted:");
        },
        onDragEnd: (details) {
          print("DEBUG: Draggable.onDragEnd: details: $details");
        },
        child: Icon(
          childIcon,
          size: 90,
        ),
        feedback: Icon(
          Icons.android,
          size: 90,
        ),
        childWhenDragging: Icon(
          Icons.flag,
          size: 90,
        ),
      );

DragTargetの実装

Draggableではchildを用いてドラッグするWidgetを指定していましたが、DragTargetではbuilderを用います。
builderはBuildContext context, List<T> candidateData, List<dynamic> rejectedDataを引数に持つWidgetを返すFunctionです。(TにはDraggableから受け取るデータと同じ型を指定します。)
引数については後ほど説明します。

実装は下記の通りです。

main.dart
  DragTarget buildDragTarget() => new DragTarget(
        builder: (context, candidateData, rejectedData) {
          return new Container(
            width: 90,
            height: 90,
            color: Colors.red,
          );
        },
      );

全体の実装は下記のようになりました。

main.dart
class _HomeState extends State<Home> {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(
          title: new Text("Drag & Drop"),
        ),
        body: Center(
            child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(24.0),
              child: buildDragTarget(),
            ),
            Padding(
              padding: const EdgeInsets.all(24.0),
              child: buildDraggable("Draggable", Icons.filter_1),
            ),
          ],
        )));
  }

  Draggable buildDraggable(String name, IconData childIcon) => new Draggable(
        data: "I'm $name!",
        onDragStarted: () {
          print("Draggable.onDragStarted:");
        },
        onDraggableCanceled: (velocity, offset) {
          print("Draggable.onDraggableCanceled: velocity: $velocity, offset: $offset");
        },
        onDragCompleted: () {
          print("Draggable.onDragCompleted:");
        },
        onDragEnd: (details) {
          print("Draggable.onDragEnd: details: $details");
        },
        child: Icon(
          childIcon,
          size: 90,
        ),
        feedback: Icon(
          Icons.android,
          size: 90,
        ),
        childWhenDragging: Icon(
          Icons.flag,
          size: 90,
        ),
      );

  DragTarget buildDragTarget() => new DragTarget(
        builder: (context, candidateData, rejectedData) {
          return new Container(
            width: 90,
            height: 90,
            color: Colors.red,
          );
        },
      );
}

output.gif
赤い四角がDragTargetです。

DragTargetのイベントハンドリング

DragTargetでハンドリングできるイベントは全部で4つあります。
(builderはDragTargetのWidget生成関数ではありますが、呼ばれるタイミングが複数あるためここに記載します。)

  • builder
    • Widget生成時、Draggableが重なった時、Draggableが重なった状態から抜けた時、DraggableがDragTargetにドロップされた時に呼ばれます。
    • candidateData, rejectedDataのどちらもDraggableがDragTargetに重なったタイミングで値が入ります。
    • 下記のonWillAcceptがtrueを返す場合はcandidateDataに、falseの場合はrejectedDataに値が入ります。(公式ドキュメントには上記の通り記載がありましたが、動作確認したところ、rejectedDataに値が入ることだけは確認できませんでした。)
  • onWillAccept
    • Draggableが重なった時に呼ばれます。
    • 引数にDraggableから受け取ったデータを持つ戻り値boolのFunctionです。
    • 戻り値のboolでDraggableを受け入れるかどうかを表現します。
  • onAccept
    • onWillAcceptがtrueを返していて尚且つDragTargetにドロップされた時に呼ばれます。
    • 引数にDraggableから受け取ったデータを持つ戻り値なしのFunctionです。
  • onLeave
    • DraggableとDragTargetが重なった状態から抜けた場合に呼ばれます。
    • onWillAcceptがfalseを返した場合は、それに加えてDraggableがDragTargetにドロップされた場合にも呼ばれます。
    • 引数にDraggableから受け取ったデータを持つ戻り値なしのFunctionです。

実装は下記の通りです。

main.dart
  DragTarget buildDragTarget() => new DragTarget(
        builder: (context, candidateData, rejectedData) {
          print("DragTarget.builder: candidateData: $candidateData, rejectedData: $rejectedData");
          return new Container(
            width: 90,
            height: 90,
            color: Colors.red,
          );
        },
        onWillAccept: (data) {
          print("DragTarget.onWillAccept: data: $data");
          return true;
        },
        onAccept: (data) {
          print("DragTarget.onAccept: data: $data");
        },
        onLeave: (data) {
          print("DragTarget.onLeave: data: $data");
        },
      );

上記GIFのように操作した場合のDraggableとDragTargetの二つのイベントの流れは下記のようになります。

各イベントの発生順
## Widget生成→ドラッグ開始→DragTarget上でドロップした場合
DragTarget.builder: candidateData: [], rejectedData: []
Draggable.onDragStarted:
DragTarget.onWillAccept: data: I'm Draggable!
DragTarget.builder: candidateData: [I'm Draggable!], rejectedData: []
(onWillAcceptの戻り値がfalseの場合は、candidateData: [], rejectedData: [I'm Draggable!])
DragTarget.onAccept: data: I'm Draggable!
Draggable.onDragEnd: details: Instance of 'DraggableDetails'
Draggable.onDragCompleted:
DragTarget.builder: candidateData: [], rejectedData: []

## Widget生成→ドラッグ開始→DragTargetに重なった状態から抜け→ドロップした場合
DragTarget.builder: candidateData: [], rejectedData: []
Draggable.onDragStarted:
DragTarget.onWillAccept: data: I'm Draggable!
DragTarget.builder: candidateData: [I'm Draggable!], rejectedData: []
(onWillAcceptの戻り値がfalseの場合は、candidateData: [], rejectedData: [I'm Draggable!])
DragTarget.onLeave: data: I'm Draggable!
DragTarget.builder: candidateData: [], rejectedData: []
Draggable.onDragEnd: details: Instance of 'DraggableDetails'
Draggable.onDraggableCanceled: velocity: Velocity(0.0, 0.0), offset: Offset(162.2, 159.2)

Draggable, DragTargetを実装する際の注意点

DraggableとDragTargetのジェネリクスの型(DragされるデータとDropされるデータの型)が同じでないとDragTargetの各種メソッド(builder, onWillAcceptなど)が呼ばれないので、ジェネリクスで型の指定をする際には注意してください。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした