🔵

スライドの下にある丸ドット・インジケーターを再現する

に公開

Dot Indicatorとは?

ときどきWebサイトやアプリでスライドする画像や広告の下にある丸あれはなんでしょうか?
ドット・インジケーターと呼ぶらしい?

ライブラリを使用すれば再現できるそうですが、使わない企業もあるらしい?
どうするかな。。。

自作するしかないので作ってみた。PageController classというWidgetが必要なので今回使用しました。

flutter_hooksを使用するパターンではusePageController functionを使用しました。

こちらがデモ

https://youtube.com/shorts/-RP6TfDqIVA

Dot Indicator Slider

こちらのサンプルコードをmain.dartでインポートして使ってみてください。

色の指定にwithOpacity method使おうとしたのですが、Flutter3.27.0では非推奨みたいになっておりました。withValues()を使うみたいですね。

https://stackoverflow.com/questions/79284946/why-is-withopacity-deprecated-in-flutter-3-27-0-and-what-is-its-recommended-rep
https://docs.flutter.dev/release/breaking-changes/wide-gamut-framework

example
import 'dart:async';
import 'package:flutter/material.dart';

class AdvertisementSlider extends StatefulWidget {
  const AdvertisementSlider({super.key});

  
  State<AdvertisementSlider> createState() => _AdvertisementSliderState();
}

class _AdvertisementSliderState extends State<AdvertisementSlider> {
  final PageController _pageController = PageController();
  int _currentPage = 0;
  Timer? _timer;

  
  void initState() {
    super.initState();
    _timer = Timer.periodic(const Duration(seconds: 5), (Timer timer) {
      if (_currentPage < 4) {
        _currentPage++;
      } else {
        _currentPage = 0;
      }
      _pageController.animateToPage(
        _currentPage,
        duration: const Duration(milliseconds: 350),
        curve: Curves.easeIn,
      );
    });
  }

  
  void dispose() {
    _timer?.cancel();
    _pageController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    // 画面の幅を取得
    final screenWidth = MediaQuery.of(context).size.width;
    // 広告の正方形サイズを画面幅の80%に設定
    final adSize = screenWidth * 0.8;

    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          // 広告スライダー
          SizedBox(
            width: adSize,
            height: adSize,
            child: PageView.builder(
              controller: _pageController,
              onPageChanged: (int page) {
                setState(() {
                  _currentPage = page;
                });
              },
              itemCount: 5,
              itemBuilder: (context, index) {
                return Container(
                  margin: const EdgeInsets.symmetric(horizontal: 4),
                  decoration: BoxDecoration(
                    color: Colors.blue, // 青色の背景
                    borderRadius: BorderRadius.circular(4), // 角を少し丸く
                  ),
                  child: Center(
                    child: Text(
                      '広告 ${index + 1}',
                      style: const TextStyle(
                        color: Colors.white,
                        fontSize: 24,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ),
                );
              },
            ),
          ),
          // ドットインジケーター
          const SizedBox(height: 16), // 広告とドットの間隔
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: List.generate(5, (index) {
              return Padding(
                padding: const EdgeInsets.symmetric(horizontal: 4.0),
                child: AnimatedContainer(
                  duration: const Duration(milliseconds: 300),
                  height: 8,
                  width: 8,
                  decoration: BoxDecoration(
                    shape: BoxShape.circle,
                    color: _currentPage == index
                        ? Colors.black.withAlpha(0xFF)
                        : Colors.black.withAlpha(0x40),
                  ),
                ),
              );
            }),
          ),
        ],
      ),
    );
  }
}

// 使用例
class AdSliderPage extends StatelessWidget {
  const AdSliderPage({super.key});

  
  Widget build(BuildContext context) {
    return const Scaffold(
      backgroundColor: Colors.white, // 背景色を白に
      body: Center(
        child: AdvertisementSlider(),
      ),
    );
  }
}

flutter_hooksを使用したパターンも考えてみました。

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

// メインのスライダーウィジェット
class HooksAdvertisementSlider extends HookWidget {
  const HooksAdvertisementSlider({super.key});

  
  Widget build(BuildContext context) {
    // Hooks
    final pageController = usePageController();
    final currentPage = useState(0);

    // タイマーの設定
    useEffect(() {
      final timer = Timer.periodic(const Duration(seconds: 1), (timer) {
        final nextPage = currentPage.value < 4 ? currentPage.value + 1 : 0;
        pageController.animateToPage(
          nextPage,
          duration: const Duration(milliseconds: 350),
          curve: Curves.easeIn,
        );
      });

      return timer.cancel; // cleanup関数
    }, []); // 空の依存配列で一度だけ実行

    // 画面サイズの取得
    final screenWidth = MediaQuery.of(context).size.width;
    final adSize = screenWidth * 0.8;

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.orange,
        title: const Text('HooksDot Indicator Slider'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          // 広告スライダー
          SizedBox(
            width: adSize,
            height: adSize,
            child: PageView.builder(
              controller: pageController,
              onPageChanged: (page) => currentPage.value = page,
              itemCount: 5,
              itemBuilder: (context, index) => _AdItem(index: index),
            ),
          ),
          const SizedBox(height: 16),
          // ドットインジケーター
          _DotIndicator(
            currentPage: currentPage.value,
            pageCount: 5,
          ),
        ],
      ),
    );
  }
}

// 広告アイテムコンポーネント
class _AdItem extends StatelessWidget {
  const _AdItem({required this.index});

  final int index;

  
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.symmetric(horizontal: 4),
      decoration: BoxDecoration(
        color: Colors.blue,
        borderRadius: BorderRadius.circular(4),
      ),
      child: Center(
        child: Text(
          '広告 ${index + 1}',
          style: const TextStyle(
            color: Colors.white,
            fontSize: 24,
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
    );
  }
}

// ドットインジケーターコンポーネント
class _DotIndicator extends StatelessWidget {
  const _DotIndicator({
    required this.currentPage,
    required this.pageCount,
  });

  final int currentPage;
  final int pageCount;

  
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: List.generate(
        pageCount,
        (index) => Padding(
          padding: const EdgeInsets.symmetric(horizontal: 4.0),
          child: AnimatedContainer(
            duration: const Duration(milliseconds: 300),
            height: 8,
            width: 8,
            decoration: BoxDecoration(
              shape: BoxShape.circle,
              color: currentPage == index
                  ? Colors.black.withAlpha(0xFF)
                  : Colors.black.withAlpha(0x40),
            ),
          ),
        ),
      ),
    );
  }
}
  1. 基本的なFlutterウィジェット
  • Scaffold: アプリの基本的な視覚構造を提供
  • Column: 子ウィジェットを縦方向に配置
  • Row: 子ウィジェットを横方向に配置
  • Center: 子ウィジェットを中央に配置
  • SizedBox: 特定のサイズを持つボックス
  • Container: カスタマイズ可能なボックス(色、装飾など)
  • Text: テキストを表示
  1. 状態管理(State Management)
class AdvertisementSlider extends StatefulWidget {
  
  State<AdvertisementSlider> createState() => _AdvertisementSliderState();
}
  • StatefulWidget: 状態を持つウィジェット
  • setState(): 状態を更新し、UIを再構築
  1. PageView と Controller
final PageController _pageController = PageController();
PageView.builder(
  controller: _pageController,
  onPageChanged: (int page) { ... },
  itemBuilder: (context, index) { ... },
)
  • PageView: スワイプ可能なページビュー
  • PageController: ページの制御とアニメーション
  • itemBuilder: 各ページのウィジェットを構築
  1. タイマー処理
Timer? _timer;
_timer = Timer.periodic(const Duration(seconds: 5), (Timer timer) { ... });
  • dart:async: 非同期処理のためのパッケージ
  • Timer.periodic: 定期的な処理の実行
  • Duration: 時間間隔の指定
  1. ライフサイクル管理

void initState() {
  super.initState();
  // 初期化処理
}


void dispose() {
  _timer?.cancel();
  _pageController.dispose();
  super.dispose();
}
  • initState(): ウィジェットの初期化
  • dispose(): リソースの解放
  1. レスポンシブデザイン
final screenWidth = MediaQuery.of(context).size.width;
final adSize = screenWidth * 0.8;
  • MediaQuery: 画面サイズなどの情報を取得
  • 相対的なサイズ計算
  1. スタイリングとデコレーション
decoration: BoxDecoration(
  color: Colors.blue,
  borderRadius: BorderRadius.circular(4),
)
  • BoxDecoration: コンテナーのスタイリング
  • BorderRadius: 角の丸み
  • Colors: 色の指定
  1. アニメーション
AnimatedContainer(
  duration: const Duration(milliseconds: 300),
  // プロパティ
)
  • AnimatedContainer: 暗黙的アニメーション
  • Duration: アニメーション時間の指定
  1. リスト生成
List.generate(5, (index) { ... })
  • List.generate: 指定した数のウィジェットを生成
  1. Dart言語の基本
  • null安全性(?演算子の使用)
  • 三項演算子(condition ? value1 : value2
  • 文字列補間(${}
  • constfinalの使用
  • クラスとコンストラクタ
  • メソッドのオーバーライド

これらの知識を組み合わせることで、カスタムスライダーとドットインジケーターを実装できます。また、以下のような追加学習も推奨されます:

  • Widgetのライフサイクル
  • Flutterのレイアウトの仕組み
  • アニメーションの種類と使い分け
  • 状態管理の異なるアプローチ
  • パフォーマンス最適化

最後に

最近決まった案件がライブラリに依存しないプロジェクトらしいので自作するのだろうなーと思い標準機能でどこまで作れるのか探求しておりました。

ライブラリも元々は誰かが作ったので作れるはず😅

Discussion