A fully customizable, OOP-based Flutter package that replicates the Instagram Reels double-tap heart animation — the heart pops up at the tap position, rotates, and flies to a target widget.
| Feature | Description |
|---|---|
| 🎯 Fly-to-Target | Heart flies from double-tap position to any target widget |
| 🎨 Custom Gradients | Use any gradient on the heart icon |
| 🔧 Custom Widget Builder | Replace the heart with your own widget (emoji, icon, lottie, etc.) |
| 📳 Haptic Control | Enable/disable/change haptic feedback type (light, medium, heavy, none) |
| ⚡ Animation Config | Customize duration, curves, scale, rotation, timing — 16 parameters |
| 🎭 Full OOP | Mixins, extensions, models, services — clean architecture |
| 📦 Zero Dependencies | Only depends on Flutter SDK |
| 🔁 DoubleTapDetector | Built-in gesture handler with position data |
Add to your pubspec.yaml:
dependencies:
instagram_like_animation_button: ^4.0.0Or with a Git reference:
dependencies:
instagram_like_animation_button:
git:
url: https://github.com/iambhabha/instagram_like_animation_button.gitThen run:
flutter pub getimport 'package:instagram_like_animation_button/instagram_like_animation_button.dart';class _MyPageState extends State<MyPage> {
final GlobalKey likeKey = GlobalKey();
bool isLiked = false;
TapDownDetails? tapDetails;
@override
Widget build(BuildContext context) {
return DoubleTapDetector(
onDoubleTap: (details) => setState(() => tapDetails = details),
child: Stack(
children: [
// Your content (image, video, reels, etc.)
Container(color: Colors.black),
// Target icon with GlobalKey
InkWell(
key: likeKey,
onTap: () => setState(() => isLiked = !isLiked),
child: Icon(
Icons.favorite,
color: isLiked ? Colors.red : Colors.white,
),
),
// Animation layer
if (tapDetails != null)
ReelAnimationLike(
key: ValueKey(tapDetails),
likeKey: likeKey,
position: tapDetails!.globalPosition,
onLikeCall: () => setState(() => isLiked = true),
onCompleteAnimation: () => setState(() => tapDetails = null),
),
],
),
);
}
}lib/
├── instagram_like_animation_button.dart ← Barrel export (single import)
└── src/
├── core/
│ ├── enums.dart ← HapticFeedbackType
│ └── typedefs.dart ← AnimationWidgetBuilder, callbacks
├── config/
│ └── like_animation_config.dart ← 16 configurable animation params
├── mixins/
│ └── haptic_feedback_mixin.dart ← Haptic feedback mixin
├── extensions/
│ ├── global_key_extensions.dart ← GlobalKey.centerPosition, etc.
│ └── offset_extensions.dart ← Offset.deltaTo()
├── models/
│ └── like_animation_style.dart ← Gradient, color, icon size
├── resources/
│ ├── asset_res.dart ← Asset path constants
│ ├── color_res.dart ← Color constants
│ └── style_res.dart ← Theme gradient
├── services/
│ ├── haptic_manager.dart ← Singleton haptic service
│ └── debounce_action.dart ← Singleton debounce service
└── widgets/
├── reel_animation_like.dart ← Main animation widget ⭐
├── heart_widget.dart ← Default heart icon
├── gradient_icon.dart ← Gradient ShaderMask wrapper
└── double_tap_detector.dart ← Double-tap gesture handler
The core widget. Place in a Stack — it animates from position → likeKey target.
| Parameter | Type | Required | Description |
|---|---|---|---|
likeKey |
GlobalKey |
✅ | Target widget key |
position |
Offset |
✅ | Tap position (global) |
config |
LikeAnimationConfig |
❌ | Animation parameters |
style |
LikeAnimationStyle |
❌ | Visual style |
builder |
AnimationWidgetBuilder? |
❌ | Custom widget builder |
onLikeCall |
VoidCallback? |
❌ | Called on like |
onCompleteAnimation |
VoidCallback? |
❌ | Called on animation end |
leftRightPosition |
double |
❌ | X offset adjustment |
topBottomPosition |
double |
❌ | Y offset adjustment |
ReelAnimationLike(
likeKey: targetKey,
position: tapPosition,
config: const LikeAnimationConfig(
duration: Duration(milliseconds: 800),
hapticType: HapticFeedbackType.medium,
scaleMax: 2.0,
),
style: const LikeAnimationStyle(
gradient: LinearGradient(colors: [Colors.pink, Colors.orange]),
iconSize: Size(60, 60),
),
onLikeCall: () => print('Liked!'),
)Important
If the animation's starting position doesn't look accurate (heart appears slightly off from the tap point), you can adjust it using these two parameters:
| Parameter | What It Does | When to Use |
|---|---|---|
leftRightPosition |
Shifts the animation horizontally (X-axis) | Heart appears too far left/right from tap |
topBottomPosition |
Shifts the animation vertically (Y-axis) | Heart appears too high/low from tap |
Why is this needed?
Every app has different layouts — AppBar, BottomNavigationBar, SafeArea, padding, etc. These affect where the tap coordinates land vs where the animation renders in the Stack. These offsets let you compensate for that difference.
How to use:
ReelAnimationLike(
likeKey: likeKey,
position: tapDetails!.globalPosition,
// 👇 Adjust these values until the heart appears exactly at the tap point
leftRightPosition: 8, // shift 8px to the left
topBottomPosition: 65, // shift 65px up (e.g., AppBar height)
onLikeCall: () {},
)Quick guide:
topBottomPosition: 56→ standard AppBar heighttopBottomPosition: 80→ AppBar + status barleftRightPosition: 0→ no horizontal adjustment needed (most cases)
💡 Tip: Start with 0 for both, double-tap on screen, and see where the heart appears. Then adjust the values until it lands exactly on your finger tap position.
Immutable config with copyWith() support. 16 customizable properties:
| Property | Type | Default | Description |
|---|---|---|---|
duration |
Duration |
600ms |
Total animation duration |
scaleBegin |
double |
0.0 |
Starting scale |
scaleMax |
double |
1.5 |
Peak scale (pop size) |
scaleEnd |
double |
0.7 |
Final scale |
scaleCurveUp |
Curve |
easeOut |
Scale-up curve |
scaleCurveDown |
Curve |
bounceIn |
Scale-down curve |
scaleUpWeight |
double |
30 |
Scale-up timing weight |
scaleDownWeight |
double |
100 |
Scale-down timing weight |
initialOffset |
Offset |
(0, -50) |
Offset from tap |
holdWeight |
double |
50 |
Hold phase weight |
flyWeight |
double |
50 |
Fly phase weight |
flyCurve |
Curve |
easeInOut |
Flight curve |
rotationRange |
double |
0.5 |
Max rotation (radians) |
hapticType |
HapticFeedbackType |
light |
Haptic feedback type |
fadeDuration |
Duration |
500ms |
Fade-out duration |
completionDelay |
Duration |
500ms |
Delay before onComplete |
// Disable haptic, fast animation
const config = LikeAnimationConfig(
duration: Duration(milliseconds: 400),
hapticType: HapticFeedbackType.none,
scaleMax: 2.0,
);
// Modify existing config
final modified = config.copyWith(hapticType: HapticFeedbackType.heavy);| Property | Type | Default | Description |
|---|---|---|---|
gradient |
Gradient? |
theme gradient | Gradient overlay |
iconColor |
Color? |
#FF2D55 |
Icon tint color |
iconSize |
Size |
80×80 |
Icon dimensions |
const style = LikeAnimationStyle(
gradient: LinearGradient(colors: [Colors.purple, Colors.blue]),
iconColor: Colors.pink,
iconSize: Size(50, 50),
);Detects double-taps with position data (Flutter's onDoubleTap doesn't provide this).
DoubleTapDetector(
onDoubleTap: (details) => print(details.globalPosition),
onTap: () => print('Single tap'),
behavior: HitTestBehavior.opaque,
child: YourContent(),
)| Value | Description |
|---|---|
none |
❌ No haptic |
light |
💫 Light tap (default) |
medium |
📳 Medium tap |
heavy |
💥 Heavy tap |
selection |
🔘 Selection click |
vibrate |
📱 Standard vibration |
HeartWidget(
style: LikeAnimationStyle(iconColor: Colors.pink, iconSize: Size(60, 60)),
)GradientIcon(
gradient: LinearGradient(colors: [Colors.red, Colors.orange]),
child: Icon(Icons.favorite, size: 48),
)// GlobalKeyX — on GlobalKey
final Offset? pos = myKey.globalPosition; // top-left
final Offset? center = myKey.centerPosition; // center
final Size? size = myKey.widgetSize; // size
// OffsetX — on Offset
final delta = tapPosition.deltaTo(targetPosition);// HapticManager (singleton)
HapticManager.instance.trigger(HapticFeedbackType.light);
HapticManager.instance.light();
HapticManager.instance.medium();
// DebounceAction (singleton)
DebounceAction.instance.call(() => print('Done'), milliseconds: 500);
DebounceAction.instance.cancel();AssetRes.icFillHeart // 'assets/ic_fill_heart.png'
AssetRes.icHeart // 'assets/ic_heart.png'
ColorRes.likeRed // #FF2D55
ColorRes.whitePure // #FFFFFF
ColorRes.blackPure // #000000
StyleRes.themeGradient // Default LinearGradientReplace the default heart with any widget — emoji, Lottie, custom icon:
ReelAnimationLike(
likeKey: likeKey,
position: tapPosition,
builder: (context, scale, rotation, opacity) {
return Opacity(
opacity: opacity,
child: Transform.scale(
scale: scale.value,
child: Transform.rotate(
angle: rotation.value,
child: Text('🔥', style: TextStyle(fontSize: 60)),
),
),
);
},
onLikeCall: () {},
)See the complete working example in example/lib/main.dart.
import 'package:flutter/material.dart';
import 'package:instagram_like_animation_button/instagram_like_animation_button.dart';
class ReelPage extends StatefulWidget {
const ReelPage({super.key});
@override
State<ReelPage> createState() => _ReelPageState();
}
class _ReelPageState extends State<ReelPage> {
final GlobalKey likeKey = GlobalKey();
bool isLiked = false;
TapDownDetails? tapDetails;
@override
Widget build(BuildContext context) {
return DoubleTapDetector(
onDoubleTap: (details) => setState(() => tapDetails = details),
child: Stack(
children: [
Container(color: Colors.black), // Your video/image
// Right-side icons (like Instagram Reels)
Positioned(
right: 16,
bottom: 100,
child: InkWell(
key: likeKey,
onTap: () => setState(() => isLiked = !isLiked),
child: Image.asset(
isLiked ? AssetRes.icFillHeart : AssetRes.icHeart,
width: 28, height: 28,
color: isLiked ? ColorRes.likeRed : ColorRes.whitePure,
package: 'instagram_like_animation_button',
),
),
),
// Animation
if (tapDetails != null)
ReelAnimationLike(
key: ValueKey(tapDetails),
likeKey: likeKey,
position: tapDetails!.globalPosition,
config: const LikeAnimationConfig(
hapticType: HapticFeedbackType.light,
),
style: const LikeAnimationStyle(iconSize: Size(50, 50)),
onLikeCall: () {
if (!isLiked) setState(() => isLiked = true);
},
onCompleteAnimation: () => setState(() => tapDetails = null),
),
],
),
);
}
}| Version | |
|---|---|
| Flutter | ≥1.17.0 |
| Dart | ^3.10.7 |
MIT License — see LICENSE for details.
Contributions are welcome! Please open an issue or submit a pull request.
Made with ❤️ by @iambhabha
