Draggable
を使用すれば、ドラッグができるウィジェットが作成できます。Draggable
にはいくつかプロパティがあります。プロパティのなかにはvelocity
(double値で計測される到達スピード)やoffset
(基準点からの距離: dx, dy)が出せるものもあります。
Draggableのプロパティ
// ドラッグを開始する方法を制御する
affinity: -> Axis?
// ドラッグ可能オブジェクトの縦横の動きを制限させる
axis: "x","y",
// ウィジェットの下に配置
child: -> Widget
// ドラッグ時に代わりに表示するウィジェット
childWhenDragging: -> Widget?
// ドロップされる内容
data: -> T?
// childDragAnchorStrategyとpointerDragAnchorStrategyの2つの関数を使用し、ドラッグしたポイントとフィードバックウィジェットの距離を計る
dragAnchorStrategy: → DragAnchorStrategy?
// ドラッグ中のポインターの下に表示するウィジェット
feedback: -> Widget
// ターゲットを見つける為のポイントを設定(ヒットテスト)
feedbackOffset: Offset.zero,
// オブジェクトのハッシュコード
hashCode: -> int
// ヒットテスト中の動作方法
hitTestBehavior: → HitTestBehavior
// フィードバックウィジェットをchildと同じオブジェクトにする場合はfalseに設定
ignoringFeedbackSemantics: true;
// ウィジェットが別のウィジェットに置き換わる方法を制御
key: -> Key?
// 最大ドラッグ数の設定、0にするとドラッグできなくなる。
maxSimultaneousDrags: 1,
// オブジェクトがドロップされ、DragTargetで受け入られたときに呼び出される。
onDragCompleted: → VoidCallback?
// Draggbleがドロップされた時に呼び出される。引数に、DragTaargetの受入れ状態・ベロシティ・オフセットのクラスが入ります。
onDragEnd: → DragEndCallback?
// Draggbleがドロップされた時に呼び出される。引数に、DragTaargetの受入れ状態・ベロシティ・オフセットのクラスが入ります。
onDraggableCanceled: → DraggableCanceledCallback?
// ドラッグが開始した時に呼び出される。
onDragStarted: → VoidCallback?
// ドラッグがドラッグされたタイミングで呼び出される。
onDragUpdate: → DragUpdateCallback?
// feedbackウィジェットをルートオーバーレイに配置するかどうか。 false(近い)/true(遠い)
rootOverlay: false,
// オブジェクトのランタイム。
runtimeType: → bool
Draggable
を長押しで起動することができるクラスです。Draggable
のプロパティに加え下記プロパティ
が追加されます。
LongPressDraggableのプロパティ
// ユーザーが長押してウィジェットが起動するまでの時間。
delay: Duration(milliseconds: 1000),
// ドラッグ開始時に振動させるかしないか。
hapticFeedbackOnStart: false
Draggable
ウィジェットでドロップしたデータを受け取るウィジェットで、ドラッグ可能オブジェクトがターゲット上にドラッグされるとデータが受入れられます。
DragTargetのプロパティ
// ドロップ範囲のUI設定
builder: → DragTargetBuilder<T>
// オブジェクトのハッシュコードを設定。
hashCode: → int
// ヒットテスト中の動作方法。
hitTestBehavior: → HitTestBehavior
// ウィジェットの置き換わる方法を制御。
key: → Key?
// ドロップが成功した時に呼び出され、ドロップしてきた値を取得。
onAccept: → DragTargetAccept<T>?
// ドロップが成功した特に呼び出される。ドラッガブルのデータとドロップされた位置を取得できる。
onAcceptWithDetails: → DragTargetAcceptWithDetails<T>?
// ドロップ範囲から離れた時に呼び出される。
onLeave: → DragTargetLeave<T>?
// Draggableがターゲット内で移動する度に呼び出される。
onMove: → DragTargetMove<T>?
// ドロップできる範囲に入った時呼び出され、bool値でドロップの可否を設定できる。
onWillAccept: (data) { return true },
// オブジェクトのランタイム。
runtimeType: → Type
今回は、Draggable
を利用して、ドラッグ&ドロップで要素を入れ替える実装に挑戦してみました。Flutter
デフォルト状態から書いたのでフックなどは使用してません。まずはmain.dart
を修正して、
main.dart
import 'package:flutter/material.dart';
// カテゴリのクラスを定義
class Category {
int index;
String name;
bool draggable = true;
// ゲッターでindexとnameを使用
Category(this.index, this.name);
}
void main() {
runApp(const DraggableScreen());
}
class DraggableScreen extends StatelessWidget {
const DraggableScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: DraggableTest(),
);
}
}
class DraggableTest extends StatefulWidget {
const DraggableTest({Key? key}) : super(key: key);
@override
State<DraggableTest> createState() => DraggableTestState();
}
class DraggableTestState extends State<DraggableTest> {
// List.generateとString.fromCharCodeで規則性のあるラテン文字を要素に持つリストを作成
List<Category> list = List.generate(
4,
(index) =>
Category(index, String.fromCharCode(index + 'A'.codeUnits[0])));
int index = 0;
// indexの初期値を設定
void updator() {
setState(() {
index = index + 1;
});
}
DraggableTestState();
@override
Widget build(BuildContext context) {
return Scaffold(
body: GridView.extent(
maxCrossAxisExtent: 120,
children: List.generate(12, (i) {
// 12個のwidgetを作成
return DragTargetItem(i, list, updator);
})),
);
}
}
// ignore: must_be_immutable
class DragTargetItem extends StatefulWidget {
// indexウィジェットの内容
final int index;
final List<Category> list;
final Function() updator;
const DragTargetItem(this.index, this.list, this.updator, {Key? key})
: super(key: key);
@override
// ignore: library_private_types_in_public_api
_DragTargetState createState() => _DragTargetState();
}
class _DragTargetState extends State<DragTargetItem> {
// ドラッグ&ドロップを判定するbool値
bool willAccept = false;
@override
Widget build(BuildContext context) {
return Stack(children: <Widget>[
// ドラッグターゲットのウィジェット
DragTarget(
builder: (context, candidateData, rejectedData) {
return Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: willAccept ? Colors.orangeAccent : Colors.white70,
border: Border.all(color: Colors.deepOrange),
borderRadius: BorderRadius.circular(5)),
child: Text(widget.index.toString(),
style: const TextStyle(color: Colors.black38, fontSize: 20.0)),
);
},
onAccept: (Category? data) {
if (category() == null) {
setState(() {
data?.index = widget.index;
});
} else {
setState(() {
// DragTargetItemでターゲットになったリスト内の最初の一致を返す
final cat = widget.list
.firstWhere((category) => category.index == widget.index);
// Draggableで掴んだリスト内の最初の一致を返す
final cat2 = widget.list
.firstWhere((category) => category.index == data!.index);
// swapで入れ替え
final tmp = cat.index;
cat.index = cat2.index;
cat2.index = tmp;
// indexを更新
widget.updator();
});
}
willAccept = false;
},
onWillAccept: (data) {
// ignore: avoid_print
print("ターゲット確認 $data");
willAccept = true;
return true;
},
onMove: (data) {
setState(() {});
},
onLeave: (data) {
willAccept = false;
},
),
if (category() != null && category()!.draggable)
Draggable(
// feedbackをchildと同じにする
ignoringFeedbackSemantics: false,
// ターゲットに入れたい値を設定、ここでは照合した結果を入れる
data: category(),
// DragTargetで受け入られた時呼び出される
onDragCompleted: () {
setState(() {
for (var d in widget.list) {
d.draggable = true;
}
widget.updator();
});
print("ドロップ完了");
},
onDragUpdate: (detail) {
setState(() {});
},
onDragStarted: () {
print("ドラッグ開始");
willAccept = true;
setState(() {
for (var d in widget.list) {
d.draggable = false;
}
widget.updator();
});
},
onDragEnd: (data) {
print("ドラッグ終了: $data");
willAccept = false;
},
// ドラッグ中、ターゲットに当てられるウィジェット
feedback: draggableWidget('B'),
// ドラッグアイテムの初期値のウィジェット
childWhenDragging: draggableWidget('C'),
// ドラッグアイテムの下に位置するウィジェットでレイアウト可能
child: draggableWidget('A'),
),
// draggableできる要素に装飾
if (category() != null && category()!.draggable)
// ignore: avoid_unnecessary_containers
Container(
alignment: Alignment.bottomCenter,
padding: const EdgeInsets.only(bottom: 5),
child: const Text("Draggable",
style: TextStyle(
color: Colors.white,
fontSize: 12.0,
)))
]);
}
// 引数でUIを変更できるようにしたウィジェット関数
Widget draggableWidget(String m) {
return Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: (m == 'C') ? Colors.orangeAccent : Colors.orange,
border: Border.all(color: Colors.deepOrange),
borderRadius: BorderRadius.circular(5)),
child: Stack(children: <Widget>[
(m == 'A')
? Text(widget.index.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 20.0,
decoration: TextDecoration.none))
: Container(),
Center(
child: Text(category()?.name ?? widget.index.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 60.0,
decoration: TextDecoration.none)))
]));
}
// 12個あるウィジェットの中にあるインスタンスのindexを照合
Category? category() {
final i = widget.list.indexWhere(((item) => item.index == widget.index));
return i >= 0 ? widget.list[i] : null;
}
}
次にwidget_test.dart
のtester.pumpWidget
の引数を下記内容に変えます。
widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_application_1/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
await tester.pumpWidget(const DraggableScreen());
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}
仕上がりはこのような感じです。入れ替えの過程で相当手こずりました。入れ替えのときはウィジェットを選べるようにしてあります。ときより移動前の挙動にバグが生じますが今後の課題にしたいと思います。