ponshu-room-lite/lib/widgets/sake_3d_carousel.dart

168 lines
5.0 KiB
Dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:carousel_slider/carousel_slider.dart';
import '../models/sake_item.dart';
import '../screens/sake_detail_screen.dart';
/// 3D風カルーセルウィジェット (v2: carousel_slider版)
/// 安定した無限スクロールと拡大アニメーションを提供
class Sake3DCarousel extends StatefulWidget {
final List<SakeItem> items;
final double height;
const Sake3DCarousel({
super.key,
required this.items,
this.height = 240, // 少し高さを確保
});
@override
State<Sake3DCarousel> createState() => _Sake3DCarouselState();
}
class _Sake3DCarouselState extends State<Sake3DCarousel> {
int _currentIndex = 0;
@override
Widget build(BuildContext context) {
if (widget.items.isEmpty) {
return SizedBox(
height: widget.height,
child: const Center(
child: Text('関連する日本酒がありません'),
),
);
}
return Column(
children: [
CarouselSlider.builder(
itemCount: widget.items.length,
itemBuilder: (context, index, realIndex) {
final item = widget.items[index];
return _buildCarouselItem(item);
},
options: CarouselOptions(
height: 200,
aspectRatio: 16/9,
viewportFraction: 0.6, // 隣が見える幅
initialPage: 0,
enableInfiniteScroll: widget.items.length > 2, // 3枚以上で無限
reverse: false,
autoPlay: false,
enlargeCenterPage: true, // 中央を拡大 (ぽにょぽにょ感)
enlargeFactor: 0.3,
scrollDirection: Axis.horizontal,
onPageChanged: (index, reason) {
setState(() {
_currentIndex = index;
});
},
),
),
const SizedBox(height: 12),
// インジケーター
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: widget.items.asMap().entries.map((entry) {
return Container(
width: 8.0,
height: 8.0,
margin: const EdgeInsets.symmetric(horizontal: 4.0),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: (Theme.of(context).brightness == Brightness.dark
? Colors.white
: Theme.of(context).primaryColor)
.withValues(alpha: _currentIndex == entry.key ? 0.9 : 0.2),
),
);
}).toList(),
),
],
);
}
Widget _buildCarouselItem(SakeItem item) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => SakeDetailScreen(sake: item),
),
);
},
child: Card(
elevation: 6,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
clipBehavior: Clip.antiAlias,
child: Stack(
fit: StackFit.expand,
children: [
// 背景画像
Hero(
tag: item.id,
child: item.displayData.imagePaths.isNotEmpty
? Image.file(
File(item.displayData.imagePaths.first),
fit: BoxFit.cover,
)
: Container(
color: Colors.grey[300],
child: const Icon(Icons.image, size: 50, color: Colors.grey),
),
),
// グラデーション
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withValues(alpha: 0.8),
],
),
),
),
// テキスト情報
Positioned(
bottom: 12,
left: 12,
right: 12,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
item.displayData.displayName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
Text(
item.displayData.displayPrefecture,
style: const TextStyle(
color: Colors.white70,
fontSize: 10,
),
),
],
),
),
],
),
),
);
}
}