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 recommendations; final double height; const Sake3DCarouselWithReason({ super.key, required this.recommendations, this.height = 260, }); @override State createState() => _Sake3DCarouselWithReasonState(); } class _Sake3DCarouselWithReasonState extends State { 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, ], ), ), ), ), ], ), ), ), ); } }