import 'package:flutter/material.dart'; import '../models/subscription_model.dart'; import '../utils/haptic_feedback_helper.dart'; import 'subscription_card.dart'; import '../theme/app_colors.dart'; class SwipeableSubscriptionCard extends StatefulWidget { final SubscriptionModel subscription; final VoidCallback? onEdit; final VoidCallback? onDelete; final VoidCallback? onTap; const SwipeableSubscriptionCard({ super.key, required this.subscription, this.onEdit, this.onDelete, this.onTap, }); @override State createState() => _SwipeableSubscriptionCardState(); } class _SwipeableSubscriptionCardState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _animation; double _dragStartX = 0; double _dragExtent = 0; bool _isSwipingLeft = false; bool _hapticTriggered = false; static const double _swipeThreshold = 80.0; static const double _deleteThreshold = 150.0; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); _animation = Tween( begin: 0.0, end: 0.0, ).animate(CurvedAnimation( parent: _controller, curve: Curves.easeOutExpo, )); } @override void dispose() { _controller.dispose(); super.dispose(); } void _handleDragStart(DragStartDetails details) { _dragStartX = details.localPosition.dx; _hapticTriggered = false; } void _handleDragUpdate(DragUpdateDetails details) { final delta = details.localPosition.dx - _dragStartX; setState(() { _dragExtent = delta; _isSwipingLeft = delta < 0; }); // 햅틱 피드백 트리거 if (!_hapticTriggered && _dragExtent.abs() > _swipeThreshold) { _hapticTriggered = true; HapticFeedbackHelper.mediumImpact(); } // 삭제 임계값에 도달했을 때 강한 햅틱 if (_dragExtent.abs() > _deleteThreshold && _hapticTriggered) { HapticFeedbackHelper.heavyImpact(); _hapticTriggered = false; // 반복 방지 } } void _handleDragEnd(DragEndDetails details) { final velocity = details.velocity.pixelsPerSecond.dx; final extent = _dragExtent.abs(); if (extent > _deleteThreshold || velocity.abs() > 800) { // 삭제 액션 if (_isSwipingLeft && widget.onDelete != null) { HapticFeedbackHelper.success(); _animateToOffset(-MediaQuery.of(context).size.width); Future.delayed(const Duration(milliseconds: 300), () { widget.onDelete!(); }); } else if (!_isSwipingLeft && widget.onEdit != null) { HapticFeedbackHelper.success(); _animateToOffset(MediaQuery.of(context).size.width); Future.delayed(const Duration(milliseconds: 300), () { widget.onEdit!(); }); } } else if (extent > _swipeThreshold) { // 액션 버튼 표시 HapticFeedbackHelper.lightImpact(); _animateToOffset(_isSwipingLeft ? -_swipeThreshold : _swipeThreshold); } else { // 원위치로 복귀 _animateToOffset(0); } } void _animateToOffset(double offset) { _animation = Tween( begin: _dragExtent, end: offset, ).animate(CurvedAnimation( parent: _controller, curve: Curves.easeOutExpo, )); _controller.forward(from: 0).then((_) { setState(() { _dragExtent = offset; }); }); } @override Widget build(BuildContext context) { return Stack( children: [ // 배경 액션 버튼들 Positioned.fill( child: Container( margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), color: _isSwipingLeft ? AppColors.dangerColor : AppColors.primaryColor, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // 편집 버튼 (오른쪽 스와이프) if (!_isSwipingLeft) Padding( padding: const EdgeInsets.only(left: 24), child: AnimatedOpacity( duration: const Duration(milliseconds: 200), opacity: _dragExtent > 40 ? 1.0 : 0.0, child: AnimatedScale( duration: const Duration(milliseconds: 200), scale: _dragExtent > 40 ? 1.0 : 0.5, child: const Icon( Icons.edit_rounded, color: Colors.white, size: 28, ), ), ), ), // 삭제 버튼 (왼쪽 스와이프) if (_isSwipingLeft) Padding( padding: const EdgeInsets.only(right: 24), child: AnimatedOpacity( duration: const Duration(milliseconds: 200), opacity: _dragExtent.abs() > 40 ? 1.0 : 0.0, child: AnimatedScale( duration: const Duration(milliseconds: 200), scale: _dragExtent.abs() > 40 ? 1.0 : 0.5, child: Icon( _dragExtent.abs() > _deleteThreshold ? Icons.delete_forever_rounded : Icons.delete_rounded, color: Colors.white, size: 28, ), ), ), ), ], ), ), ), // 스와이프 가능한 카드 AnimatedBuilder( animation: _animation, builder: (context, child) { return Transform.translate( offset: Offset(_animation.value, 0), child: child, ); }, child: GestureDetector( onHorizontalDragStart: _handleDragStart, onHorizontalDragUpdate: _handleDragUpdate, onHorizontalDragEnd: _handleDragEnd, child: Transform.translate( offset: Offset(_dragExtent, 0), child: Transform.scale( scale: 1.0 - (_dragExtent.abs() / 2000), child: Transform.rotate( angle: _dragExtent / 2000, child: GestureDetector( onTap: () { if (_dragExtent.abs() < 10) { widget.onTap?.call(); } }, child: SubscriptionCard( subscription: widget.subscription, ), ), ), ), ), ), ), ], ); } }