215 lines
7.2 KiB
Dart
215 lines
7.2 KiB
Dart
import 'dart:async';
|
|
import 'dart:math' as math;
|
|
|
|
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;
|
|
|
|
const Carousel3D({
|
|
super.key,
|
|
required this.items,
|
|
this.height = 220,
|
|
this.autoPlayDuration = const Duration(seconds: 4),
|
|
this.onItemChanged,
|
|
this.showControls = true,
|
|
});
|
|
|
|
@override
|
|
State<Carousel3D> createState() => _Carousel3DState();
|
|
}
|
|
|
|
class _Carousel3DState extends State<Carousel3D> with SingleTickerProviderStateMixin {
|
|
late final AnimationController _controller;
|
|
Timer? _autoPlayTimer;
|
|
int _currentIndex = 0;
|
|
int _targetIndex = 0;
|
|
|
|
@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: [
|
|
SizedBox(
|
|
height: widget.height,
|
|
child: AnimatedBuilder(
|
|
animation: _controller,
|
|
builder: (context, child) {
|
|
return Stack(
|
|
alignment: Alignment.center,
|
|
children: List.generate(widget.items.length, (index) {
|
|
final double currentRelativePos = (index - _currentIndex).toDouble();
|
|
final double targetRelativePos = (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: GestureDetector(
|
|
onTap: () {
|
|
if (relativePos != 0) {
|
|
_goToIndex(index);
|
|
}
|
|
},
|
|
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,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
} |