import 'package:flutter/material.dart'; class BounceTap extends StatefulWidget { final Widget child; final VoidCallback? onTap; final HitTestBehavior behavior; final Duration pressDuration; final Duration releaseDuration; const BounceTap({ super.key, required this.child, this.onTap, this.behavior = HitTestBehavior.opaque, this.pressDuration = const Duration(milliseconds: 80), this.releaseDuration = const Duration(milliseconds: 120), }); @override State createState() => _BounceTapState(); } class _BounceTapState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller; late final Animation _scale; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, value: 1, lowerBound: 0.94, upperBound: 1, duration: widget.pressDuration, reverseDuration: widget.releaseDuration, ); _scale = CurvedAnimation( parent: _controller, curve: Curves.easeOut, reverseCurve: Curves.elasticOut, ); } void _press() { if (widget.onTap == null) return; _controller.animateTo( 0.94, duration: widget.pressDuration, curve: Curves.easeOut, ); } void _release({bool triggerTap = false}) { if (widget.onTap == null) return; _controller.animateTo( 1, duration: widget.releaseDuration, curve: Curves.elasticOut, ); if (triggerTap) { widget.onTap?.call(); } } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Listener( behavior: widget.behavior, onPointerDown: (_) => _press(), onPointerCancel: (_) => _release(), onPointerUp: (_) => _release(triggerTap: true), child: AnimatedBuilder( animation: _scale, child: widget.child, builder: (context, child) => Transform.scale( scale: _scale.value, child: child, ), ), ); } }