2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【備忘録】flutterのPageViewでドットインジケーター

Last updated at Posted at 2023-10-30

はじめに

flutterでドットインジケータ作ったので備忘録的に残します。
もっといい書き方や内容的に間違っているところはコメント・編集リクエスト等で 優しく 教えてください.

githubのリポジトリはこちら

完成品
画面収録 2023-10-30 13.53.30.gif

実装

完成コード

まずは完成コードを載せておきます。

完成コード
dot_indicator.dart
class PageViewWithDotIndicator extends HookWidget {
  const PageViewWithDotIndicator({super.key});

  @override
  Widget build(BuildContext context) {
    final pages = [
      const SimplePage(
        title: '0ページ',
        contentText: '0ページ目のコンテンツです',
      ),
      const SimplePage(
        title: '1ページ',
        contentText: '1ページ目のコンテンツです',
      ),
      const SimplePage(
        title: '2ページ',
        contentText: '2ページ目のコンテンツです',
      ),
      const SimplePage(
        title: '3ページ',
        contentText: '3ページ目のコンテンツです',
      ),
      const SimplePage(
        title: '4ページ',
        contentText: '4ページ目のコンテンツです',
      ),
    ];

    final pageController = usePageController();

    final currentIndex = useState(
      (pageController.positions.isNotEmpty ? pageController.page : 0)!.round(),
    );

    useEffect(
      () {
        void listener() {
          if (pageController.positions.isNotEmpty) {
            currentIndex.value = pageController.page!.round();
          }
        }

        pageController.addListener(listener);

        return () {
          pageController.removeListener(listener);
        };
      },
      [],
    );

    return Scaffold(
      appBar: AppBar(
        title: Text(
          '現在のpageIndex : ${currentIndex.value.toString()}',
        ),
      ),
      body: Stack(
        children: [
          PageView(
            controller: pageController,
            children: pages,
          ),
          Align(
            alignment: Alignment.bottomCenter,
            child: Padding(
              padding: const EdgeInsets.only(
                bottom: 32,
              ),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  for (int i = 0; i < pages.length; i++)
                    Padding(
                      padding: const EdgeInsets.symmetric(
                        horizontal: 8,
                      ),
                      child: Container(
                        width: 16,
                        height: 16,
                        decoration: BoxDecoration(
                          shape: BoxShape.circle,
                          color: Colors.black.withOpacity(
                            currentIndex.value == i ? 1 : 0.1,
                          ),
                        ),
                      ),
                    ),
                ],
              ),
            ),
          )
        ],
      ),
    );
  }
}

詳細な実装

flutter_hooksを導入しましょう

flutter pub add flutter_hooks

HooksWidgetの雛形を作ります。

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

class PageViewWithDotIndicator extends HookWidget {
  const PageViewWithDotIndicator({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: PageView(
        children: [],
      ),
    );
  }
}

適当にpageを作っておいてください

    final pages = [
      const SimplePage(
        title: '0ページ',
        contentText: '0ページ目のコンテンツです',
      ),
      const SimplePage(
        title: '1ページ',
        contentText: '1ページ目のコンテンツです',
      ),
      const SimplePage(
        title: '2ページ',
        contentText: '2ページ目のコンテンツです',
      ),
      const SimplePage(
        title: '3ページ',
        contentText: '3ページ目のコンテンツです',
      ),
      const SimplePage(
        title: '4ページ',
        contentText: '4ページ目のコンテンツです',
      ),
    ];

一応SimplePageのコードも載せておきます!

SimplePage
simple_page.dart
import 'package:flutter/material.dart';

class SimplePage extends StatelessWidget {
  const SimplePage({
    required this.title,
    required this.contentText,
    super.key,
  });

  final String title;
  final String contentText;
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.amber[200],
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              contentText,
              style: TextStyle(
                fontSize: 30,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

次にPageControllerを作成します.

            title: '4ページ',
        contentText: '4ページ目のコンテンツです',
      ),
    ];

    // これを追記
    final pageController = usePageController();
    
    return Scaffold(
      body: PageView(

現在のページ番号とページの変化を管理するuseState, useEffectを書きます.

    final currentIndex = useState(
      (pageController.positions.isNotEmpty ? pageController.page : 0),
    );

    // ページの移動を管理
    useEffect(
      () {
        void listener() {
          if (pageController.positions.isNotEmpty) {
            currentIndex.value = pageController.page;
          }
        }

        pageController.addListener(listener);

        return () {
          pageController.removeListener(listener);
        };
      },
      [],
    );

また、ウィジェットの部分をいい感じにします。PageViewに対してStackでドットの列を重ねて実装しています。
page番号は0から始まるので、現在のページ番号とドットのインデックスiが一致する時に透明度が1になるように作成しています。

    return Scaffold(
      appBar: AppBar(
        title: Text(
          '現在のpageIndex : ${currentIndex.value.toString()}',
        ),
      ),
      body: Stack(
        children: [
          PageView(
            controller: pageController,
            children: pages,
          ),
          Align(
            alignment: Alignment.bottomCenter,
            child: Padding(
              padding: const EdgeInsets.only(
                bottom: 32,
              ),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  for (int i = 0; i < pages.length; i++)
                    Padding(
                      padding: const EdgeInsets.symmetric(
                        horizontal: 8,
                      ),
                      child: Container(
                        width: 16,
                        height: 16,
                        decoration: BoxDecoration(
                          shape: BoxShape.circle,
                          color: Colors.black.withOpacity(
                            currentIndex.value == i ? 1 : 0.1,
                          ),
                        ),
                      ),
                    ),
                ],
              ),
            ),
          )
        ],
      ),
    );

一旦動かしてみましょう!
この状態で動かすと以下のgifのようになるかと思います。
画面収録 2023-10-30 13.54.08.gif

ページが切り替わっている途中にどのインジケータも点灯していないことがわかるかと思います。
pageの値はページの遷移中は小数値になるので、うまく表示されないようですね.

これを解決するために、currentPage.valueの値を四捨五入して必ず整数値になるように調整します。
以下のように書き換えてみてください.

    final currentIndex = useState(
      (pageController.positions.isNotEmpty ? pageController.page : 0)!.floor(),
    );

    useEffect(
      () {
        void listener() {
          if (pageController.positions.isNotEmpty) {
            currentIndex.value = pageController.page!.floor();
          }
        }

        pageController.addListener(listener);

        return () {
          pageController.removeListener(listener);
        };
      },
      [],
    );

これでうまく動作するはずです!

終わりに

今回はドットインジケータを実装してみました。
もし皆さんの参考になれば幸いです🙏

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?