【簡単】並び替えのできるリストReorderableListViewとそのプロパティ

ListViewでリストを作成しているときに、並び替え機能を追加したくなることはありませんか?
ReorderableListViewは、ユーザーがリスト項目をドラッグアンドドロップすることで、
並び替えることができるFlutterのウィジェットです。

今回はReorderableListViewと設定できるプロパティについてご紹介します。

ご紹介
《公開中アプリのご紹介》
vLIST
vLIST
スマートなチェックリストアプリ
Google Playでダウンロード

基本的なReorderableListViewの使い方

以下に基本的なReorderableListViewの実装コードを示します。
下記の例では、リストに含まれる項目をドラッグして順序を変更できます。

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 const MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'ReorderableListView Example',
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final List<String> _items = List.generate(20, (index) => "Item $index");

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ReorderableListView Example'),
      ),
      body: ReorderableListView.builder(
        itemCount: _items.length,
        itemBuilder: (context, index) {
          return ListTile(
            key: ValueKey(_items[index]),
            title: Text(_items[index]),
            trailing: const Icon(Icons.drag_handle),
          );
        },
        onReorder: (oldIndex, newIndex) {
          setState(() {
            if (newIndex > oldIndex) {
              newIndex -= 1;
            }
            final item = _items.removeAt(oldIndex);
            _items.insert(newIndex, item);
          });
        },
      ),
    );
  }
}

ReorderableListView.builderを使用して、20個のリストアイテムを表示しています。

ReorderableListViewのプロパティ

ReorderableListViewには、リストをカスタマイズするための多くのプロパティがあります。
使えるプロパティはListViewと似ていますが、若干異なるようです。

itemBuilder(必須)

itemBuilderは各アイテムを生成するプロパティです。
受け取ったリストアイテムを1つずつ処理して表示されるウィジェットを返します。

itemCount: _items.length,
itemBuilder: (context, index) {
  return ListTile(
    key: ValueKey(_items[index]),
    title: Text(_items[index]),
    trailing: const Icon(Icons.drag_handle),
  );
},

itemCount(必須)

itemCountはリストのアイテム数を指定するプロパティです。
ReorderableListViewではitemCountプロパティは必須です。

itemCount: _items.length,

onReorder(必須)

リストの項目が再配置されたときに呼び出されるコールバック関数です。
onReorderはユーザーがドロップした際に、実行されます。
以下は基本的なonReorderの処理ですが、説明しやすいようにコードの順序を変えています。

onReorder: (oldIndex, newIndex) {
  setState(() {
    // リストからoldIndexのItemを削除&取得します
    final item = _items.removeAt(oldIndex);
    // すると、oldIndex以降のIndexが1つズレるため、
    // oldIndexよりnewIndexが大きい場合はnewIndexに-1する必要があります
    if (newIndex > oldIndex) {
      newIndex -= 1;
    }
    // 先ほど削除した際に取得したItemをnewIndexに挿入します
    _items.insert(newIndex, item);
  });
},

itemExtent(任意)

各項目の固定高さ(縦幅)を指定できます。
設定すると、各項目の高さが統一されるため、スクロール性能が向上します。初期値はnullです。

itemExtent: 30.0,

prototypeItem(任意)

リストのレイアウトが提供したウィジェットに基づいて計算されます。
これにより、レイアウト計算のオーバーヘッドが減少します。
固定のサイズ(内容)のリストを利用する時に使えます。

prototypeItem: ListTile(
  title: Text('Sample Item'),
),

proxyDecorator(任意)

ドラッグ中の項目の見た目を変更できます。
以下はドラッグ中の項目が半透明になります。

proxyDecorator: (child, index, animation) {
  return Material(
    elevation: 6.0,
    color: Colors.transparent,
    shadowColor: Colors.black,
    child: child,
  );
},

buildDefaultDragHandles(任意)

(PCやスマホの)デフォルトのドラッグハンドルを使用するかどうかを指定することができます。
以下はデフォルトではなく、ReorderableDragStartListenerでハンドリングしています。

buildDefaultDragHandles: false,
itemBuilder: (context, index) {
  return ListTile(
    key: ValueKey(_items[index]),
    title: Text(_items[index]),
    trailing: ReorderableDragStartListener(
      index: index,
      child: Icon(Icons.drag_handle),
    ),
  );
},

padding(任意)

様々なWidgetにある、おなじみのプロパティです。リスト全体にパディングが追加されます。
以下のように設定します。

padding: const EdgeInsets.only(left: 15, right: 15, top: 5, bottom: 5),

header(任意)

リストの先頭に表示するウィジェット。リストのタイトルとかに付けるには良いかも!?
リストの1要素なので、スクロールすると消えます。
常に表示させたい場合は、リストの前にWidgetを追加するかSliverListを利用する必要があります。

header: Padding(
  padding: const EdgeInsets.all(16.0),
  child: Text(
    'リストのヘッダー',
    style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
  ),
),

footer(任意)

リストの末尾に表示するウィジェット。使い方はheaderと同様です。

footer: Padding(
  padding: const EdgeInsets.all(16.0),
  child: Text(
    'リストのフッター',
    style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
  ),
),

scrollDirection(任意)

scrollDirectionはリストのスクロール方向を指定します。
垂直方向(Axis.vertical)、水平方向(Axis.horizontal)に設定することができます。
デフォルトでは垂直方向(Axis.vertical)です。

scrollDirection: Axis.horizontal,

reverse(任意)

reverseはリストの並びを逆順にすることができます。
trueに設定すると、リストの表示順序が逆になります。デフォルトはfalseです。

reverse:true,

scrollController(任意)

プログラムからリストビューのスクロールを細かく制御できます。

scrollController: _scrollController,

以下はTopボタンを押下すると先頭に移動する機能をもったコードです。

class _MainState extends State<HomePage> {
  final List<String> _items = List.generate(20, (index) => "Item $index");
  final ScrollController _scrollController = ScrollController();

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(() {
      print("Scroll position: ${_scrollController.position.pixels}");
    });
  }

  void _scrollToTop() {
    _scrollController.animateTo(
      0.0,
      duration: Duration(seconds: 1),
      curve: Curves.easeInOut,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('先頭へ'),
        actions: [
          IconButton(
            icon: Icon(Icons.arrow_upward),
            onPressed: _scrollToTop,
          ),
        ],
      ),
      body: ReorderableListView.builder(
        itemCount: _items.length,
        itemBuilder: (context, index) {
          return ListTile(
            key: ValueKey(_items[index]),
            title: Text(_items[index]),
            trailing: ReorderableDragStartListener(
              index: index,
              child: Icon(Icons.drag_handle),
            ),
          );
        },
        scrollController: _scrollController,
        onReorder: (oldIndex, newIndex) {
          setState(() {
            if (newIndex > oldIndex) {
              newIndex -= 1;
            }
            final item = _items.removeAt(oldIndex);
            _items.insert(newIndex, item);
          });
        },
      ),
    );
  }
}

primary(任意)

例えば入れ子のスクロールビューの場合、子のリストに「primary: false」を設定すると、
子のリストのスクロールを優先することができます。
逆に設定しない場合は、親のリストのスクロールが優先されてしまうため、
子のリストのスクロールがスムーズにできなくなります。デフォルトはtrueです。

primary: false,

physics(任意)

スクロールの挙動を設定できます。
標準のスクロールクラスは以下です。デフォルトはClampingScrollPhysicsです。

  • BouncingScrollPhysics:
    リストの端に到達すると、スクロールが反発
  • ClampingScrollPhysics:
    リストの端に到達すると、スクロールが固定
  • FixedExtentScrollPhysics:
    スクロールが一定の高さで位置付けられる動きをする
  • NeverScrollableScrollPhysics:
    スクロールができなくなる
  • PageScrollPhysics:
    ページビューのようなスクロール横スクロールと組み合わせると便利かも
physics: BouncingScrollPhysics(),

shrinkWrap(任意)

shrinkWrapはリストが必要なだけのサイズを取るかどうかを指定します。
trueに設定すると、リストはその内容に基づいて自身の高さを決定します。
デフォルトはfalseです。

shrinkWrap: true,

anchor(任意)

リストの表示位置を設定できます。
設定すると最初のアイテムの上にスペースができます。範囲は0~1です。
(どういった時に利用できるかイメージがつきません…)

anchor: 0.1,

cacheExtent(任意)

スクロールビューが表示範囲の外にどれだけのウィジェットをキャッシュするかを制御できます。
デフォルトでは、非常に少量しかキャッシュしないため、スクロールパフォーマンスを改善するため、
利用することがあるかもしれません。

ただ、デフォルトでも十分早いので、少量でサイズの小さいリストではあまり必要ないかも。

dragStartBehavior(任意)

ドラッグ操作のタッチイベントの開始時動作を制御できます。

  • DragStartBehavior.start:
    ドラッグ動作は、ユーザーが指を動かし始めた瞬間から開始されます
    アニメーションはこちらの方がスムーズ。デフォルトはこちら。
  • DragStartBehavior.down:
    ダウン イベントが最初に検出された位置から開始されます
    ドラッグ動作の反応はこちらの方が若干良くなります
dragStartBehavior: DragStartBehavior.down,

ただ体感としてはどっちも同じでした。気になる場合は設定を検討してください。

keyboardDismissBehavior(任意)

ユーザーがスクロールしたときにキーボードを閉じるかどうかの挙動を設定できます。

  • ScrollViewKeyboardDismissBehavior.manual:
    ユーザーがスクロールしても、キーボードは自動的に閉じません
    デフォルトはこちらが設定されています
  • ScrollViewKeyboardDismissBehavior.onDrag:
    ユーザーがスクロールすると、キーボードが自動的に閉じます
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,

restorationId(任意)

リストの状態復元のためのIDを設定できます。
スクロール位置や入力内容など、ユーザーのセッション中の状態を保存し、
アプリケーションの再起動や再生成時にその状態を復元することができます。
デフォルトでは設定されていません。
詳しくはFlutter公式(リンク)を参照ください。

restorationId: 'reorderable_list', // このIDを使ってリストアなどの処理を実装する

clipBehavior(任意)

ウィジェットの境界を超える部分をどのように切り取るを制御するために使用されます。
デフォルトはClip.hardEdgeが設定されています。

  • Clip.none:
    親の境界を超えて描画されるのを許可します
  • Clip.hardEdge:
    親の境界を超える部分は表示されません
  • Clip.antiAlias:
    親の境界を超える部分をアンチエイリアス処理(被った部分を良い感じする処理)を行います
    より滑らかなエッジになりますが、パフォーマンスに影響を与える場合があります
  • Clip.antiAliasWithSaveLayer:
    親の境界を超える部分をアンチエイリアス処理を行います
    高品質のレンダリングを提供しますが、パフォーマンスコストが高くなります
clipBehavior: Clip.antiAlias,

Clip.antiAliasClip.antiAliasWithSaveLayerはテキストのリストではあまり効果を
発揮しないかもしれませんが、画像や図を用いたリストだと有効かもしれません。

まとめ

並び替え可能なReorderableListView.builderをご紹介しました。
プロパティを設定することで、かなり挙動をカスタマイズできそうですね。
どのようなプロパティがあるか知っておくだけで、実装の幅が広がりそうです。

タイトルとURLをコピーしました