Flutter onPageChanged でスムーズなページングを実現!実装方法と活用事例

はじめに:Flutter onPageChangedとは

FlutterにおけるonPageChangedは、PageViewウィジェットのページが変更された際に発火するコールバック関数です。PageViewは、ユーザーが左右にスワイプすることで複数のページを切り替えられるウィジェットで、アプリのチュートリアル画面や画像ギャラリー、タブ形式のコンテンツ表示など、様々な場面で利用されます。

onPageChangedを使用することで、現在表示されているページ番号を取得したり、ページ遷移に合わせてアニメーションを制御したり、ページインジケーターを更新したりといった処理を行うことができます。つまり、onPageChangedは、PageViewを使ったページング機能をよりインタラクティブで使いやすくするための重要な要素なのです。

このコールバック関数を適切に活用することで、ユーザーエクスペリエンス(UX)を大幅に向上させることが可能です。例えば、ページが変わる際にスムーズなアニメーションを追加したり、現在のページ位置を視覚的に示すインジケーターを表示したりすることで、ユーザーは自分がどのページにいるのか、あとどれくらいのコンテンツがあるのかを直感的に理解できるようになります。

本記事では、onPageChangedの基本的な使い方から、具体的な実装例、パフォーマンスの最適化まで、詳しく解説していきます。onPageChangedをマスターして、より洗練されたFlutterアプリを開発しましょう。

PageViewとPageControllerの基本

onPageChangedを理解するためには、まずPageViewPageControllerについて理解しておく必要があります。

PageViewとは

PageViewは、複数のウィジェット(ページ)を横または縦方向にスクロール可能なリストとして表示するためのウィジェットです。ユーザーはスワイプ操作によってページを切り替えることができます。

PageViewにはいくつかの種類があります。

  • PageView: 標準的なPageViewです。
  • PageView.builder: 大量のページを効率的に生成する場合に使用します。必要なページのみをレンダリングするため、メモリ使用量を抑えることができます。
  • PageView.custom: 独自のページ遷移ロジックを実装する場合に使用します。

PageViewは、childrenプロパティにウィジェットのリストを受け取ります。これらのウィジェットが個別のページとして表示されます。

PageControllerとは

PageControllerは、PageViewのスクロール動作を制御するためのコントローラです。PageViewの初期ページを設定したり、指定したページにアニメーション付きで移動したり、現在のページ番号を取得したりすることができます。

PageControllerPageViewcontrollerプロパティに渡されます。

基本的な使い方:

final PageController _pageController = PageController(
  initialPage: 0, // 初期ページ(デフォルトは0)
);

PageView(
  controller: _pageController,
  children: [
    Container(color: Colors.red),
    Container(color: Colors.green),
    Container(color: Colors.blue),
  ],
)

PageControllerの主な機能:

  • initialPage: 初期表示するページを指定します。
  • viewportFraction: ページの一部を表示し、隣接するページを部分的に見せることで、スクロール可能なことを視覚的に表現できます。
  • animateToPage: 指定したページにアニメーション付きで移動します。
  • jumpToPage: 指定したページに瞬時に移動します。
  • page: 現在表示されているページ番号を取得します(double型)。

PageControllerPageViewを組み合わせることで、高度なページング機能を実装することができます。onPageChangedは、PageControllerによって制御されるPageViewの状態変化を検知するために使用されます。

onPageChanged の実装方法:基本的な使い方

onPageChangedは、PageViewウィジェットのプロパティの一つであり、ページが切り替わったときに呼び出される関数です。この関数は、現在のページのインデックス(0から始まる整数)を引数として受け取ります。

基本的な実装例:

import 'package:flutter/material.dart';

class MyPageView extends StatefulWidget {
  @override
  _MyPageViewState createState() => _MyPageViewState();
}

class _MyPageViewState extends State<MyPageView> {
  final PageController _pageController = PageController();
  int _currentPage = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('onPageChanged Example'),
      ),
      body: PageView(
        controller: _pageController,
        children: [
          Container(
            color: Colors.red,
            child: Center(child: Text('Page 1')),
          ),
          Container(
            color: Colors.green,
            child: Center(child: Text('Page 2')),
          ),
          Container(
            color: Colors.blue,
            child: Center(child: Text('Page 3')),
          ),
        ],
        onPageChanged: (int page) {
          setState(() {
            _currentPage = page;
          });
          print('Current Page: $page');
        },
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentPage,
        onTap: (int index) {
          _pageController.animateToPage(
            index,
            duration: Duration(milliseconds: 300),
            curve: Curves.easeInOut,
          );
        },
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Page 1'),
          BottomNavigationBarItem(icon: Icon(Icons.business), label: 'Page 2'),
          BottomNavigationBarItem(icon: Icon(Icons.school), label: 'Page 3'),
        ],
      ),
    );
  }
}

解説:

  1. _pageControllerの作成: PageControllerを初期化し、PageViewcontrollerプロパティに渡します。
  2. onPageChangedの定義: PageViewonPageChangedプロパティに、ページが切り替わった際に実行される関数を定義します。
  3. setStateによる状態更新: onPageChanged内でsetStateを呼び出し、現在のページ番号を保持する_currentPage変数を更新します。これにより、UIが再描画され、ページ番号の変化が反映されます。
  4. ページ番号の表示: print('Current Page: $page'); でコンソールに現在のページ番号を出力しています。 実際には、このpageの値を使って、インジケーターを更新したり、別のアクションを実行したりします。
  5. ボトムナビゲーションの実装例: BottomNavigationBarを使って、各ページに直接移動できるボタンを配置しています。onTap_pageController.animateToPageを呼び出し、指定したページにアニメーション付きで移動しています。

ポイント:

  • onPageChangedは、ページが完全に切り替わった後にのみ呼び出されます。スワイプ中に何度も呼び出されるわけではありません。
  • setStateを使用することで、UIを再描画し、ページ番号の変化を反映させます。
  • onPageChangedの引数pageは、現在のページのインデックス(0から始まる整数)です。

この基本的な実装を理解することで、onPageChangedを様々な場面で活用できるようになります。次のセクションでは、onPageChangedの引数について詳しく見ていきましょう。

onPageChanged の引数:現在のページ番号の取得

onPageChangedの引数として渡されるpageは、int型の整数で、現在表示されているページのインデックスを表します。インデックスは0から始まるため、最初のページは0、次のページは1、その次は2、…となります。

このpageの値を取得することで、以下のような処理を行うことができます。

  • 現在のページ番号を表示する: 画面上に「現在xページ / 全yページ」のような形でページ番号を表示する。
  • 特定ページのコンテンツを読み込む: pageの値に基づいて、APIから該当ページのコンテンツを読み込む。
  • ページインジケーターを更新する: ドットや線で現在位置を示すページインジケーターを更新する。
  • 特定のページに到達したときにイベントを発火する: 例えば、最後のページに到達したときにチュートリアル完了のダイアログを表示する。

例:現在のページ番号を表示する

import 'package:flutter/material.dart';

class MyPageView extends StatefulWidget {
  @override
  _MyPageViewState createState() => _MyPageViewState();
}

class _MyPageViewState extends State<MyPageView> {
  final PageController _pageController = PageController();
  int _currentPage = 0;
  final int _totalPages = 3; // 総ページ数

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('onPageChanged Example'),
      ),
      body: Column(
        children: [
          Expanded(
            child: PageView(
              controller: _pageController,
              children: [
                Container(
                  color: Colors.red,
                  child: Center(child: Text('Page 1')),
                ),
                Container(
                  color: Colors.green,
                  child: Center(child: Text('Page 2')),
                ),
                Container(
                  color: Colors.blue,
                  child: Center(child: Text('Page 3')),
                ),
              ],
              onPageChanged: (int page) {
                setState(() {
                  _currentPage = page;
                });
              },
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Text('現在 ${ _currentPage + 1 } / $_totalPages ページ'),
          ),
        ],
      ),
    );
  }
}

解説:

  • _totalPages変数で総ページ数を定義しています。
  • onPageChanged内で_currentPageを更新しています。
  • Text('現在 ${ _currentPage + 1 } / $_totalPages ページ')で、現在のページ番号を画面に表示しています。 _currentPageは0から始まるため、表示する際は+1しています。

注意点:

  • pageの値は、ページ遷移が完了した後に更新されます。スワイプ中にリアルタイムに更新されるわけではありません。
  • pageの値は、PageView.builderを使用している場合でも同様に、表示されているページのインデックスを表します。

pageの値を効果的に活用することで、ユーザーに分かりやすく、インタラクティブなページング体験を提供することができます。次のセクションでは、onPageChangedとアニメーションを連携させる方法について見ていきましょう。

アニメーションと連携:スムーズなページ遷移

onPageChangedを利用することで、ページ遷移に合わせて様々なアニメーションを実装し、ユーザーエクスペリエンスを向上させることができます。

1. フェードイン・フェードアウトアニメーション:

ページが切り替わる際に、現在のページをフェードアウトさせ、新しいページをフェードインさせるアニメーションです。

import 'package:flutter/material.dart';

class MyPageView extends StatefulWidget {
  @override
  _MyPageViewState createState() => _MyPageViewState();
}

class _MyPageViewState extends State<MyPageView> {
  final PageController _pageController = PageController();
  int _currentPage = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Fade Animation Example'),
      ),
      body: PageView(
        controller: _pageController,
        children: [
          FadeAnimatedContainer(color: Colors.red, pageIndex: 0, currentPage: _currentPage),
          FadeAnimatedContainer(color: Colors.green, pageIndex: 1, currentPage: _currentPage),
          FadeAnimatedContainer(color: Colors.blue, pageIndex: 2, currentPage: _currentPage),
        ],
        onPageChanged: (int page) {
          setState(() {
            _currentPage = page;
          });
        },
      ),
    );
  }
}

class FadeAnimatedContainer extends StatelessWidget {
  final Color color;
  final int pageIndex;
  final int currentPage;

  const FadeAnimatedContainer({
    Key? key,
    required this.color,
    required this.pageIndex,
    required this.currentPage,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return AnimatedOpacity(
      opacity: pageIndex == currentPage ? 1.0 : 0.0,
      duration: Duration(milliseconds: 300),
      child: Container(
        color: color,
        child: Center(child: Text('Page ${pageIndex + 1}')),
      ),
    );
  }
}

解説:

  • FadeAnimatedContainerというカスタムウィジェットを作成し、AnimatedOpacityを使ってフェードイン・フェードアウトのアニメーションを実現しています。
  • pageIndexcurrentPageを比較し、opacityを切り替えることで、現在のページのみを表示するようにしています。

2. スケールアニメーション:

ページが切り替わる際に、現在のページを縮小させ、新しいページを拡大させるアニメーションです。

同様にTransform.scaleAnimatedScaleを組み合わせて実装することができます。onPageChanged_currentPageを更新し、pageIndexcurrentPageを比較して、スケール値を調整します。

3. トランスレーションアニメーション:

ページが切り替わる際に、ページを左右にスライドさせるアニメーションです。

Transform.translateAnimatedPositionedを組み合わせて実装することができます。

4. カスタムアニメーション:

onPageChangedでアニメーションコントローラを操作し、より複雑なアニメーションを実装することも可能です。

ポイント:

  • AnimatedOpacityTransform.scaleTransform.translateAnimatedPositionedなどのアニメーションウィジェットを活用することで、簡単にアニメーションを実装できます。
  • onPageChangedsetStateを呼び出し、アニメーションの状態を更新します。
  • AnimationControllerを使うことで、より高度なアニメーションを制御できます。
  • アニメーションのdurationを調整することで、アニメーションの速度を調整できます。
  • 適切なアニメーションを実装することで、ユーザーにスムーズで快適なページング体験を提供できます。

アニメーションを効果的に活用することで、PageViewの使いやすさを向上させることができます。次のセクションでは、onPageChangedを使ってページインジケーターを実装する方法について見ていきましょう。

インジケーターの実装:現在のページを視覚的に表示

ページインジケーターは、現在表示されているページが全体の何ページ中何ページ目なのかを視覚的に示すUI要素です。これにより、ユーザーはコンテンツの全体像を把握しやすくなり、より快適な操作が可能になります。onPageChangedを活用することで、このインジケーターを動的に更新することができます。

一般的なインジケーターの種類:

  • ドットインジケーター: 現在のページに対応するドットを強調表示するシンプルなインジケーター。
  • ラインインジケーター: 現在のページに対応するラインを強調表示するインジケーター。
  • 数値インジケーター: 現在のページ番号と総ページ数を数字で表示するインジケーター (例: 1/5, 2/5, …)。
  • プログレスバーインジケーター: スクロール位置に応じて進捗状況を示すプログレスバー。

ドットインジケーターの実装例:

import 'package:flutter/material.dart';

class MyPageView extends StatefulWidget {
  @override
  _MyPageViewState createState() => _MyPageViewState();
}

class _MyPageViewState extends State<MyPageView> {
  final PageController _pageController = PageController();
  int _currentPage = 0;
  final int _numPages = 3; // 総ページ数

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Dot Indicator Example'),
      ),
      body: Column(
        children: [
          Expanded(
            child: PageView(
              controller: _pageController,
              children: [
                Container(
                  color: Colors.red,
                  child: Center(child: Text('Page 1')),
                ),
                Container(
                  color: Colors.green,
                  child: Center(child: Text('Page 2')),
                ),
                Container(
                  color: Colors.blue,
                  child: Center(child: Text('Page 3')),
                ),
              ],
              onPageChanged: (int page) {
                setState(() {
                  _currentPage = page;
                });
              },
            ),
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: _buildPageIndicator(),
          ),
          SizedBox(height: 16.0),
        ],
      ),
    );
  }

  List<Widget> _buildPageIndicator() {
    List<Widget> list = [];
    for (int i = 0; i < _numPages; i++) {
      list.add(i == _currentPage ? _indicator(true) : _indicator(false));
    }
    return list;
  }

  Widget _indicator(bool isActive) {
    return AnimatedContainer(
      duration: Duration(milliseconds: 150),
      margin: EdgeInsets.symmetric(horizontal: 8.0),
      height: 8.0,
      width: isActive ? 24.0 : 8.0,
      decoration: BoxDecoration(
        color: isActive ? Colors.blue : Colors.grey,
        borderRadius: BorderRadius.all(Radius.circular(12)),
      ),
    );
  }
}

解説:

  • _numPages変数で総ページ数を定義しています。
  • _buildPageIndicator()関数で、ドットインジケーターを生成しています。
  • _indicator()関数で、アクティブなドットと非アクティブなドットの見た目を定義しています。isActiveの値に応じて、ドットの色と幅を変えています。
  • onPageChanged内で_currentPageを更新し、インジケーターを再描画しています。

ポイント:

  • AnimatedContainerを使うことで、ドットのサイズや色をスムーズにアニメーションさせることができます。
  • インジケーターのデザインは、アプリのテーマに合わせて調整できます。
  • より複雑なインジケーター(例: プログレスバー)を実装することも可能です。

インジケーターを適切に実装することで、ユーザーは現在位置を把握しやすくなり、より快適にアプリを利用できます。次のセクションでは、onPageChangedを使って無限スクロールを実装する方法について見ていきましょう。

無限スクロールの実装:リストの繰り返し表示

無限スクロール(またはループスクロール)は、リストの終端に到達してもスクロールが止まらず、先頭に戻ってループする機能です。PageViewで無限スクロールを実現するには、onPageChangedPageControllerを組み合わせて、擬似的に無限にスクロールしているように見せかけます。

基本的な考え方:

  1. リストの複製: 元のリストを複数回繰り返したリストを作成します。
  2. 初期位置の調整: スクロール開始位置をリストの中央付近に設定します。
  3. onPageChangedでの位置調整: onPageChangedで、リストの先頭または末尾に近づいた場合に、PageControllerを使ってリストの中央付近に移動させます。

実装例:

import 'package:flutter/material.dart';

class InfinitePageView extends StatefulWidget {
  @override
  _InfinitePageViewState createState() => _InfinitePageViewState();
}

class _InfinitePageViewState extends State<InfinitePageView> {
  final PageController _pageController = PageController(initialPage: _initialPage);
  List<int> _items = [1, 2, 3, 4, 5];
  late List<int> _infiniteItems;
  static const int _initialPage = 1000; // 初期ページ
  static const int _multiplier = 10000; // リストの繰り返し回数

  @override
  void initState() {
    super.initState();
    _infiniteItems = List.generate(_items.length * _multiplier, (index) => _items[index % _items.length]);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Infinite Scroll Example'),
      ),
      body: PageView.builder(
        controller: _pageController,
        itemCount: _infiniteItems.length,
        itemBuilder: (context, index) {
          return Container(
            color: index % _items.length == 0 ? Colors.red : (index % _items.length == 1 ? Colors.green : (index % _items.length == 2 ? Colors.blue : (index % _items.length == 3 ? Colors.yellow : Colors.orange))),
            child: Center(child: Text('Page ${_infiniteItems[index]}')),
          );
        },
        onPageChanged: (int page) {
          // 先頭または末尾に近づいたら中央に戻る
          if (page <= 10) {
            _jumpToCenter();
          } else if (page >= _infiniteItems.length - 10) {
            _jumpToCenter();
          }
        },
      ),
    );
  }

  void _jumpToCenter() {
    Future.microtask(() {
      _pageController.jumpToPage(_initialPage);
    });
  }
}

解説:

  • _items: 元のリストです。
  • _infiniteItems: _items_multiplier回繰り返したリストです。
  • _initialPage: PageControllerの初期ページです。リストの中央付近に設定しています。
  • onPageChanged: 現在のページがリストの先頭または末尾に近づいた場合に、_jumpToCenter()を呼び出して、リストの中央付近に戻します。
  • _jumpToCenter(): PageController.jumpToPage()を使って、指定されたページに瞬時に移動します。Future.microtask()で囲むことで、現在のフレームが完了した後に実行されるようにスケジュールし、スムーズな動作を保証します。

ポイント:

  • PageView.builderを使用することで、大量のページを効率的に生成できます。
  • _multiplierの値を大きくすることで、より長い無限スクロールを実現できます。
  • _initialPageの値を適切に設定することで、スクロール開始位置を調整できます。
  • onPageChangedの条件式を調整することで、リストの端に近づいたときに中央に戻るタイミングを調整できます。
  • _jumpToCenter()内でanimateToPage()を使用することもできますが、アニメーションが発生するため、ユーザーがスクロールしている感覚が途切れてしまう可能性があります。jumpToPage()の方がスムーズな無限スクロールを実現できます。

無限スクロールを実装することで、ユーザーはコンテンツを途切れることなく閲覧し続けることができます。次のセクションでは、onPageChangedを使ったパフォーマンス最適化について見ていきましょう。

パフォーマンス最適化:setState() の利用を最小限に

onPageChanged内でsetState()を呼び出すことは、UIの再描画をトリガーするため、パフォーマンスに影響を与える可能性があります。特に、複雑なウィジェットや大量のデータを扱う場合、setState()の呼び出し回数を最小限に抑えることが重要です。

setState() の利用を避ける/削減する方法:

  1. 値の変更が必要な部分のみsetState() で囲む:

    UI全体をsetState()で囲むのではなく、実際に値が変更される部分のみをsetState()で囲むようにします。これにより、不要なウィジェットの再描画を防ぐことができます。

  2. Provider、Riverpod、Blocなどの状態管理ライブラリの利用:

    状態管理ライブラリを使用することで、UIとロジックを分離し、より効率的な状態管理を実現できます。これらのライブラリは、変更された状態に関連するウィジェットのみを再描画するため、setState()の利用を大幅に削減できます。

  3. ValueNotifier/ChangeNotifier の利用:

    シンプルな状態管理であれば、ValueNotifierChangeNotifierを使用することもできます。これらのクラスは、値が変更されたときにリスナーに通知し、必要な部分のみを再描画することができます。

  4. shouldRepaint を使用した CustomPainter の最適化:

    CustomPainterを使用している場合、shouldRepaintメソッドを実装することで、再描画が必要な場合にのみpaintメソッドが呼び出されるように制御できます。

  5. const キーワードの活用:

    変更されないウィジェットや変数はconstキーワードで宣言することで、再構築を防ぐことができます。

具体的な例:ドットインジケーターの最適化

前のセクションで紹介したドットインジケーターの例を最適化してみましょう。

import 'package:flutter/material.dart';

class MyPageView extends StatefulWidget {
  @override
  _MyPageViewState createState() => _MyPageViewState();
}

class _MyPageViewState extends State<MyPageView> {
  final PageController _pageController = PageController();
  final ValueNotifier<int> _currentPage = ValueNotifier<int>(0); // ValueNotifierを使用
  final int _numPages = 3;

  @override
  void dispose() {
    _currentPage.dispose(); // ValueNotifierの破棄
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Dot Indicator Example (Optimized)'),
      ),
      body: Column(
        children: [
          Expanded(
            child: PageView(
              controller: _pageController,
              children: [
                Container(
                  color: Colors.red,
                  child: Center(child: Text('Page 1')),
                ),
                Container(
                  color: Colors.green,
                  child: Center(child: Text('Page 2')),
                ),
                Container(
                  color: Colors.blue,
                  child: Center(child: Text('Page 3')),
                ),
              ],
              onPageChanged: (int page) {
                _currentPage.value = page; // ValueNotifierの値を更新
              },
            ),
          ),
          ValueListenableBuilder<int>( // ValueListenableBuilderを使用
            valueListenable: _currentPage,
            builder: (context, value, child) {
              return Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: _buildPageIndicator(value),
              );
            },
          ),
          SizedBox(height: 16.0),
        ],
      ),
    );
  }

  List<Widget> _buildPageIndicator(int currentPage) { // 引数にcurrentPageを追加
    List<Widget> list = [];
    for (int i = 0; i < _numPages; i++) {
      list.add(i == currentPage ? _indicator(true) : _indicator(false));
    }
    return list;
  }

  Widget _indicator(bool isActive) {
    return AnimatedContainer(
      duration: Duration(milliseconds: 150),
      margin: EdgeInsets.symmetric(horizontal: 8.0),
      height: 8.0,
      width: isActive ? 24.0 : 8.0,
      decoration: BoxDecoration(
        color: isActive ? Colors.blue : Colors.grey,
        borderRadius: BorderRadius.all(Radius.circular(12)),
      ),
    );
  }
}

変更点:

  • _currentPageint型からValueNotifier<int>型に変更しました。
  • onPageChanged内で、_currentPage.value = page;のようにValueNotifierの値を更新します。
  • RowウィジェットをValueListenableBuilderで囲みました。ValueListenableBuilderは、_currentPageの値が変更されたときにのみ、内部のbuilder関数を呼び出します。
  • _buildPageIndicator関数にcurrentPageを引数として渡すように変更しました。

利点:

  • onPageChanged内でsetState()を呼び出す必要がなくなりました。
  • ドットインジケーターのみが再描画されるため、パフォーマンスが向上します。

まとめ:

onPageChanged内でsetState()を最小限に抑えることで、アプリのパフォーマンスを向上させることができます。状態管理ライブラリやValueNotifierなどのツールを活用し、効率的な状態管理を心がけましょう。次のセクションでは、onPageChangedの活用事例について見ていきましょう。

活用事例:チュートリアル、ステップ表示、スワイプ操作

onPageChangedは、PageViewを使った様々なUIデザインにおいて、中心的な役割を果たすことができます。ここでは、代表的な活用事例をいくつかご紹介します。

1. チュートリアル画面:

アプリの初回起動時や、新機能が追加された際に、使い方を説明するチュートリアル画面で、onPageChangedは非常に有効です。

  • 実装: 各ページに説明文や画像を配置し、onPageChangedで現在のページ番号を取得します。
  • 活用:

    • 最後のページに到達したら「完了」ボタンを表示する。
    • ページインジケーターで進捗状況を視覚的に示す。
    • ページ遷移に合わせてアニメーションを付与し、インタラクティブな体験を提供する。

2. ステップ表示 (ウィザード形式):

フォームへの入力や設定などを、複数のステップに分けて表示するウィザード形式のUIで、onPageChangedを活用できます。

  • 実装: 各ページに異なる入力フォームや設定項目を配置し、onPageChangedで現在のステップ番号を取得します。
  • 活用:

    • 「次へ」「戻る」ボタンを配置し、PageControllerを使ってページを遷移させる。
    • 現在のステップに応じて、ボタンの有効/無効を切り替える。
    • ステップインジケーターで現在のステップと全体の進捗状況を示す。
    • 各ステップの入力内容を検証し、エラーがある場合は次のステップに進めないように制御する。

3. スワイプ操作:

写真ギャラリーや商品リストなど、スワイプ操作でコンテンツを切り替えるUIで、onPageChangedを活用できます。

  • 実装: 各ページに画像や商品情報を配置し、onPageChangedで現在のページ番号を取得します。
  • 活用:

    • ページ遷移に合わせて、AppBarのタイトルを更新する。
    • 関連する情報を表示/非表示する。
    • ページの読み込み状況を監視し、必要な場合にのみコンテンツを読み込む。
    • スワイプ操作を検知して、特定のアクションを実行する (例: お気に入り登録、商品の詳細表示)。

4. タブ切り替え:

TabViewウィジェットの代わりに、PageViewとonPageChangedを使って、タブ切り替えを実装することもできます。

  • 実装: 各ページに異なるタブの内容を配置し、onPageChangedで選択されたタブのインデックスを取得します。
  • 活用:

    • カスタムタブバーを実装し、PageControllerを使ってページを遷移させる。
    • タブの選択状態を視覚的に示す。
    • タブごとに異なるアニメーションを付与する。

コード例 (タブ切り替え):

import 'package:flutter/material.dart';

class TabPageView extends StatefulWidget {
  @override
  _TabPageViewState createState() => _TabPageViewState();
}

class _TabPageViewState extends State<TabPageView> {
  final PageController _pageController = PageController();
  int _selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Tab PageView Example'),
      ),
      body: Column(
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              _buildTabItem(0, 'Tab 1'),
              _buildTabItem(1, 'Tab 2'),
              _buildTabItem(2, 'Tab 3'),
            ],
          ),
          Expanded(
            child: PageView(
              controller: _pageController,
              children: [
                Container(color: Colors.red, child: Center(child: Text('Tab 1 Content'))),
                Container(color: Colors.green, child: Center(child: Text('Tab 2 Content'))),
                Container(color: Colors.blue, child: Center(child: Text('Tab 3 Content'))),
              ],
              onPageChanged: (int index) {
                setState(() {
                  _selectedIndex = index;
                });
              },
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildTabItem(int index, String label) {
    return GestureDetector(
      onTap: () {
        _pageController.animateToPage(
          index,
          duration: Duration(milliseconds: 300),
          curve: Curves.easeInOut,
        );
      },
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Text(
          label,
          style: TextStyle(
            fontWeight: _selectedIndex == index ? FontWeight.bold : FontWeight.normal,
            color: _selectedIndex == index ? Colors.blue : Colors.black,
          ),
        ),
      ),
    );
  }
}

まとめ:

onPageChangedは、PageViewを使ったUIデザインにおいて、柔軟性と表現力を高めるための強力なツールです。これらの活用事例を参考に、onPageChangedを効果的に活用して、より洗練されたFlutterアプリを開発してください。最後のセクションでは、本記事の内容をまとめます。

まとめ:onPageChanged を活用してより良いUXを実現

本記事では、FlutterのonPageChangedコールバック関数について、その基本的な概念から、実装方法、パフォーマンス最適化、そして具体的な活用事例まで、幅広く解説してきました。onPageChangedは、PageViewウィジェットにおけるページ遷移を検知し、UIの更新やアニメーションの制御、ユーザーへのフィードバックなど、様々な処理を行うための重要なツールです。

主なポイント:

  • PageViewPageControllerの理解: PageViewでコンテンツをページとして表示し、PageControllerでスクロールを制御します。
  • onPageChangedの基本的な実装: ページが切り替わった際に実行されるコールバック関数で、現在のページ番号を取得できます。
  • アニメーションとの連携: ページ遷移に合わせてスムーズなアニメーションを付与し、ユーザーエクスペリエンスを向上させます。
  • インジケーターの実装: 現在のページ位置を視覚的に示すインジケーターを実装し、ユーザーがコンテンツの全体像を把握しやすくします。
  • 無限スクロールの実装: リストを繰り返し表示することで、途切れることのないコンテンツ閲覧体験を提供します。
  • パフォーマンス最適化: setState()の利用を最小限に抑え、アプリのパフォーマンスを向上させます。
  • 活用事例: チュートリアル画面、ステップ表示、スワイプ操作など、様々なUIデザインにonPageChangedを活用できます。

onPageChangedを活用することで、以下のメリットが得られます:

  • インタラクティブ性の向上: ページ遷移に合わせてUIを動的に変化させることで、ユーザーの操作に対するレスポンスを高めます。
  • 視覚的な分かりやすさの向上: インジケーターやアニメーションを活用することで、ユーザーがコンテンツの状況を直感的に理解できるようにします。
  • スムーズな操作性の実現: アニメーションや状態管理を適切に行うことで、快適なページング体験を提供します。

今後の学習:

  • より複雑なアニメーションの実装に挑戦してみましょう。
  • 状態管理ライブラリ (Provider, Riverpod, Blocなど) を利用して、onPageChangedと連携させた状態管理を実践してみましょう。
  • PageView以外のスクロール可能なウィジェット (ListView, GridView) でも、同様の概念を応用できる場合があります。
  • Flutter公式ドキュメントやサンプルコードを参考に、さらに高度なテクニックを習得しましょう。

onPageChangedを効果的に活用することで、より洗練された、使いやすいFlutterアプリを開発することができます。本記事が、あなたのFlutter開発の一助となれば幸いです。

コメントを残す