👌
FlutterでPageViewを使ったオンボーディング画面を作成する方法
はじめに
アプリを初めて起動した時に、使い方を説明する画面を見たことはありませんか?
例えば、InstagramやTikTokなどのアプリでは、初回起動時に「写真を共有しよう」「動画を見よう」といった説明画面が表示されます。
このようなオンボーディング画面(アプリの使い方を説明する画面)を作りたいと思ったことはありませんか?
実は、FlutterではPageViewというウィジェットを使うことで、簡単にスワイプでページを切り替えられる画面を作成できます。
PageViewとは?
PageViewは、複数のページを横に並べて、スワイプで切り替えられるウィジェットです。
主な特徴:
- スワイプでページを切り替え可能
- アニメーション付きの滑らかな切り替え
- ページインジケータ(現在のページを表示するドット)と組み合わせやすい
- オンボーディング画面やチュートリアル画面に最適
実装手順
1. 基本的なPageViewの使い方
まずは、最もシンプルなPageViewの実装例を見てみましょう:
PageView(
children: [
// 1ページ目
Container(
color: Colors.red,
child: const Center(
child: Text('1ページ目'),
),
),
// 2ページ目
Container(
color: Colors.blue,
child: const Center(
child: Text('2ページ目'),
),
),
// 3ページ目
Container(
color: Colors.green,
child: const Center(
child: Text('3ページ目'),
),
),
],
)
このコードで、赤、青、緑の3つのページが作成され、左右にスワイプして切り替えることができます。
2. ページの切り替えを制御する
PageControllerを使って、プログラムでページを切り替えることができます:
class _MyPageState extends State<MyPage> {
// ページの切り替えを制御するコントローラー
final PageController _controller = PageController();
// 現在のページ番号
int _currentPage = 0;
// 次のページに進む処理
void _nextPage() {
_controller.animateToPage(
_currentPage + 1,
duration: const Duration(milliseconds: 300), // アニメーション時間
curve: Curves.easeInOut, // アニメーションの曲線
);
}
Widget build(BuildContext context) {
return PageView(
controller: _controller,
onPageChanged: (index) {
setState(() {
_currentPage = index; // ページが変わった時に現在のページ番号を更新
});
},
children: [
// 各ページの内容
],
);
}
}
3. ページインジケータの追加
現在のページを表示するドット(インジケータ)を追加する方法:
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
3, // ページ数
(index) => Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
width: _currentPage == index ? 16 : 8, // 現在のページは大きく
height: 8,
decoration: BoxDecoration(
color: _currentPage == index ? Colors.black : Colors.grey, // 現在のページは濃い色
borderRadius: BorderRadius.circular(4),
),
),
),
)
4. 次へボタンの追加
ページを進めるためのボタンを追加する方法:
ElevatedButton(
onPressed: _nextPage,
child: Text(
_currentPage == 2 ? '完了' : '次へ', // 最後のページでは「完了」と表示
),
)
よく使うPageViewのバリエーション
PageView.builder
ページ数が多い場合や、動的にページを生成したい場合に使用します:
PageView.builder(
itemCount: 10, // ページ数
itemBuilder: (context, index) {
return Container(
color: Colors.primaries[index % Colors.primaries.length],
child: Center(
child: Text('${index + 1}ページ目'),
),
);
},
)
PageViewの他のメソッド
-
controller.jumpToPage(index)
- アニメーションなしでページを切り替え -
controller.nextPage()
- 次のページに移動 -
controller.previousPage()
- 前のページに移動 -
controller.position.page
- 現在のページ番号を取得
注意点
- PageControllerは必ずdisposeする必要があります
- ページ数が多い場合は、PageView.builderを使用することをお勧めします
- アニメーションの時間は、ユーザビリティを考慮して300-500ミリ秒程度が適切です
完全な実装例
以下は、オンボーディング画面の完全な実装例です。
// Flutterの基本パッケージをインポート
import 'package:flutter/material.dart';
/// アプリケーションのエントリーポイント
/// アプリが起動した時に最初に実行される関数
void main() {
runApp(const MyApp());
}
/// アプリケーションのルートウィジェット
/// MaterialAppを設定し、アプリ全体のテーマやナビゲーションを管理
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Page View Demo',
// デモページをホーム画面として設定
home: const PageViewDemoPage(),
);
}
}
/// オンボーディングページのメインウィジェット
/// ユーザーにアプリの使い方を説明する画面を管理
class PageViewDemoPage extends StatefulWidget {
const PageViewDemoPage({super.key});
State<PageViewDemoPage> createState() => _PageViewDemoPageState();
}
/// オンボーディングページの状態管理クラス
/// ページの切り替えやユーザーの操作を管理
class _PageViewDemoPageState extends State<PageViewDemoPage> {
/// ページの切り替えを制御するコントローラー
/// アニメーションやページの移動を管理
final PageController _controller = PageController();
/// 現在表示されているページのインデックス(0から開始)
int _currentPage = 0;
/// 各ページの情報を格納するリスト
/// 各ページのタイトル、説明文、背景色を定義
final List<Map<String, dynamic>> _pages = [
{
'title': 'Welcome', // ページのタイトル
'description': 'Let\'s get started with our amazing app!', // ページの説明文
'color': Colors.purpleAccent, // ページの背景色
},
{
'title': 'Discover',
'description': 'Explore features and enjoy the experience.',
'color': Colors.orangeAccent,
},
{
'title': 'Get Started',
'description': 'Sign up and begin your journey!',
'color': Colors.teal,
},
];
/// 次のページに進む処理
/// 現在のページが最後でない場合は次のページに移動し、
/// 最後のページの場合は完了メッセージを表示
void _nextPage() {
if (_currentPage < _pages.length - 1) {
// まだ次のページがある場合:アニメーション付きで次のページに移動
_controller.animateToPage(
_currentPage + 1, // 移動先のページインデックス
duration: const Duration(milliseconds: 300), // アニメーション時間(300ミリ秒)
curve: Curves.easeInOut, // アニメーションの曲線(滑らかな動き)
);
} else {
// 最後のページの場合:完了メッセージを表示
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('完了しました!')),
);
}
}
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
// メインのページビュー(画面の大部分を占める)
PageView(
controller: _controller, // ページの制御に使用
// ページが変更された時の処理(ユーザーがスワイプした時など)
onPageChanged: (index) {
setState(() {
_currentPage = index; // 現在のページインデックスを更新
});
},
children: [
// 各ページのウィジェットを順番に配置
OnboardingPage(
title: _pages[0]['title'],
description: _pages[0]['description'],
color: _pages[0]['color'],
),
OnboardingPage(
title: _pages[1]['title'],
description: _pages[1]['description'],
color: _pages[1]['color'],
),
OnboardingPage(
title: _pages[2]['title'],
description: _pages[2]['description'],
color: _pages[2]['color'],
),
],
),
// 下部のインジケータとボタン(画面の最前面に表示)
Positioned(
bottom: 40, // 画面下部から40ピクセル上
left: 24, // 左端から24ピクセル
right: 24, // 右端から24ピクセル
child: Column(
children: [
// ページインジケータ(現在のページを表示するドット)
PageIndicator(
currentPage: _currentPage, // 現在のページ番号
totalPages: _pages.length, // 総ページ数
),
const SizedBox(height: 16), // 16ピクセルの空白
// 次へボタン
NextButton(
onPressed: _nextPage, // ボタンが押された時の処理
isLastPage: _currentPage == _pages.length - 1, // 最後のページかどうか
),
],
),
),
],
),
);
}
}
/// 各オンボーディングページのウィジェット
/// タイトル、説明文、背景色を表示する再利用可能なコンポーネント
class OnboardingPage extends StatelessWidget {
/// ページのタイトル
final String title;
/// ページの説明文
final String description;
/// ページの背景色(グラデーションの開始色として使用)
final Color color;
/// コンストラクタ
/// [title] ページのタイトル
/// [description] ページの説明文
/// [color] ページの背景色
const OnboardingPage({
super.key,
required this.title,
required this.description,
required this.color,
});
Widget build(BuildContext context) {
return Container(
// グラデーション背景を設定(上から下へのグラデーション)
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [color, Colors.white], // 指定された色から白へのグラデーション
begin: Alignment.topCenter, // グラデーションの開始位置(上部中央)
end: Alignment.bottomCenter, // グラデーションの終了位置(下部中央)
),
),
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0), // 左右に24ピクセルの余白
child: Column(
mainAxisAlignment: MainAxisAlignment.center, // 縦方向の中央揃え
children: [
// タイトルテキスト
Text(
title,
style: const TextStyle(
fontSize: 36, // フォントサイズ36
fontWeight: FontWeight.bold, // 太字
color: Colors.black87, // 色(黒に近いグレー)
),
),
const SizedBox(height: 20), // 20ピクセルの空白
// 説明テキスト
Text(
description,
style: const TextStyle(
fontSize: 18, // フォントサイズ18
color: Colors.black54, // 色(薄いグレー)
),
textAlign: TextAlign.center, // テキストを中央揃え
),
],
),
),
),
);
}
}
/// ページインジケータのウィジェット
/// 現在のページを表示するドットの集合体
class PageIndicator extends StatelessWidget {
/// 現在表示されているページのインデックス(0から開始)
final int currentPage;
/// 総ページ数
final int totalPages;
/// コンストラクタ
/// [currentPage] 現在のページ番号
/// [totalPages] 総ページ数
const PageIndicator({
super.key,
required this.currentPage,
required this.totalPages,
});
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center, // 横方向の中央揃え
children: List.generate(
totalPages, // 総ページ数分のドットを生成
(index) => AnimatedContainer(
duration: const Duration(milliseconds: 300), // アニメーション時間
margin: const EdgeInsets.symmetric(horizontal: 4), // 左右に4ピクセルの余白
// 現在のページの場合は大きく(16ピクセル)、そうでなければ小さく(8ピクセル)
width: currentPage == index ? 16 : 8,
height: 8, // 高さは常に8ピクセル
decoration: BoxDecoration(
// 現在のページの場合は濃い色(黒)、そうでなければ薄い色(グレー)
color: currentPage == index
? Colors.black87
: Colors.grey,
borderRadius: BorderRadius.circular(4), // 角を丸くする(4ピクセルの半径)
),
),
),
);
}
}
/// 次へボタンのウィジェット
/// ページを進めるためのボタン(最後のページでは「完了」と表示)
class NextButton extends StatelessWidget {
/// ボタンが押された時に実行される処理
final VoidCallback onPressed;
/// 最後のページかどうかのフラグ
final bool isLastPage;
/// コンストラクタ
/// [onPressed] ボタンが押された時の処理
/// [isLastPage] 最後のページかどうか
const NextButton({
super.key,
required this.onPressed,
required this.isLastPage,
});
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onPressed, // ボタンが押された時の処理
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black87, // ボタンの背景色
padding: const EdgeInsets.symmetric(
horizontal: 40, // 左右の余白40ピクセル
vertical: 14, // 上下の余白14ピクセル
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30), // 角を丸くする(30ピクセルの半径)
),
),
child: Text(
// 最後のページの場合は「完了」、そうでなければ「次へ」
isLastPage ? '完了' : '次へ',
style: const TextStyle(fontSize: 16), // フォントサイズ16
),
);
}
}
まとめ
PageViewを使用することで、簡単に美しいオンボーディング画面を作成できます。
スワイプでのページ切り替え、アニメーション、インジケータなど、ユーザーにとって使いやすい機能を簡単に実装できます。
Discussion