対象者
- FlutterでListView内のタイルなどをスワイプで消したい人
はじめに
Flutterの中でリストの一覧を作成することがあります。その一覧の中のデータを削除しようとしたときに、長押しで削除確認ダイアログを出すようにしていました。しかしスワイプでも同じことができるということで、UX的にそっちの方が良いケースもあるでしょうから、実験してみました。
実施するソース
urlsはList
Dismissibleでスワイプで削除できるようにできるWidgetです。各画像をこれで包むことで、画像を消せるようにしています。
ListView.builder(
itemCount: urls.length,
itemBuilder: (context, index) {
var url = urls[index];
return Padding(
padding: const EdgeInsets.all(8.0),
child: Dismissible(
key: ValueKey(url),
onDismissed: (direction) {
setState(() {
urls.removeAt(index);
});
},
direction: DismissDirection.startToEnd,
confirmDismiss: (direction) async {
var result = await showDialog<bool>(
【中略】
);
return Future.value(result);
},
background: Container(
color: Colors.black26,
child: const Align(
alignment: Alignment.centerLeft,
child: Icon(
Icons.delete,
color: Colors.white,
size: 48,
),
),
),
dismissThresholds: const {DismissDirection.startToEnd: 0.4},
child: AspectRatio(
aspectRatio: 16 / 9,
child: Image.network(
url,
fit: BoxFit.fitWidth,
)),
),
);
},
Dismissibleを細かく見ていきましょう。
-
key
他の画像を削除した後、同じWidgetを再利用してくれるようにkeyを渡してます。URLがユニークであることが前提になってます。 -
onDismissed
スワイプが完了して、削除すると決まったときの処理を定義します。ここではurlsからデータを削除しています。そうすることで、削除しようとした画像が表示されなくなります。 -
direction
スワイプする方向を指定します。今回は「DismissDirection.startToEnd」ですので、左から右にスワイプしたときに削除の処理がされます。他にも「右から左(endToStart)」「水平(horizontal)」「上から下(down)」「下から上(up)」「垂直(vertical)」が指定できます。
ちなみに今回「垂直」を選択したら、消せる代わりに、スクロールできなくなりました(笑) -
confirmDismiss
スワイプを実施したときと削除処理の間に実行します。Future.value(true)を返すと削除され、Future.value(false)だと削除がキャンセルされます。
この場合、本当に削除するかダイアログを表示して聞いています。 -
background
削除しようとスワイプしたときに表示されるWidgetを定義します。黒色でゴミ箱が表示されるようにしています。設定無しで中央に配置されるのと、半分までスワイプしないとゴミ箱が表示されないので左寄せにしてます。 -
dismissThresholds
どれくらいスワイプしたら、削除処理を実行するかを決めます。
マップで「どの向きに対して」をキーにして、「どれくらいの割合」を値にします。0に近い方が少しでもスワイプすると、削除処理を開始するようになります。 -
child
実際に表示するWidgetを定義します。この場合、16:9で表示されたネットワーク画像です。
まとめ
ということで、スワイプでWidgetを消す方法を紹介しました。以前パッケージを使って実現しましたが、標準のWidgetでできるんですね!
参考
「スライドして削除」に取り組もうとしたきっかけのページ
- 【Flutter】Dismissibleの使い方(リストをスライドして削除する)
DismissDirectionの方向について、詳しい。
全ソース
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
),
home: const MyHomePage(title: 'Flutter Demo: swipe to dismiss'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static const urlPrefix =
'https://docs.flutter.dev/cookbook/img-files/effects/parallax';
final urls = [
'$urlPrefix/01-mount-rushmore.jpg',
'$urlPrefix/02-singapore.jpg',
'$urlPrefix/03-machu-picchu.jpg',
'$urlPrefix/04-vitznau.jpg',
'$urlPrefix/05-bali.jpg',
'$urlPrefix/06-mexico-city.jpg',
'$urlPrefix/07-cairo.jpg',
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: ListView.builder(
itemCount: urls.length,
itemBuilder: (context, index) {
var url = urls[index];
return Padding(
padding: const EdgeInsets.all(8.0),
child: Dismissible(
key: ValueKey(url),
onDismissed: (direction) {
setState(() {
urls.removeAt(index);
});
},
direction: DismissDirection.startToEnd,
confirmDismiss: (direction) async {
var result = await showDialog<bool>(
context: context,
builder: (BuildContext context) => AlertDialog(
content: const Text('Would you delete it?'),
actions: <Widget>[
SimpleDialogOption(
child: const Text('Cancel'),
onPressed: () => Navigator.pop(context, false),
),
SimpleDialogOption(
child: const Text('Delete'),
onPressed: () => Navigator.pop(context, true),
),
],
),
);
return Future.value(result);
},
background: Container(
color: Colors.black26,
child: const Align(
alignment: Alignment.centerLeft,
child: Icon(
Icons.delete,
color: Colors.white,
size: 48,
),
),
),
dismissThresholds: const {DismissDirection.startToEnd: 0.4},
child: AspectRatio(
aspectRatio: 16 / 9,
child: Image.network(
url,
fit: BoxFit.fitWidth,
)),
),
);
},
),
);
}
}