| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286 |
- 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();
- }
- }
|