Browse Source

[feat]添加预览页,左右移动时候的icon

云天逵 1 year ago
parent
commit
333d24dde3

BIN
assets/images/icon_photo_preview_delete.webp


BIN
assets/images/icon_photo_preview_keep.webp


+ 5 - 0
lib/module/photo_preview/photo_preview_controller.dart

@@ -34,6 +34,7 @@ class PhotoPreviewController extends BaseController
   final RxBool isSwiperEnd = false.obs;
   RxInt groupIndex = 0.obs;
 
+
   late PhotosType photosType;
   late String? currentImageId;
   RxDouble selectedFilesSize = 0.0.obs;
@@ -210,7 +211,9 @@ class PhotoPreviewController extends BaseController
 
   void recoverSelectPhoto() {
     print('PhotoPreviewController recoverSelectPhoto');
+
     cardSwiperController.value.undo();
+
   }
 
   void clickSelect() {
@@ -310,6 +313,7 @@ class PhotoPreviewController extends BaseController
     int currentIndex,
     CardSwiperDirection direction,
   ) {
+
     print(
         'The card $currentIndex was swiped back to the ${direction.name}. Now the card $previousIndex is on top');
 
@@ -325,6 +329,7 @@ class PhotoPreviewController extends BaseController
     selectedFileCount.value = selectedPhotosIds.length;
     updateSelectedFilesSize();
     PhotoManager.clearFileCache();
+
     return true;
   }
 

+ 65 - 11
lib/module/photo_preview/photo_preview_view.dart

@@ -3,6 +3,8 @@ import 'package:clean/data/bean/photos_type.dart';
 import 'package:clean/module/photo_preview/photo_preview_controller.dart';
 import 'package:clean/resource/assets.gen.dart';
 import 'package:clean/router/app_pages.dart';
+import 'package:clean/utils/toast_util.dart';
+import 'package:clean/widget/photo_swipe_card.dart';
 import 'package:flutter/Material.dart';
 import 'package:flutter/src/widgets/framework.dart';
 import 'package:flutter_card_swiper/flutter_card_swiper.dart';
@@ -57,7 +59,7 @@ class PhotoPreviewPage extends BasePage<PhotoPreviewController> {
                   children: [
                     _titleCard(),
                     Spacer(),
-                    Container(
+                    SizedBox(
                       width: 314.w,
                       height: 392.h,
                       child: CardSwiper(
@@ -79,15 +81,69 @@ class PhotoPreviewPage extends BasePage<PhotoPreviewController> {
                             index,
                             horizontalOffsetPercentage,
                             verticalOffsetPercentage) {
+                          print(
+                              'index: $index, horizontalOffsetPercentage: $horizontalOffsetPercentage, verticalOffsetPercentage: $verticalOffsetPercentage');
                           final assetEntity = controller.listAssetEntity[index];
-                          return ClipRRect(
-                            borderRadius: BorderRadius.circular(20.r),
-                            child: AssetEntityImage(
-                              assetEntity,
-                              width: 314.w,
-                              height: 392.h,
-                              fit: BoxFit.cover,
-                            ),
+                          return Stack(
+                            children: [
+                              ClipRRect(
+                                borderRadius: BorderRadius.circular(20.r),
+                                child: AssetEntityImage(
+                                  assetEntity,
+                                  width: 314.w,
+                                  height: 392.h,
+                                  fit: BoxFit.cover,
+                                ),
+                              ),
+                              if (horizontalOffsetPercentage != 0)
+                                Positioned(
+                                  top: 0,
+                                  right: horizontalOffsetPercentage < -10
+                                      ? 0
+                                      : null,
+                                  left: horizontalOffsetPercentage > 10
+                                      ? 0
+                                      : null,
+                                  child: Container(
+                                      child: Column(
+                                    children: [
+                                      (horizontalOffsetPercentage < -10)
+                                          ? Assets.images.iconPhotoPreviewDelete
+                                              .image(
+                                              width: 60.w,
+                                              height: 60.w,
+                                            )
+                                          : (horizontalOffsetPercentage > 10)
+                                              ? Assets
+                                                  .images.iconPhotoPreviewKeep
+                                                  .image(
+                                                  width: 60.w,
+                                                  height: 60.w,
+                                                )
+                                              : SizedBox(),
+                                      (horizontalOffsetPercentage < -10)
+                                          ? Text(
+                                              'Delete ',
+                                              style: TextStyle(
+                                                color: Colors.white,
+                                                fontSize: 24.sp,
+                                                fontWeight: FontWeight.w500,
+                                              ),
+                                            )
+                                          : (horizontalOffsetPercentage > 10)
+                                              ? Text(
+                                                  'Keep',
+                                                  style: TextStyle(
+                                                    color: Colors.white,
+                                                    fontSize: 24.sp,
+                                                    fontWeight: FontWeight.w500,
+                                                  ),
+                                                )
+                                              : SizedBox(),
+                                    ],
+                                  )),
+                                ),
+                            ],
                           );
                         },
                       ),
@@ -108,14 +164,12 @@ class PhotoPreviewPage extends BasePage<PhotoPreviewController> {
           return IgnorePointer(
             child: Assets.images.bgPhotoSelectedPreviewFinish.image(
               width: 360.w,
-
             ),
           );
         } else {
           return IgnorePointer(
             child: Assets.images.bgHome.image(
               width: 360.w,
-
             ),
           );
         }

+ 286 - 0
lib/widget/photo_swipe_card.dart

@@ -0,0 +1,286 @@
+import 'dart:math' as math;
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:photo_manager/photo_manager.dart';
+import 'package:wechat_assets_picker/wechat_assets_picker.dart';
+
+class PhotoSwipeCard extends StatefulWidget {
+  final List<AssetEntity> assets;
+  final Function(int)? onIndexChanged;
+  final Function(AssetEntity, bool)?
+      onActionSelected; // true for keep, false for delete
+  final Function()? onEnd;
+  final bool enableVerticalDrag;
+  final bool loop;
+  final double threshold;
+
+  const PhotoSwipeCard({
+    super.key,
+    required this.assets,
+    this.onIndexChanged,
+    this.onActionSelected,
+    this.onEnd,
+    this.enableVerticalDrag = false,
+    this.loop = false,
+    this.threshold = 0.5,
+  });
+
+  @override
+  State<PhotoSwipeCard> createState() => _PhotoSwipeCardState();
+}
+
+class _PhotoSwipeCardState extends State<PhotoSwipeCard>
+    with SingleTickerProviderStateMixin {
+  late AnimationController _controller;
+  late Animation<Offset> _animation;
+  late Animation<double> _rotation;
+  Offset _dragOffset = Offset.zero;
+  int currentIndex = 0;
+
+  // 新增:弧形动画相关计算
+  double get _rotationAngle =>
+      (_dragOffset.dx / MediaQuery.of(context).size.width) * 0.3;
+
+  double get _verticalOffset => -math.sin(_rotationAngle * 2) * 100;
+
+  double get _nextCardScale {
+    final double dragPercent =
+        _dragOffset.dx.abs() / (MediaQuery.of(context).size.width * 0.3);
+    return 0.9 + (0.1 * math.min(1.0, dragPercent));
+  }
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = AnimationController(
+      duration: const Duration(milliseconds: 300),
+      vsync: this,
+    );
+    _initializeAnimations();
+  }
+
+  void _initializeAnimations() {
+    _animation = Tween<Offset>(begin: Offset.zero, end: Offset.zero)
+        .animate(CurvedAnimation(
+      parent: _controller,
+      curve: Curves.easeOut,
+    ));
+    _rotation = Tween<double>(begin: 0, end: 0).animate(CurvedAnimation(
+      parent: _controller,
+      curve: Curves.easeOut,
+    ));
+  }
+
+  void _onPanStart(DragStartDetails details) {
+    _controller.reset();
+  }
+
+  void _onPanUpdate(DragUpdateDetails details) {
+    setState(() {
+      if (widget.enableVerticalDrag) {
+        _dragOffset += details.delta;
+      } else {
+        _dragOffset += Offset(details.delta.dx, 0);
+      }
+    });
+  }
+
+  void _onPanCancel() {
+    _returnToCenter();
+  }
+
+  void _returnToCenter() {
+    setState(() {
+      _dragOffset = Offset.zero; // 重置偏移量
+    });
+  }
+
+  void _onPanEnd(DragEndDetails details) {
+    final double threshold =
+        widget.threshold * MediaQuery.of(context).size.width;
+    final velocity = details.velocity.pixelsPerSecond.dx;
+    final isFlick = velocity.abs() > 1000;
+
+    if (_dragOffset.dx.abs() > threshold || isFlick) {
+      final bool isKeepAction = _dragOffset.dx > 0;
+      final bool isLastCard = currentIndex >= widget.assets.length - 1;
+
+      // 添加弧形退出动画
+      final endRotation = _dragOffset.dx > 0 ? 0.3 : -0.3;
+      final endVerticalOffset = -math.sin(endRotation * 2) * 100;
+
+      _animation = Tween<Offset>(
+        begin: _dragOffset,
+        end: Offset(
+          _dragOffset.dx > 0
+              ? MediaQuery.of(context).size.width * 1.5
+              : -MediaQuery.of(context).size.width * 1.5,
+          endVerticalOffset,
+        ),
+      ).animate(CurvedAnimation(
+        parent: _controller,
+        curve: Curves.easeOut,
+      ));
+
+      _controller.forward().then((_) {
+        widget.onActionSelected
+            ?.call(widget.assets[currentIndex], isKeepAction);
+
+        setState(() {
+          if (isLastCard) {
+            if (widget.loop) {
+              currentIndex = 0;
+              _controller.addListener(_onLoopStarted);
+            } else {
+              currentIndex = widget.assets.length;
+              widget.onEnd?.call();
+            }
+          } else {
+            currentIndex++;
+            if (widget.loop && currentIndex == widget.assets.length) {
+              currentIndex = 0;
+            }
+          }
+          _dragOffset = Offset.zero;
+          widget.onIndexChanged?.call(currentIndex);
+        });
+
+        _controller.reset();
+        _initializeAnimations();
+      });
+    } else {
+      _returnToCenter();
+    }
+  }
+
+  void _onLoopStarted() {
+    if (currentIndex == 0) {
+      widget.onEnd?.call();
+      _controller.removeListener(_onLoopStarted);
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Stack(
+      children: [
+        // 下一张卡片
+        if (_hasNextCard())
+          Positioned.fill(
+            child: AnimatedBuilder(
+              animation: _controller,
+              builder: (context, child) {
+                return Transform.scale(
+                  scale: _nextCardScale,
+                  child: Card(
+                    elevation: 4,
+                    shape: RoundedRectangleBorder(
+                      borderRadius: BorderRadius.circular(20),
+                    ),
+                    child: ClipRRect(
+                      borderRadius: BorderRadius.circular(20),
+                      child: AssetEntityImage(
+                        widget.assets[_getNextIndex()],
+                        width: 314.w,
+                        height: 392.h,
+                        fit: BoxFit.cover,
+                      ),
+                    ),
+                  ),
+                );
+              },
+            ),
+          ),
+        // 当前卡片
+        if (currentIndex < widget.assets.length)
+          AnimatedBuilder(
+            animation: _controller,
+            builder: (context, child) {
+              final offset = _animation.value + _dragOffset;
+              return Transform(
+                transform: Matrix4.identity()
+                  ..translate(offset.dx, _verticalOffset + offset.dy)
+                  ..rotateZ(_rotationAngle)
+                  ..setEntry(3, 2, 0.001), // 添加透视效果
+                alignment: Alignment.center,
+                child: Stack(
+                  children: [
+                    Card(
+                      elevation: 8,
+                      shape: RoundedRectangleBorder(
+                        borderRadius: BorderRadius.circular(20),
+                      ),
+                      child: GestureDetector(
+                        onPanStart: _onPanStart,
+                        onPanUpdate: _onPanUpdate,
+                        onPanEnd: _onPanEnd,
+                        onPanCancel: _onPanCancel,
+                        child: ClipRRect(
+                          borderRadius: BorderRadius.circular(20),
+                          child: Stack(
+                            children: [
+                              AssetEntityImage(
+                                widget.assets[currentIndex],
+                                width: 314.w,
+                                height: 392.h,
+                                fit: BoxFit.cover,
+                              ),
+                              // 添加操作提示UI
+                              if (_dragOffset.dx != 0)
+                                Positioned(
+                                  top: 20,
+                                  right: _dragOffset.dx < 0 ? 20 : null,
+                                  left: _dragOffset.dx > 0 ? 20 : null,
+                                  child: Container(
+                                    padding: EdgeInsets.symmetric(
+                                        horizontal: 20.w, vertical: 10.h),
+                                    decoration: BoxDecoration(
+                                      color: _dragOffset.dx > 0
+                                          ? Colors.blue.withOpacity(0.8)
+                                          : Colors.red.withOpacity(0.8),
+                                      borderRadius: BorderRadius.circular(20),
+                                    ),
+                                    child: Text(
+                                      _dragOffset.dx > 0 ? 'Keep' : 'Delete',
+                                      style: TextStyle(
+                                        color: Colors.white,
+                                        fontSize: 16.sp,
+                                        fontWeight: FontWeight.bold,
+                                      ),
+                                    ),
+                                  ),
+                                ),
+                            ],
+                          ),
+                        ),
+                      ),
+                    ),
+                  ],
+                ),
+              );
+            },
+          ),
+      ],
+    );
+  }
+
+  bool _hasNextCard() {
+    if (widget.loop) {
+      return widget.assets.length > 1;
+    }
+    return currentIndex < widget.assets.length - 1;
+  }
+
+  int _getNextIndex() {
+    if (widget.loop) {
+      return (currentIndex + 1) % widget.assets.length;
+    }
+    return currentIndex + 1;
+  }
+
+  @override
+  void dispose() {
+    _controller.dispose();
+    super.dispose();
+  }
+}