😀

[Flutter] 各要素を並び替えできる ListView & GridView を使ってみる

2022/03/21に公開

成果物

デモ

https://popy1017.github.io/flutter_reorder_list_sample/

ソースコード

https://github.com/popy1017/flutter_reorder_list_sample

要素を並び替えられる ListView

デフォルトで入っているReorderableListView&SliverReorderableListを使います。

https://api.flutter.dev/flutter/material/ReorderableListView-class.html

ReorderableListView の使い方

  • 基本的にはListViewと同じ使い方
  • itemBuilderには並べたい要素を生成する関数を指定します。
  • ListViewと異なる点としては、各要素にユニークなKeyを指定する必要があることです。
  • onReorderには並び替えが完了したとき(ドラッグしている要素を放したとき)に行う処理を記載します。
  • proxyDecoratorは任意に指定可能で、ドラッグしている要素をデコレーションできます(今回のコードでは、ドラッグする要素を半透明にしています。
reordable_list_view_page.dart
class ReorderableListViewPage extends ConsumerWidget {
  const ReorderableListViewPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    final items = ref.watch(itemsProvider);

    return Scaffold(
      appBar: AppBar(),
      body: ReorderableListView.builder(
        itemBuilder: (_, index) => ItemTile(
          items[index],
          key: Key('$index'), // 各要素にユニークなKeyをつける必要がある
        ),
        itemCount: items.length,
        onReorder: (int oldIndex, int newIndex) {
          _onReorder(items, oldIndex, newIndex);
        },
        proxyDecorator: (widget, _, __) {
          return Opacity(opacity: 0.5, child: widget);
        },
      ),
    );
  }

  void _onReorder(List<Item> items, int oldIndex, int newIndex) {
    if (oldIndex < newIndex) {
      newIndex -= 1;
    }
    items.insert(newIndex, items.removeAt(oldIndex));
  }
}

ReorderableListView(children: [])という書き方もできます。

SliverReorderableList の使い方

  • ReorderableListViewと異なり、並べたい要素をReorderableDragStartListenerまたはReorderableDelayedDragStartListenerでラップする必要があります。
  • その影響で、ユニークなkeyを付与する対処も異なるので注意。
            itemBuilder: (_, index) => ReorderableDelayedDragStartListener(
              index: index,
              // ItemTileではなく、ReorderableDelayedDragStartListenerにkeyを付与
              key: Key('$index'), 
              child: ItemTile(items[index]),
            ),
  • ReorderableDragStartListenerReorderableDelayedDragStartListenerの違いはドラッグ開始までのタップ時間。ReorderableDragStartListenerはほぼタップした瞬間にドラッグが始まりますが、ReorderableDelayedDragStartListenerは長押ししたらドラッグが始まります。
  • ReorderableDragStartListenerは画面全体のスクロールがしづらくなってしまうため、要素が多くスクロールが必要な場合はReorderableDelayedDragStartListenerを使うのがおすすめです。
  • ReorderableListViewと同様、proxyDecoratorを指定することでドラッグ中の要素をデコレーションできます。
class SliverReorderableListPage extends ConsumerWidget {
  const SliverReorderableListPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    final items = ref.watch(itemsProvider);

    return Scaffold(
      body: CustomScrollView(
        slivers: [
          const SliverAppBar(),
          SliverReorderableList(
            // ReorderableListViewと異なり、
            // 各要素を ReorderableDragStartListener または ReorderableDelayedDragStartListener
            // でラップする必要がある。
            // その影響もあり、keyを付与する対象は ReorderableDragStartListener または
            // ReorderableDragStartListenerになるので注意。
            //
            // ReorderableDragStartListenerはタップで移動が開始されるが、
            // ReorderableDelayedDragStartListenerはロングタップで移動開始となる。
            // タップで要素の移動が始まってしまうと
            // スクロールがしづらくなるので ReorderableDelayedDragStartListener のほうがいい
            itemBuilder: (_, index) => ReorderableDelayedDragStartListener(
              index: index,
              key: Key('$index'),
              child: ItemTile(items[index]),
            ),
            itemCount: items.length,
            onReorder: (int oldIndex, int newIndex) {
              _onReorder(items, oldIndex, newIndex);
            },
            proxyDecorator: (widget, _, __) {
              return Opacity(opacity: 0.5, child: widget);
            },
          ),
        ],
      ),
    );
  }

  void _onReorder(List<Item> items, int oldIndex, int newIndex) {
    if (oldIndex < newIndex) {
      newIndex -= 1;
    }
    items.insert(newIndex, items.removeAt(oldIndex));
  }
}

並べ替えできるGridView

以下のパッケージを使いました。
使い方もReorderableListViewとほとんど同じで、非公式とは思えないほど使いやすかったです。
Sliver対応のSliverReorderableGridクラスもあります。

https://pub.dev/packages/reorderable_grid

ReorderableGridView の使い方

  • ほとんどReorderableListViewと同じ。
  • itemBuilderに並べたい要素を返す関数を指定し、各要素にはユニークなkeyを指定します。
  • コードにはないですが、こちらもproxyDecoratorを指定することでドラッグ中の要素の見た目を変えることができます。
  • gridDelegateにはGridViewでおなじみのSliverGridDelegateWithFixedCrossAxisCountを指定。(他にもあった気がします)
  • ReorderableListViewと同じく、.builderを使わない書き方もOK。
class ReorderableGridViewSample extends ConsumerWidget {
  const ReorderableGridViewSample({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    final items = ref.watch(itemsProvider);

    return Scaffold(
      appBar: AppBar(),
      body: ReorderableGridView.builder(
        itemBuilder: (_, index) => ItemCard(
          items[index],
          key: Key('$index'), // 各要素にユニークなKeyをつける必要がある
        ),
        itemCount: items.length,
        onReorder: (int oldIndex, int newIndex) =>
            _onReorder(items, oldIndex, newIndex),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: kIsWeb ? 4 : 2,
        ),
      ),
    );
  }

  void _onReorder(List<Item> items, int oldIndex, int newIndex) {
    final item = items.removeAt(oldIndex);
    items.insert(newIndex, item);
  }
}

SliverReorderableGrid の使い方

  • こちらもほぼ SliverReorderableList と同じ。すごい。
  • ドラッグリスナーとしてはReorderableGridDragStartListenerまたは ReorderableGridDelayedDragStartListenerが用意されており、各要素をいずれかでラップする。
  • ユニークなkeyは並べたい要素自体(ここではItemCard)ではなく上記のいずれかのリスナーに付ける必要があるので注意。
class SliverReorderableGridPage extends ConsumerWidget {
  const SliverReorderableGridPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    final items = ref.watch(itemsProvider);

    return Scaffold(
      body: CustomScrollView(
        slivers: [
          const SliverAppBar(),
          SliverReorderableGrid(
            // SliverReorderableListと同様、こちらも各要素を
            // ReorderableGridDragStartListener または ReorderableGridDelayedDragStartListenerで
            // ラップする必要がある。
            itemBuilder: (_, index) => ReorderableGridDelayedDragStartListener(
              index: index,
              key: Key('$index'),
              child: ItemCard(
                items[index],
                key: Key('$index'), // 各要素にユニークなKeyをつける必要がある
              ),
            ),
            itemCount: items.length,
            onReorder: (int oldIndex, int newIndex) =>
                _onReorder(items, oldIndex, newIndex),
            gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: kIsWeb ? 4 : 2,
            ),
            proxyDecorator: (widget, _, __) {
              return Opacity(opacity: 0.5, child: widget);
            },
          ),
        ],
      ),
    );
  }

  void _onReorder(List<Item> items, int oldIndex, int newIndex) {
    final item = items.removeAt(oldIndex);
    items.insert(newIndex, item);
  }
}

不具合?

  • そんなに大したことではありませんが、ドラッグ中に元の位置に戻せないです(v1.0.3時点)。

Discussion