はじめに
以前AndroidでViewをドラッグ&ドロップする方法について紹介しました。
(AndroidでViewをドラッグ&ドロップする方法はこちらをご覧ください。)
今回はFlutterでどのようにWidgetをドラッグ&ドロップするのか調査したのでまとめます。
ちなみに今回はこちらの英文記事を参考にさせていただきました。
ソースコードはGithubで公開しています。
Draggableの実装
Flutterでドラッグ可能なWidgetを作成するには、Draggableを使います。
Draggableのchild
にドラッグしたいWidgetを指定することで、そのWidgetをドラッグ可能にします。
また、Draggableはfeedback
というプロパティが必須となっています。
これはドラッグ中に表示されるWidgetのことです。
必須ではありませんが、childWhenDragging
を指定するとドラッグ中にchildの表示を変更できます。
実装は下記の通りです。
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,
),
);
}
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を持つクラスです。
実装は下記の通りです。
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
で受け取ることができます。
実装は下記の通りです。
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から受け取るデータと同じ型を指定します。)
引数については後ほど説明します。
実装は下記の通りです。
DragTarget buildDragTarget() => new DragTarget(
builder: (context, candidateData, rejectedData) {
return new Container(
width: 90,
height: 90,
color: Colors.red,
);
},
);
全体の実装は下記のようになりました。
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,
);
},
);
}
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です。
実装は下記の通りです。
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など)が呼ばれないので、ジェネリクスで型の指定をする際には注意してください。