Main featured image

FlutterのListViewの中にPageViewで画像のカルーセルを作る(縦横入れ子のNested Listの作り方)

Flutter
Dart

Flutter の ListView のリストの中に PageView で画像のカルーセルを作ります。

縦の ListView の中に横の PageView を表示する縦横入れ子の Nested List です。

完成後はこのような動きになります。

iOS や Android のネイティブで入れ子のリストを実装しようとすると結構大変なのですが、Flutter だと割と簡単に実装できました。

それでは見ていきましょう。

環境
  • macOS Big Sur 11.1
  • Android Studio 4.1.2
  • Flutter 1.22.6
  • Dart 2.10.5
package を追加する

pubspec.yamlenglish_words package を追加します。

dependencies:
  flutter:
    sdk: flutter
  english_words: # added

english_words はランダムに英単語を出力してくれる package です。

追加後は忘れずに flutter pub get を実行しましょう。

ListView と PageView を実装する

以下ファイルを作成してください。

  • lib/nested_list_screen.dart

実装内容は以下となります。

import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';

class NestedListScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(primaryColor: Colors.white),
      home: NestedList(),
    );
  }
}

class NestedList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Nested List'),
      ),
      body: ListView.builder(
        padding: const EdgeInsets.symmetric(vertical: 16),
        itemBuilder: _buildVerticalItem,
      ),
    );
  }

  Widget _buildVerticalItem(BuildContext context, int verticalIndex) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
      child: SizedBox(
        height: 320,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              WordPair.random().asPascalCase,
              style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
            ),
            Text(
              WordPair.random().asPascalCase,
              style: const TextStyle(fontSize: 16, color: Colors.grey),
            ),
            _buildHorizontalItem(context, verticalIndex),
          ],
        ),
      ),
    );
  }

  Widget _buildHorizontalItem(BuildContext context, int verticalIndex) {
    return SizedBox(
      height: 240,
      child: PageView.builder(
        controller: PageController(viewportFraction: 0.8),
        itemBuilder: (context, horizontalIndex) =>
            _buildHorizontalView(context, verticalIndex, horizontalIndex),
      ),
    );
  }

  Widget _buildHorizontalView(
      BuildContext context, int verticalIndex, int horizontalIndex) {
    final imageUrl =
        'https://source.unsplash.com/random/275x240?sig=$verticalIndex$horizontalIndex';
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 8),
      child: Card(
        child: Image.network(imageUrl),
      ),
    );
  }
}

まず、ListView.builder を実装します。

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Nested List'),
      ),
      body: ListView.builder(
        padding: const EdgeInsets.symmetric(vertical: 16),
        itemBuilder: _buildVerticalItem,
      ),
    );
  }

次に itemBuilder に指定している縦リストの要素である _buildVerticalItem メソッドを実装します。

  Widget _buildVerticalItem(BuildContext context, int verticalIndex) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
      child: SizedBox(
        height: 320,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              WordPair.random().asPascalCase,
              style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
            ),
            Text(
              WordPair.random().asPascalCase,
              style: const TextStyle(fontSize: 16, color: Colors.grey),
            ),
            _buildHorizontalItem(context, verticalIndex),
          ],
        ),
      ),
    );
  }

ListView の縦リストのレイアウトを実装しています。

Padding で縦要素全体の padding、 SizedBox で縦要素全体の高さを決定します。

Column で表示する要素を縦に並べて、 MainAxisAlignment.spaceEvenly で要素を均等に並べます。

表示ラベルには WordPair.random().asPascalCase でランダムな英単語を生成して表示しています。

次に横リストである _buildHorizontalItem メソッドを実装します。

  Widget _buildHorizontalItem(BuildContext context, int verticalIndex) {
    return SizedBox(
      height: 240,
      child: PageView.builder(
        controller: PageController(viewportFraction: 0.8),
        itemBuilder: (context, horizontalIndex) =>
            _buildHorizontalView(context, verticalIndex, horizontalIndex),
      ),
    );
  }

ここでは横リスト要素全体の高さを SizeBox で決定します。

次に横リスト本体である、画像カルーセル部分の PageView の controller に PageController を指定します。

コンストラクタで指定している viewportFraction: 0.8 は、隣同士の画像の端をどれくらい画面に表示するかの割合です。

posted image

赤枠の部分です。

この数字が小さければ、表示中画像の隣の画像がより広く表示されます。

この調整が iOS や Android ネイティブだと難しくて、Flutter は一発で出来るので感動しました。

最後に PageView に表示する横リストの要素である _buildHorizontalView メソッドを実装します。

  Widget _buildHorizontalView(
      BuildContext context, int verticalIndex, int horizontalIndex) {
    final imageUrl =
        'https://source.unsplash.com/random/275x240?sig=$verticalIndex$horizontalIndex';
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 8),
      child: Card(
        child: Image.network(imageUrl, fit: BoxFit.cover),
      ),
    );
  }

カルーセルの画像表示部分です。

Padding で隣同士の画像の padding を指定します。

画像カルーセルをキレイに表示する為、この padding 値と、viewportFraction の値を同時に微調整しました。

ランダム画像は https://source.unsplash.com/random/ から取得しています。

Image.network の第二引数に fit: BoxFit.cover を指定して画像を親 Widget(今回でいう Padding)に収めるようにします。

筆者が BoxFit で主に使用するパラメータは containcover です。

  • contain
    • 画像の縦横比は変えずに親をはみ出さない範囲で最大の大きさまで拡大縮小する
  • cover
    • 画像の縦横比は変えずに親を埋めていない部分ができない範囲で最小の大きさまで拡大縮小する

こちらはお好みで設定してください。

表示する画像サイズを変える場合は画像表示領域に合わせて padding と viewportFraction を調整しましょう。

後は main.dart の runApp に 実装した NestedListScreen を指定します。

import 'package:flutter/material.dart';

import 'nested_list_screen.dart';

void main() {
  runApp(NestedListScreen());
}
おわりに

今回 ListView で縦リスト、PageView で横リスト(画像カルーセル)の Nested List ができました。

実装していてプロダクトに導入する時はパフォーマンスを考慮した設計が必要と感じました。

入れ子の Nested List は build する Widget が多くなるので、画面スクロール時の build パフォーマンスを考えて実装しないと UX が悪くなると思います。

次回以降パフォーマンスについては調査していきます。

最後に今回作成したアプリのソースコードは Github にもあるので参照ください。

Written by ZUMA a.k.a. Kazuma. Web/Mobile App developer.  My profile.
Tags
Archives
2021-072021-062021-052021-042021-032021-022021-01
Recent Posts