Flutterでドラッグ＆ドロップ

Flutterで drag-and-drop を作ってみました。リスト形式でよいなら完成されたものがありますが、いろいろカスタマイズしたかったので調べてみました。この先ドラッグ＆ドロップ文化は生き残るのでしょうか。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253995/21677727-693e-c600-9449-d5deb438b6aa.gif">

# 概要
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253995/2d65340c-8587-04e6-2d73-724a7b5125ea.jpeg">

ドロップするウィジェットとドロップされるウィジェットが必要です。DragTargetとDraggableというクラスを使います。今回の例では9個のDragTargetをタイル状に置き、その上に2個のDraggableを置きます。

# ソース

```dart:screen.dart
import 'package:flutter/material.dart';

class ItemData{
  int index = -1;
  String name = "";
  ItemData({int? index, String? name}){
    if(index != null) this.index = index;
    if(name != null) this.name = name;
  }
}

class DragDropScreen extends StatefulWidget {
  const DragDropScreen();
  @override
  _DragDropState createState() => _DragDropState();
}

class _DragDropState extends State<DragDropScreen> {
  List<ItemData> itemList = [];
  _DragDropState() {
    itemList.add(ItemData(index:2, name:"A"));
    itemList.add(ItemData(index:6, name:"B"));
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      backgroundColor: Color(0xFF333333),
        body: GridView.extent(
          maxCrossAxisExtent: 150,
          children: List.generate(9, (index) {
            return DragTargetItem(index+1, this.itemList);
          })),
      );
  }
}

class DragTargetItem extends StatefulWidget {
  int index = 0;
  List<ItemData> itemList = [];
  DragTargetItem(int index, List<ItemData> itemList) {
    this.index = index;
    this.itemList = itemList;
  }
  @override
  _DragTargetState createState() => _DragTargetState();
}

class _DragTargetState extends State<DragTargetItem> {
  bool isWillAccept = false; // 重なり中
  @override
  Widget build(BuildContext context) {
    return Stack(
        children: <Widget>[
          // ドロップされるウィジェット
          DragTarget(
            builder: (context, candidateData, rejectedData) {
              return dropWedget();
            },
            // ドロップされたときデータのインデックスだけ入れ替える
            onAccept: (ItemData? data) {
              if(data != null && getItemData()==null){
                setState((){
                  data.index = widget.index;
                });
              }
              isWillAccept = false;
            },
            // 重なったとき
            onWillAccept: (data) {
              isWillAccept = true;
              return true;
            },
            // 重なりから抜けたとき
            onLeave: (data) {
              isWillAccept = false;
            },
          ),

          // アイテムウィジェット
          getItemData()==null ? Container() :
          Draggable(
            data: getItemData(),
            onDragStarted: () {},
            onDraggableCanceled: (velocity, offset) {},
            onDragCompleted: () { setState((){}); }, // ドロップに成功したとき
            onDragEnd: (details) {},
            child: itemWedget(0), // 通常
            feedback: itemWedget(1), // ドラッグ中
            childWhenDragging: itemWedget(2), // ドラッグ元
          ),
        ]);
  }

  /// ドロップされるウィジェット
  Widget dropWedget() {
    return Container(
      width: 130.0, height: 130.0,
      decoration: BoxDecoration(
        color: isWillAccept ? Color(0xFFAAAAAA) : Color(0xFF888888),
      ),
      child: Text(widget.index.toString(), style: TextStyle(color: Colors.white, fontSize: 20.0)),
    );
  }

  /// アイテムウィジェット（m=0通常、1ドラッグ中、2ドラッグ元）
  Widget itemWedget(int m) {
    return Container(
      width: 130.0, height: 130.0,
      decoration: BoxDecoration(
        color: (m==2) ? Color(0x44224488) : Color(0x88224488),
      ),
      child: Stack(children: <Widget>[
        (m==0) ? Text(widget.index.toString(), style: TextStyle(color: Colors.white, fontSize: 20.0)) : Container(),
        Center(child:Text(getName(), style: TextStyle(color: Colors.white, fontSize: 40.0)))
      ])
    );
  }

  // 親のitemListを参照
  ItemData? getItemData() {
    final i = widget.itemList.indexWhere(((item) => item.index==widget.index));
    return i>=0 ? widget.itemList[i] : null;
  }

  String getName() {
    return getItemData()!=null ? getItemData()!.name : "";
  }
}
```

# 解説
ドロップされるウィジェットをタイル状に並べるという方法にしました。Windowsで作った時は自由に移動してグリッドに吸い付くという処理をしましたが、これならグリッドの吸い付き処理もいらなそうです。ドラッグで呼ばれる data はする側とされる側で同じものを入れます。
子ウィジェットの変更を親ウィジェットに伝えるためにリストを参照しています。リストを参照する仕組みや、StatefulWidgetに代わる便利なものが出ていますね（勉強中）

# 今後
## 将来すべてのアプリがAndroid,iOSに置き換わるのか
私は現在Windowsの業務用アプリを作っています。納品するときはWindowsのノートPCを提供しています。androidで作ってタブレットにマウスとキーボードをつければ一緒ですよね。今後どうなっていくのでしょうか。

## ドラッグ＆ドロップ文化はこの先生き残るのか
スマホやタブレットの他のアプリでは、画面を拡大したり移動したりするモードと、アイテムをドラッグ＆ドロップするモードを切り替えたりしています。MACのタッチバッドだと一本指と二本指の操作で動きを変えています。10インチのiPadで連続してドラッグ＆ドロップすると、汗で止まったり指の指紋がなくなりますよね。

今私が担当しているWindowsアプリが100個や200個をドラッグ＆ドロップするアプリです。また同じようにドラッグ＆ドロップで実装してよいのかどうか。そもそも実際にアプリを作ってみないと深く考えなかった疑問です。作ってみるしかないですね。