// ignore_for_file: deprecated_member_use import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; class Carousel3D extends StatefulWidget { final List items; final double height; final Duration autoPlayDuration; final Function(int)? onItemChanged; final bool showControls; final void Function(int index)? onItemTap; const Carousel3D({ super.key, required this.items, this.height = 220, this.autoPlayDuration = const Duration(seconds: 4), this.onItemChanged, this.showControls = true, this.onItemTap, }); @override State createState() => _Carousel3DState(); } class _Carousel3DState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller; Timer? _autoPlayTimer; int _currentIndex = 0; int _targetIndex = 0; double _dragDelta = 0.0; static const double _dragDistanceToComplete = 120.0; static const double _dragDistanceThreshold = 20.0; // was 40 static const double _dragVelocityThreshold = 150.0; // was 300 @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 150), ); _startAutoPlay(); } void _startAutoPlay() { _autoPlayTimer?.cancel(); _autoPlayTimer = Timer.periodic(widget.autoPlayDuration, (timer) { if (mounted) { _goToIndex((_currentIndex + 1) % widget.items.length); } }); } void _goToIndex(int index) { if (index == _currentIndex) return; setState(() { _targetIndex = index; }); _controller.forward(from: 0).then((_) { setState(() { _currentIndex = _targetIndex; }); widget.onItemChanged?.call(_currentIndex); _controller.reset(); }); _startAutoPlay(); } void _nextPage() { _goToIndex((_currentIndex + 1) % widget.items.length); } void _previousPage() { _goToIndex((_currentIndex - 1 + widget.items.length) % widget.items.length); } @override void dispose() { _autoPlayTimer?.cancel(); _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; return Column( children: [ GestureDetector( behavior: HitTestBehavior.opaque, onHorizontalDragStart: (_) { _autoPlayTimer?.cancel(); _dragDelta = 0.0; _controller.stop(); _controller.value = 0.0; }, onHorizontalDragUpdate: (details) { _dragDelta += details.delta.dx; final len = widget.items.length; final bool dragLeft = _dragDelta < 0; final int newTarget = dragLeft ? (_currentIndex - 1 + len) % len : (_currentIndex + 1) % len; if (newTarget != _targetIndex) { setState(() => _targetIndex = newTarget); } final double progress = (_dragDelta.abs() / _dragDistanceToComplete).clamp(0.0, 1.0); _controller.value = progress; }, onHorizontalDragEnd: (details) { final velocity = details.primaryVelocity ?? 0.0; final bool commit = velocity.abs() > _dragVelocityThreshold || _dragDelta.abs() > _dragDistanceThreshold; if (commit) { _controller .animateTo(1.0, duration: const Duration(milliseconds: 150), curve: Curves.easeOut) .then((_) { setState(() { _currentIndex = _targetIndex; }); widget.onItemChanged?.call(_currentIndex); _controller.value = 0.0; _dragDelta = 0.0; _startAutoPlay(); }); } else { setState(() { _targetIndex = _currentIndex; }); _controller .animateBack(0.0, duration: const Duration(milliseconds: 150), curve: Curves.easeOut) .then((_) { _dragDelta = 0.0; _startAutoPlay(); }); } }, child: SizedBox( height: widget.height, child: AnimatedBuilder( animation: _controller, builder: (context, child) { return Stack( alignment: Alignment.center, children: List.generate(widget.items.length, (index) { final int len = widget.items.length; final int half = len ~/ 2; int circularDelta(int i, int center) { int raw = i - center; if (raw > half) raw -= len; if (raw < -half) raw += len; return raw; } final double currentRelativePos = circularDelta(index, _currentIndex).toDouble(); final double targetRelativePos = circularDelta(index, _targetIndex).toDouble(); final double relativePos = Tween( begin: currentRelativePos, end: targetRelativePos, ).transform(CurvedAnimation( parent: _controller, curve: Curves.easeInOutCubic) .value); final bool isGrayscale = relativePos != 0; final double absRelativePos = relativePos.abs(); final double scale = (1 - (absRelativePos * 0.15)).clamp(0.0, 1.0); final double translateX = relativePos * -30.0; final double translateZ = absRelativePos * -100.0; final double opacity = (1 - (absRelativePos * 0.3)).clamp(0.0, 1.0); Matrix4 transform = Matrix4.identity() ..setEntry(3, 2, 0.001) ..translate(translateX, 0.0, translateZ) ..scale(scale); return Transform( transform: transform, alignment: Alignment.center, child: Opacity( opacity: opacity, child: IgnorePointer( ignoring: relativePos != 0, child: GestureDetector( onTap: relativePos == 0 ? () => widget.onItemTap?.call(index) : null, child: Container( width: screenWidth * 0.7, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black .withOpacity(isGrayscale ? 0.1 : 0.25), spreadRadius: 2, blurRadius: 10, offset: const Offset(0, 5), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: ColorFiltered( colorFilter: isGrayscale ? const ColorFilter.matrix([ 0.2126, 0.7152, 0.0722, 0, 0, 0.2126, 0.7152, 0.0722, 0, 0, 0.2126, 0.7152, 0.0722, 0, 0, 0, 0, 0, 1, 0, ]) : const ColorFilter.mode( Colors.transparent, BlendMode.multiply, ), child: widget.items[index], ), ), ), ), ), ), ); }).toList() ..sort((a, b) { final transformA = a.transform; final transformB = b.transform; return transformA .getTranslation() .z .compareTo(transformB.getTranslation().z); }), ); }, ), ), ), if (widget.showControls) ...[ const SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( icon: SvgPicture.asset('lib/assets/icons/Arrow Right.svg'), iconSize: 40, onPressed: _previousPage, ), const SizedBox(width: 20), Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(widget.items.length, (index) { return AnimatedContainer( duration: const Duration(milliseconds: 150), margin: const EdgeInsets.symmetric(horizontal: 3), width: _currentIndex == index ? 9.0 : 7.0, height: _currentIndex == index ? 9.0 : 7.0, decoration: BoxDecoration( color: _currentIndex == index ? const Color.fromARGB(255, 0, 126, 167) : Colors.grey.withOpacity(0.5), shape: BoxShape.circle, ), ); }), ), const SizedBox(width: 20), IconButton( icon: SvgPicture.asset('lib/assets/icons/Click Area.svg'), iconSize: 40, onPressed: _nextPage, ), ], ), ], ], ); } }