photo_swipe_card.dart 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. import 'dart:math' as math;
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter_screenutil/flutter_screenutil.dart';
  4. import 'package:photo_manager/photo_manager.dart';
  5. import 'package:wechat_assets_picker/wechat_assets_picker.dart';
  6. class PhotoSwipeCard extends StatefulWidget {
  7. final List<AssetEntity> assets;
  8. final Function(int)? onIndexChanged;
  9. final Function(AssetEntity, bool)?
  10. onActionSelected; // true for keep, false for delete
  11. final Function()? onEnd;
  12. final bool enableVerticalDrag;
  13. final bool loop;
  14. final double threshold;
  15. const PhotoSwipeCard({
  16. super.key,
  17. required this.assets,
  18. this.onIndexChanged,
  19. this.onActionSelected,
  20. this.onEnd,
  21. this.enableVerticalDrag = false,
  22. this.loop = false,
  23. this.threshold = 0.5,
  24. });
  25. @override
  26. State<PhotoSwipeCard> createState() => _PhotoSwipeCardState();
  27. }
  28. class _PhotoSwipeCardState extends State<PhotoSwipeCard>
  29. with SingleTickerProviderStateMixin {
  30. late AnimationController _controller;
  31. late Animation<Offset> _animation;
  32. late Animation<double> _rotation;
  33. Offset _dragOffset = Offset.zero;
  34. int currentIndex = 0;
  35. // 新增:弧形动画相关计算
  36. double get _rotationAngle =>
  37. (_dragOffset.dx / MediaQuery.of(context).size.width) * 0.3;
  38. double get _verticalOffset => -math.sin(_rotationAngle * 2) * 100;
  39. double get _nextCardScale {
  40. final double dragPercent =
  41. _dragOffset.dx.abs() / (MediaQuery.of(context).size.width * 0.3);
  42. return 0.9 + (0.1 * math.min(1.0, dragPercent));
  43. }
  44. @override
  45. void initState() {
  46. super.initState();
  47. _controller = AnimationController(
  48. duration: const Duration(milliseconds: 300),
  49. vsync: this,
  50. );
  51. _initializeAnimations();
  52. }
  53. void _initializeAnimations() {
  54. _animation = Tween<Offset>(begin: Offset.zero, end: Offset.zero)
  55. .animate(CurvedAnimation(
  56. parent: _controller,
  57. curve: Curves.easeOut,
  58. ));
  59. _rotation = Tween<double>(begin: 0, end: 0).animate(CurvedAnimation(
  60. parent: _controller,
  61. curve: Curves.easeOut,
  62. ));
  63. }
  64. void _onPanStart(DragStartDetails details) {
  65. _controller.reset();
  66. }
  67. void _onPanUpdate(DragUpdateDetails details) {
  68. setState(() {
  69. if (widget.enableVerticalDrag) {
  70. _dragOffset += details.delta;
  71. } else {
  72. _dragOffset += Offset(details.delta.dx, 0);
  73. }
  74. });
  75. }
  76. void _onPanCancel() {
  77. _returnToCenter();
  78. }
  79. void _returnToCenter() {
  80. setState(() {
  81. _dragOffset = Offset.zero; // 重置偏移量
  82. });
  83. }
  84. void _onPanEnd(DragEndDetails details) {
  85. final double threshold =
  86. widget.threshold * MediaQuery.of(context).size.width;
  87. final velocity = details.velocity.pixelsPerSecond.dx;
  88. final isFlick = velocity.abs() > 1000;
  89. if (_dragOffset.dx.abs() > threshold || isFlick) {
  90. final bool isKeepAction = _dragOffset.dx > 0;
  91. final bool isLastCard = currentIndex >= widget.assets.length - 1;
  92. // 添加弧形退出动画
  93. final endRotation = _dragOffset.dx > 0 ? 0.3 : -0.3;
  94. final endVerticalOffset = -math.sin(endRotation * 2) * 100;
  95. _animation = Tween<Offset>(
  96. begin: _dragOffset,
  97. end: Offset(
  98. _dragOffset.dx > 0
  99. ? MediaQuery.of(context).size.width * 1.5
  100. : -MediaQuery.of(context).size.width * 1.5,
  101. endVerticalOffset,
  102. ),
  103. ).animate(CurvedAnimation(
  104. parent: _controller,
  105. curve: Curves.easeOut,
  106. ));
  107. _controller.forward().then((_) {
  108. widget.onActionSelected
  109. ?.call(widget.assets[currentIndex], isKeepAction);
  110. setState(() {
  111. if (isLastCard) {
  112. if (widget.loop) {
  113. currentIndex = 0;
  114. _controller.addListener(_onLoopStarted);
  115. } else {
  116. currentIndex = widget.assets.length;
  117. widget.onEnd?.call();
  118. }
  119. } else {
  120. currentIndex++;
  121. if (widget.loop && currentIndex == widget.assets.length) {
  122. currentIndex = 0;
  123. }
  124. }
  125. _dragOffset = Offset.zero;
  126. widget.onIndexChanged?.call(currentIndex);
  127. });
  128. _controller.reset();
  129. _initializeAnimations();
  130. });
  131. } else {
  132. _returnToCenter();
  133. }
  134. }
  135. void _onLoopStarted() {
  136. if (currentIndex == 0) {
  137. widget.onEnd?.call();
  138. _controller.removeListener(_onLoopStarted);
  139. }
  140. }
  141. @override
  142. Widget build(BuildContext context) {
  143. return Stack(
  144. children: [
  145. // 下一张卡片
  146. if (_hasNextCard())
  147. Positioned.fill(
  148. child: AnimatedBuilder(
  149. animation: _controller,
  150. builder: (context, child) {
  151. return Transform.scale(
  152. scale: _nextCardScale,
  153. child: Card(
  154. elevation: 4,
  155. shape: RoundedRectangleBorder(
  156. borderRadius: BorderRadius.circular(20),
  157. ),
  158. child: ClipRRect(
  159. borderRadius: BorderRadius.circular(20),
  160. child: AssetEntityImage(
  161. widget.assets[_getNextIndex()],
  162. width: 314.w,
  163. height: 392.h,
  164. fit: BoxFit.cover,
  165. ),
  166. ),
  167. ),
  168. );
  169. },
  170. ),
  171. ),
  172. // 当前卡片
  173. if (currentIndex < widget.assets.length)
  174. AnimatedBuilder(
  175. animation: _controller,
  176. builder: (context, child) {
  177. final offset = _animation.value + _dragOffset;
  178. return Transform(
  179. transform: Matrix4.identity()
  180. ..translate(offset.dx, _verticalOffset + offset.dy)
  181. ..rotateZ(_rotationAngle)
  182. ..setEntry(3, 2, 0.001), // 添加透视效果
  183. alignment: Alignment.center,
  184. child: Stack(
  185. children: [
  186. Card(
  187. elevation: 8,
  188. shape: RoundedRectangleBorder(
  189. borderRadius: BorderRadius.circular(20),
  190. ),
  191. child: GestureDetector(
  192. onPanStart: _onPanStart,
  193. onPanUpdate: _onPanUpdate,
  194. onPanEnd: _onPanEnd,
  195. onPanCancel: _onPanCancel,
  196. child: ClipRRect(
  197. borderRadius: BorderRadius.circular(20),
  198. child: Stack(
  199. children: [
  200. AssetEntityImage(
  201. widget.assets[currentIndex],
  202. width: 314.w,
  203. height: 392.h,
  204. fit: BoxFit.cover,
  205. ),
  206. // 添加操作提示UI
  207. if (_dragOffset.dx != 0)
  208. Positioned(
  209. top: 20,
  210. right: _dragOffset.dx < 0 ? 20 : null,
  211. left: _dragOffset.dx > 0 ? 20 : null,
  212. child: Container(
  213. padding: EdgeInsets.symmetric(
  214. horizontal: 20.w, vertical: 10.h),
  215. decoration: BoxDecoration(
  216. color: _dragOffset.dx > 0
  217. ? Colors.blue.withOpacity(0.8)
  218. : Colors.red.withOpacity(0.8),
  219. borderRadius: BorderRadius.circular(20),
  220. ),
  221. child: Text(
  222. _dragOffset.dx > 0 ? 'Keep' : 'Delete',
  223. style: TextStyle(
  224. color: Colors.white,
  225. fontSize: 16.sp,
  226. fontWeight: FontWeight.bold,
  227. ),
  228. ),
  229. ),
  230. ),
  231. ],
  232. ),
  233. ),
  234. ),
  235. ),
  236. ],
  237. ),
  238. );
  239. },
  240. ),
  241. ],
  242. );
  243. }
  244. bool _hasNextCard() {
  245. if (widget.loop) {
  246. return widget.assets.length > 1;
  247. }
  248. return currentIndex < widget.assets.length - 1;
  249. }
  250. int _getNextIndex() {
  251. if (widget.loop) {
  252. return (currentIndex + 1) % widget.assets.length;
  253. }
  254. return currentIndex + 1;
  255. }
  256. @override
  257. void dispose() {
  258. _controller.dispose();
  259. super.dispose();
  260. }
  261. }