🌎
【Flutter】画面をモダンなフルスクリーンの縦カルーセルの作り方
要約
本記事では、Flutterで縦方向のフルスクリーンのカルーセルの実装方法を紹介します。パッケージは carousel_slider_plusを使います。さらに、画面右中央にページインジケータを重ねて表示する手順も示します。
対象
本ブログの対象者は以下の方々に向けて執筆しています。
- TikTokやInstagramのリール動画風に縦スワイプでページを切り替える画面を実装したい人
- 画面いっぱいに1ページを見せ、右側に縦インジケータを出したい人
課題
このブログでは以下を実現できる実装の解説をします。
- 縦方向のページ送りを1ページで見せたい
- 完全フルスクリーンで表示したい
- 現在のページ位置がわかるように、右中央に縦並びのドットを表示したい
解決アプローチ
Step1. 縦方向のフルスクリーンのカルーセルの実装
ポイントは大きく4点あります
- CarouselSliderに
CarouselOptions(scrollDirection: Axis.vertical)を設定。 - 表示しているカードの前後をどの程度見せるかの指標
viewportFraction: 1.0を指定する。 - 表示する高さにデバイスの高さを指定する
- AppBarを透過させるため、
extendBodyBehindAppBar: trueを指定する
サンプルコードは以下です
Widget build(BuildContext context) {
final size = MediaQuery.sizeOf(context);
return Scaffold(
// SafeArea を無視してフルスクリーン
extendBodyBehindAppBar: true,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: const Text('Fullscreen Vertical Carousel'),
),
body: Stack(
children: [
// 画面いっぱい
SizedBox.expand(
child: CarouselSlider.builder(
itemCount: items.length,
itemBuilder: (context, index, realIndex) {
return Container(
width: size.width,
height: size.height,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.indigo.shade800, Colors.indigo.shade400],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Center(
child: Text(
items[index],
style: const TextStyle(
fontSize: 36,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
);
},
options: CarouselOptions(
height: size.height, // ← PageViewのため高さ制約が必須
scrollDirection: Axis.vertical, // ← 縦スクロール
viewportFraction: 1.0, // ← 1ページを画面いっぱいに
enableInfiniteScroll: true,
autoPlay: false,
enlargeCenterPage: false,
onPageChanged: (index, reason) {
setState(() => currentIndex = index);
},
),
),
),
Step2. 画面右中央にページインジケータを重ねて表示する
ポイントは大きく3点あります
- Stackでカルーセルの上に重ねる
- ドットはColumnで縦一列に並べ、アクティブな位置だけ高さを強調して視認性を向上させる
- カルーセルのonPageChangedでcurrentIndexを更新してインジケータへ反映させる
完成系コードは以下です。以下コピペで動作確認までできます。
class FullscreenVerticalCarouselRightIndicator extends StatefulWidget {
const FullscreenVerticalCarouselRightIndicator({super.key});
State<FullscreenVerticalCarouselRightIndicator> createState() =>
_FullscreenVerticalCarouselRightIndicatorState();
}
class _FullscreenVerticalCarouselRightIndicatorState
extends State<FullscreenVerticalCarouselRightIndicator> {
final items = List.generate(6, (i) => 'Page ${i + 1}');
int currentIndex = 0;
Widget build(BuildContext context) {
final size = MediaQuery.sizeOf(context);
return Scaffold(
// SafeArea を無視してフルスクリーン
extendBodyBehindAppBar: true,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: const Text('Fullscreen Vertical Carousel'),
),
body: Stack(
children: [
// 画面いっぱい
SizedBox.expand(
child: CarouselSlider.builder(
itemCount: items.length,
itemBuilder: (context, index, realIndex) {
return Container(
width: size.width,
height: size.height,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.indigo.shade800, Colors.indigo.shade400],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Center(
child: Text(
items[index],
style: const TextStyle(
fontSize: 36,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
);
},
options: CarouselOptions(
height: size.height, // ← PageViewのため高さ制約が必須
scrollDirection: Axis.vertical, // ← 縦スクロール
viewportFraction: 1.0, // ← 1ページを画面いっぱいに
enableInfiniteScroll: true,
autoPlay: false,
enlargeCenterPage: false,
onPageChanged: (index, reason) {
setState(() => currentIndex = index);
},
),
),
),
// 右中央に縦インジケータ(ドット + 枚数バッジ)
Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.only(right: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 枚数バッジ(例: 3 / 6 )
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.35),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${currentIndex + 1} / ${items.length}',
style: const TextStyle(color: Colors.white, fontSize: 13),
),
),
const SizedBox(height: 10),
// 縦向きドット
_VerticalDotsIndicator(
count: items.length,
index: currentIndex,
activeColor: Colors.white,
inactiveColor: Colors.white.withOpacity(0.35),
size: 8,
spacing: 8,
),
],
),
),
),
],
),
);
}
}
class _VerticalDotsIndicator extends StatelessWidget {
const _VerticalDotsIndicator({
required this.count,
required this.index,
this.activeColor = Colors.white,
this.inactiveColor = Colors.white54,
this.size = 8,
this.spacing = 8,
super.key,
});
final int count;
final int index;
final Color activeColor;
final Color inactiveColor;
final double size;
final double spacing;
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: List.generate(count, (i) {
final isActive = i == index;
return AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeOut,
margin: EdgeInsets.symmetric(vertical: spacing / 2),
width: size,
height: isActive ? size * 2.2 : size, // 縦向きなので高さで強調
decoration: BoxDecoration(
color: isActive ? activeColor : inactiveColor,
borderRadius: BorderRadius.circular(size),
),
);
}),
);
}
}
よくあるハマりどころ
- 高さ指定忘れ
- CarouselSliderは内部でPageViewを使うため、高さ指定が必須です。高さが未確定だと、例外が出るので要注意です。今回の例だと、MediaQuery.sizeOf(context).height を使って高さを指定してます
- 親も縦スクロールで競合
- 親Widgetが縦のListView等で子カルーセルも縦だとジェスチャが奪い合いになるので要注意です
- その際は、親のListViewに
physics: const NeverScrollableScrollPhysics()を指定し、スクロール不可にする方法で解消できます
- 次のカードを少し見せたい
- 次のカードを少し見せたい場合は、
viewportFractionの値を0.8〜0.95に調整すると良いでしょう
- 次のカードを少し見せたい場合は、
代替案
より細かい制御がしたい時は、PageViewを使う方法もあるようです。
まとめ
CarouselSliderはスクロール方向を指定できるので、scrollDirectionの指定値で縦スクロールを実現できました。また、モダンな見た目にするため、高さを画面いっぱいにしAppBarを意識させないUI、また、ページインジケータも表示することでユーザビリティも向上させてみました。今回の実装を参考により良いUI・UXの実現に挑戦してみてください。
今回の参考コードについてご質問・ご意見ございましたらコメントで連絡いただけると幸いです。
本ブログを読んでくださりありがとうございました。
参考リンク
- 【pub.dev】carousel_slider_plus
- 【pub.dev】carousel_slider
Discussion