ponshu-room-lite/lib/widgets/sake_3d_carousel_with_reaso...

294 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:io';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:carousel_slider/carousel_slider.dart';
import '../services/sake_recommendation_service.dart';
import '../screens/sake_detail_screen.dart';
import 'package:lucide_icons/lucide_icons.dart';
/// 3D風カルーセルウィジェット推薦理由付き
/// 円形配置で左右対称、奥行き感のあるローリングスライダー
///
/// v2.0: よりスムーズで3D的な奥行き表現に改善
class Sake3DCarouselWithReason extends StatefulWidget {
final List<RecommendedSake> recommendations;
final double height;
const Sake3DCarouselWithReason({
super.key,
required this.recommendations,
this.height = 260,
});
@override
State<Sake3DCarouselWithReason> createState() => _Sake3DCarouselWithReasonState();
}
class _Sake3DCarouselWithReasonState extends State<Sake3DCarouselWithReason> {
int _currentIndex = 0;
final CarouselSliderController _carouselController = CarouselSliderController();
@override
Widget build(BuildContext context) {
if (widget.recommendations.isEmpty) {
return SizedBox(
height: widget.height,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(LucideIcons.info, color: Colors.grey[400], size: 32),
const SizedBox(height: 8),
Text(
'関連する日本酒を追加すると\nおすすめが表示されます',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey[600], fontSize: 12),
),
],
),
),
);
}
return Column(
children: [
SizedBox(
height: widget.height - 30,
child: CarouselSlider.builder(
carouselController: _carouselController,
itemCount: widget.recommendations.length,
options: CarouselOptions(
height: widget.height - 30,
enlargeCenterPage: true,
enlargeFactor: 0.35, // より強い拡大効果
enlargeStrategy: CenterPageEnlargeStrategy.zoom, // ズーム戦略
viewportFraction: 0.42, // 左右のカードをより見せる
enableInfiniteScroll: widget.recommendations.length > 2,
autoPlay: false,
scrollPhysics: const BouncingScrollPhysics(), // よりスムーズなスクロール
onPageChanged: (index, reason) {
setState(() {
_currentIndex = index;
});
},
),
itemBuilder: (context, index, realIndex) {
final rec = widget.recommendations[index];
// 奥行き計算
final distance = (_currentIndex - index).abs();
final depth = distance == 0 ? 1.0 : math.max(0.6, 1.0 - (distance * 0.2));
return _buildCarouselCard(rec, index == _currentIndex, depth);
},
),
),
const SizedBox(height: 12),
// スムーズなインジケーター
_buildSmoothIndicator(),
],
);
}
Widget _buildSmoothIndicator() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
widget.recommendations.length,
(index) {
final isActive = _currentIndex == index;
return GestureDetector(
onTap: () {
_carouselController.animateToPage(index);
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
width: isActive ? 20 : 8,
height: 8,
margin: const EdgeInsets.symmetric(horizontal: 3),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: isActive
? Theme.of(context).colorScheme.secondary
: Colors.grey[400],
boxShadow: isActive
? [
BoxShadow(
color: Theme.of(context).colorScheme.secondary.withValues(alpha: 0.4),
blurRadius: 4,
spreadRadius: 1,
),
]
: null,
),
),
);
},
),
);
}
Widget _buildCarouselCard(RecommendedSake rec, bool isCurrent, double depth) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => SakeDetailScreen(sake: rec.item),
),
);
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 400),
curve: Curves.easeOutCubic, // よりスムーズなカーブ
margin: EdgeInsets.symmetric(
vertical: isCurrent ? 0 : 16 * (1 - depth + 0.4),
horizontal: 6,
),
// 3D変形効果perspective + scale
transform: Matrix4.diagonal3Values(depth, depth, 1.0)
..setEntry(3, 2, 0.001), // パースペクティブ
transformAlignment: Alignment.center,
child: Card(
elevation: isCurrent ? 16 : 4 * depth,
shadowColor: Colors.black.withValues(alpha: 0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(isCurrent ? 20 : 16),
),
clipBehavior: Clip.antiAliasWithSaveLayer,
child: Stack(
fit: StackFit.expand,
children: [
// 背景画像
rec.item.displayData.imagePaths.isNotEmpty
? Image.file(
File(rec.item.displayData.imagePaths.first),
fit: BoxFit.cover,
)
: Container(
color: Colors.grey[300],
child: Icon(
LucideIcons.image,
size: 50,
color: Colors.grey[600],
),
),
// グラデーションオーバーレイ(より滑らか)
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: const [0.3, 0.7, 1.0],
colors: [
Colors.transparent,
Colors.black.withValues(alpha: 0.3),
Colors.black.withValues(alpha: 0.85),
],
),
),
),
// テキスト情報
Positioned(
bottom: 14,
left: 10,
right: 10,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 300),
opacity: isCurrent ? 1.0 : 0.7,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(
rec.item.displayData.displayName,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: isCurrent ? 15 : 13,
fontWeight: FontWeight.bold,
shadows: const [
Shadow(color: Colors.black, blurRadius: 6),
],
),
),
const SizedBox(height: 4),
Text(
rec.item.displayData.displayPrefecture,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.9),
fontSize: isCurrent ? 11 : 10,
shadows: const [
Shadow(color: Colors.black, blurRadius: 4),
],
),
),
const SizedBox(height: 8),
// 推薦理由(中央カードのみ完全表示)
AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: EdgeInsets.symmetric(
horizontal: isCurrent ? 10 : 6,
vertical: isCurrent ? 5 : 3,
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondary.withValues(alpha: isCurrent ? 0.95 : 0.7),
borderRadius: BorderRadius.circular(6),
boxShadow: isCurrent
? [
BoxShadow(
color: Theme.of(context).colorScheme.secondary.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
]
: null,
),
child: Text(
rec.reason,
style: TextStyle(
color: Colors.white,
fontSize: isCurrent ? 11 : 9,
fontWeight: FontWeight.w600,
),
maxLines: isCurrent ? 2 : 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
),
],
),
),
),
// 中央のカードにグロー効果
if (isCurrent)
Positioned(
top: 0,
left: 0,
right: 0,
height: 3,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.transparent,
Theme.of(context).colorScheme.secondary.withValues(alpha: 0.8),
Colors.transparent,
],
),
),
),
),
],
),
),
),
);
}
}