didvan-app/lib/views/widgets/carousel_3d.dart

315 lines
11 KiB
Dart

// 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<Widget> 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<Carousel3D> createState() => _Carousel3DState();
}
class _Carousel3DState extends State<Carousel3D>
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<double>(
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,
),
],
),
],
],
);
}
}