Files
doyin/assets/icons/slider_test.dart
2025-08-04 16:06:42 +05:00

693 lines
18 KiB
Dart

import 'dart:async';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class OnBoardingSlider extends StatefulWidget {
const OnBoardingSlider({Key? key}) : super(key: key);
@override
State<OnBoardingSlider> createState() => _OnBoardingSliderState();
}
class _OnBoardingSliderState extends State<OnBoardingSlider> {
final PageController _pageController = PageController();
int _currentPage = 0;
@override
var imageList = [
'assets/s1.png',
'assets/s2.png',
'assets/s3.png',
'assets/s4.png',
'assets/s5.png'
];
int currentIndex = 0;
Widget buildDot(int index) {
return Container(
margin: EdgeInsets.all(5),
width: 10,
height: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: currentIndex == index ? Colors.white : Colors.grey,
),
);
}
Widget buildGallery3D() {
return Gallery3D(
controller: Gallery3DController(
itemCount: imageList.length,
ellipseHeight: 0,
autoLoop: true,
minScale: 0.4,
delayTime: 2000,
scrollTime: 1000,
),
width: MediaQuery.of(context).size.width,
height: 300,
isClip: true,
onItemChanged: (index) {
setState(
() {
currentIndex = index;
_currentPage = index;
},
);
},
itemConfig: const GalleryItemConfig(
width: 180,
height: 850,
radius: 10,
isShowTransformMask: false,
),
onClickItem: (index) {
if (kDebugMode) print("currentIndex:$index");
},
itemBuilder: (context, index) {
return Image.asset(
imageList[index],
fit: BoxFit.fill,
);
},
);
}
@override
Widget build(BuildContext context) {
var screenHeight = MediaQuery.of(context).size.height;
return Expanded(
child: Column(
children: [
// Expanded(
// child:
Container(
height: screenHeight / 4,
child: PageView.builder(
controller: _pageController,
itemCount: imageList.length,
itemBuilder: ((context, index) {
return buildGallery3D();
}),
onPageChanged: (index) {
setState(() {
_currentPage = index;
});
},
),
// ),
),
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
imageList.length,
(index) => buildDot(index),
),
),
],
),
);
}
}
class Gallery3D extends StatefulWidget {
final double? height;
final double width;
final IndexedWidgetBuilder itemBuilder;
final ValueChanged<int>? onItemChanged;
final ValueChanged<int>? onClickItem;
final Gallery3DController controller;
final GalleryItemConfig itemConfig;
final EdgeInsetsGeometry? padding;
final bool isClip;
Gallery3D(
{Key? key,
this.onClickItem,
this.onItemChanged,
this.isClip = true,
this.height,
this.padding,
required this.itemConfig,
required this.controller,
required this.width,
required this.itemBuilder})
: super(key: key);
@override
_Gallery3DState createState() => _Gallery3DState();
}
class _Gallery3DState extends State<Gallery3D>
with TickerProviderStateMixin, WidgetsBindingObserver, Gallery3DMixin {
List<Widget> _galleryItemWidgetList = [];
AnimationController? _autoScrollAnimationController;
Timer? _timer;
late Gallery3DController controller = widget.controller;
AppLifecycleState appLifecycleState = AppLifecycleState.resumed;
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
appLifecycleState = state;
super.didChangeAppLifecycleState(state);
}
@override
void initState() {
controller.widgetWidth = widget.width;
controller.vsync = this;
controller.init(widget.itemConfig);
_updateWidgetIndexOnStack();
if (controller.autoLoop) {
this._timer =
Timer.periodic(Duration(milliseconds: controller.delayTime), (timer) {
if (!mounted) return;
if (appLifecycleState != AppLifecycleState.resumed) return;
if (DateTime.now().millisecondsSinceEpoch - _lastTouchMillisecond <
controller.delayTime) return;
if (_isTouching) return;
animateTo(controller.getOffsetAngleFormTargetIndex(
getNextIndex(controller.currentIndex)));
});
}
WidgetsBinding.instance.addObserver(this);
super.initState();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_timer?.cancel();
_timer = null;
_autoScrollAnimationController?.stop(canceled: true);
super.dispose();
}
@override
void animateTo(angle) {
_isTouching = true;
_lastTouchMillisecond = DateTime.now().millisecondsSinceEpoch;
_scrollToAngle(angle);
}
@override
void jumpTo(angle) {
setState(() {
_updateAllGalleryItemTransformByAngle(angle);
});
}
var _isTouching = false;
var _lastTouchMillisecond = 0;
Offset? _panDownLocation;
Offset? _lastUpdateLocation;
@override
Widget build(BuildContext context) {
return Container(
width: widget.width,
height: widget.height ?? widget.itemConfig.height,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onHorizontalDragCancel: (() {
_onFingerUp();
}),
onHorizontalDragDown: (details) {
_isTouching = true;
_panDownLocation = details.localPosition;
_lastUpdateLocation = details.localPosition;
_lastTouchMillisecond = DateTime.now().millisecondsSinceEpoch;
},
onHorizontalDragEnd: (details) {
_onFingerUp();
},
onHorizontalDragStart: (details) {},
onHorizontalDragUpdate: (details) {
setState(() {
_lastUpdateLocation = details.localPosition;
_lastTouchMillisecond = DateTime.now().millisecondsSinceEpoch;
_updateAllGalleryItemTransformByOffsetDx(details.delta.dx);
});
},
child: _buildWidgetList(),
),
);
}
Widget _buildWidgetList() {
if (widget.isClip) {
return ClipRect(
child: Stack(
children: _galleryItemWidgetList,
));
}
return Stack(
children: _galleryItemWidgetList,
);
}
void _scrollToAngle(double angle) {
_autoScrollAnimationController =
AnimationController(duration: Duration(milliseconds: 100), vsync: this);
Animation animation;
if (angle.ceil().abs() == 0) return;
animation =
Tween(begin: 0.0, end: angle).animate(_autoScrollAnimationController!);
double lastValue = 0;
animation.addListener(() {
setState(() {
_updateAllGalleryItemTransformByAngle(animation.value - lastValue);
lastValue = animation.value;
});
});
_autoScrollAnimationController?.forward();
_autoScrollAnimationController?.addListener(() {
if (_autoScrollAnimationController != null &&
_autoScrollAnimationController!.isCompleted) {
_isTouching = false;
}
});
}
void _onFingerUp() {
if (_lastUpdateLocation == null) {
_isTouching = false;
return;
}
double angle = controller.getTransformInfo(controller.currentIndex).angle;
double targetAngle = 0;
var offsetX = _lastUpdateLocation!.dx - _panDownLocation!.dx;
// Adjust the threshold for considering it a swipe
double swipeThreshold = widget.width * 0.1;
if (offsetX.abs() > swipeThreshold) {
// Swipe detected
if (offsetX > 0) {
// Swipe to the right, move to the bottom
targetAngle = controller
.getTransformInfo(getPreIndex(controller.currentIndex))
.angle +
180; // Move to the bottom
} else {
// Swipe to the left
targetAngle = angle + 180; // Maintain the current position
}
} else {
// No swipe, move to the bottom from the current position
targetAngle = angle + 180; // Move to the bottom
}
_scrollToAngle(targetAngle);
}
void _updateAllGalleryItemTransformByAngle(double angle) {
controller.updateTransformByAngle(angle);
_updateAllGalleryItemTransform();
}
void _updateAllGalleryItemTransformByOffsetDx(double offsetDx) {
controller.updateTransformByOffsetDx(offsetDx);
_updateAllGalleryItemTransform();
}
void _updateAllGalleryItemTransform() {
for (var i = 0; i < controller.getTransformInfoListSize(); i++) {
var item = controller.getTransformInfo(i);
if (item.angle > 180 - controller.unitAngle / 2 &&
item.angle < 180 + controller.unitAngle / 2) {
if (controller.currentIndex != i) {
controller.currentIndex = i;
widget.onItemChanged?.call(controller.currentIndex);
}
}
_updateWidgetIndexOnStack();
}
}
/// Get Pre Index >>>
int getPreIndex(int index) {
var preIndex = index - 1;
if (preIndex < 0) {
preIndex = controller.itemCount - 1;
}
return preIndex;
}
/// Get Next Index >>>
int getNextIndex(int index) {
var nextIndex = index + 1;
if (nextIndex == controller.itemCount) {
nextIndex = 0;
}
return nextIndex;
}
List<GalleryItem> _leftWidgetList = [];
List<GalleryItem> _rightWidgetList = [];
List<GalleryItem> _tempList = [];
/// Update Widget Index >>>
void _updateWidgetIndexOnStack() {
_leftWidgetList.clear();
_rightWidgetList.clear();
_tempList.clear();
for (var i = 0; i < controller.getTransformInfoListSize(); i++) {
var angle = controller.getTransformInfo(i).angle;
if (angle >= 180 + controller.unitAngle / 2) {
_leftWidgetList.add(_buildGalleryItem(
i,
));
} else {
_rightWidgetList.add(_buildGalleryItem(
i,
));
}
}
_rightWidgetList.sort((widget1, widget2) =>
widget1.transformInfo.angle.compareTo(widget2.transformInfo.angle));
_rightWidgetList.forEach((element) {
if (element.transformInfo.angle < controller.unitAngle / 2) {
element.transformInfo.angle += 360;
_tempList.add(element);
}
});
_tempList.forEach((element) {
_rightWidgetList.remove(element);
});
_leftWidgetList.sort((widget1, widget2) =>
widget2.transformInfo.angle.compareTo(widget1.transformInfo.angle));
_galleryItemWidgetList = [
..._leftWidgetList,
..._rightWidgetList,
];
}
/// Build Gallery Item >>>>>
GalleryItem _buildGalleryItem(int index) {
return GalleryItem(
index: index,
ellipseHeight: controller.ellipseHeight,
builder: widget.itemBuilder,
config: widget.itemConfig,
onClick: (index) {
if (widget.onClickItem != null && index == controller.currentIndex) {
widget.onClickItem?.call(index);
}
},
transformInfo: controller.getTransformInfo(index),
);
}
}
class _GalleryItemTransformInfo {
Offset offset;
double scale;
double angle;
int index;
_GalleryItemTransformInfo(
{required this.index,
this.scale = 1,
this.angle = 0,
this.offset = Offset.zero});
}
class GalleryItem extends StatelessWidget {
final GalleryItemConfig config;
final double ellipseHeight;
final int index;
final IndexedWidgetBuilder builder;
final ValueChanged<int>? onClick;
final _GalleryItemTransformInfo transformInfo;
final double minScale;
GalleryItem({
Key? key,
required this.index,
required this.transformInfo,
required this.config,
required this.builder,
this.minScale = 0.4,
this.onClick,
this.ellipseHeight = 0,
}) : super(key: key);
Widget _buildItem(BuildContext context) {
return Container(
width: config.width,
height: config.height,
child: builder(context, index));
}
Widget _buildMaskTransformItem(Widget child) {
if (!config.isShowTransformMask) return child;
return Stack(children: [
child,
Container(
width: config.width,
height: config.height,
color: Color.fromARGB(
100 * (1 - transformInfo.scale) ~/ (1 - minScale), 0, 0, 0),
)
]);
}
Widget _buildRadiusItem(Widget child) {
if (config.radius <= 0) return child;
return ClipRRect(
borderRadius: BorderRadius.circular(config.radius), child: child);
}
Widget _buildShadowItem(Widget child) {
if (config.shadows.isEmpty) return child;
return Container(
child: child, decoration: BoxDecoration(boxShadow: config.shadows));
}
@override
Widget build(BuildContext context) {
return Transform.translate(
offset: transformInfo.offset,
child: Container(
width: config.width,
height: config.height,
child: Transform.scale(
scale: transformInfo.scale,
child: InkWell(
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
onTap: () => onClick?.call(index),
child: _buildShadowItem(
_buildRadiusItem(_buildMaskTransformItem(_buildItem(context)))),
),
),
),
);
}
}
/// Gallery Item Config >>>>>
class GalleryItemConfig {
final double width;
final double height;
final double radius;
final List<BoxShadow> shadows;
final bool isShowTransformMask;
const GalleryItemConfig(
{this.width = 220,
this.height = 600,
this.radius = 0,
this.isShowTransformMask = true,
this.shadows = const []});
}
class Gallery3DController {
double perimeter = 0;
double unitAngle = 0;
final double minScale;
double widgetWidth = 0;
double ellipseHeight;
int itemCount;
late GalleryItemConfig itemConfig;
int currentIndex = 0;
final int delayTime;
final int scrollTime;
final bool autoLoop;
late Gallery3DMixin vsync;
List<_GalleryItemTransformInfo> _galleryItemTransformInfoList = [];
double baseAngleOffset = 0;
Gallery3DController(
{required this.itemCount,
this.ellipseHeight = 0,
this.autoLoop = true,
this.minScale = 0.4,
this.delayTime = 5000,
this.scrollTime = 1000})
: assert(itemCount >= 3, 'ItemCount must be greater than or equal to 3');
void init(GalleryItemConfig itemConfig) {
this.itemConfig = itemConfig;
unitAngle = 360 / itemCount;
perimeter = calculatePerimeter(widgetWidth * 0.7, 50);
_galleryItemTransformInfoList.clear();
for (var i = 0; i < itemCount; i++) {
var itemAngle = getItemAngle(i);
_galleryItemTransformInfoList.add(_GalleryItemTransformInfo(
index: i,
angle: itemAngle,
scale: calculateScale(itemAngle),
offset: calculateOffset(itemAngle)));
}
}
_GalleryItemTransformInfo getTransformInfo(int index) {
return _galleryItemTransformInfoList[index];
}
int getTransformInfoListSize() {
return _galleryItemTransformInfoList.length;
}
double getItemAngle(int index) {
double angle = 360 - (index * unitAngle + 180) % 360;
return angle;
}
void updateTransformByAngle(double offsetAngle) {
baseAngleOffset -= offsetAngle;
for (int index = 0; index < _galleryItemTransformInfoList.length; index++) {
_GalleryItemTransformInfo transformInfo =
_galleryItemTransformInfoList[index];
double angle = getItemAngle(index);
double scale = transformInfo.scale;
Offset offset = transformInfo.offset;
if (baseAngleOffset.abs() > 360) {
baseAngleOffset %= 360;
}
angle += baseAngleOffset;
angle = angle % 360;
offset = calculateOffset(angle);
scale = calculateScale(angle);
transformInfo
..angle = angle
..scale = scale
..offset = offset;
}
}
void updateTransformByOffsetDx(double offsetDx) {
double offsetAngle = offsetDx / perimeter / 2 * 360;
updateTransformByAngle(offsetAngle);
}
/// Calculate Scale >>>>
double calculateScale(double angle) {
angle = angle % 360;
if (angle > 180) {
angle = 360 - angle;
}
angle += 30;
var scale = angle / 180.0;
if (scale > 1) {
scale = 1;
} else if (scale < minScale) {
scale = minScale;
}
return scale;
}
/// Calculate Offset >>>>
Offset calculateOffset(double angle) {
double width = widgetWidth * 1;
double radiusOuterX = width / 2.4;
double radiusOuterY = ellipseHeight;
double angleOuter = (4 * pi / 360) * angle;
double x = radiusOuterX * sin(angleOuter);
double y = radiusOuterY > 0 ? radiusOuterY * cos(angleOuter) : 0;
return Offset(x + (widgetWidth - itemConfig.width) / 2, -y);
}
/// Calculate Perimeter >>>>
double calculatePerimeter(double width, double height) {
var a = width;
// var a = width * 0.8;
var b = height;
return 2 * pi * b + 4 * (a - b);
}
/// Get Final Angle >>>>
double getFinalAngle(double angle) {
if (angle >= 360) {
angle -= 360;
} else if (angle < 0) {
angle += 360;
}
return angle;
}
double getOffsetAngleFormTargetIndex(int index) {
double targetItemAngle = getItemAngle(index) + baseAngleOffset;
double offsetAngle = targetItemAngle % 180;
if (targetItemAngle < 180 || targetItemAngle > 360) {
offsetAngle = offsetAngle - 180;
}
return offsetAngle;
}
void animateTo(int index) {
if (index == currentIndex) return;
vsync.animateTo(getOffsetAngleFormTargetIndex(index));
}
void jumpTo(int index) {
if (index == currentIndex) return;
vsync.jumpTo(getOffsetAngleFormTargetIndex(index));
}
}
mixin class Gallery3DMixin {
void animateTo(angle) {}
void jumpTo(angle) {}
}