Skip to content

Commit 77afe28

Browse files
committed
Add HoverFloatEffect
1 parent 4a45fe5 commit 77afe28

File tree

2 files changed

+90
-2
lines changed

2 files changed

+90
-2
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter/foundation.dart';
3+
4+
/// A widget that applies a floating hover effect to its child.
5+
///
6+
/// When the mouse hovers over this widget, it creates a subtle 3D-like effect
7+
/// by translating and scaling the child based on mouse position.
8+
///
9+
/// This effect is only applied on desktop platforms (Windows, macOS, Linux) and web.
10+
/// On mobile platforms, the child widget is returned without any hover effects.
11+
class HoverFloatEffect extends StatefulWidget {
12+
/// The widget to apply the hover effect to.
13+
final Widget child;
14+
15+
/// The scale factor applied to the child when hovering.
16+
/// A value of 1.0 means no scaling, 1.05 means 5% larger.
17+
/// Defaults to 1.05 for a subtle zoom effect.
18+
final double zoomScale;
19+
20+
/// The translation distance in pixels for the hover effect.
21+
/// Controls how far the widget moves from its center position.
22+
/// Defaults to 20 pixels for a subtle floating effect.
23+
final double translationDistance;
24+
25+
const HoverFloatEffect({
26+
super.key,
27+
required this.child,
28+
this.zoomScale = 1.05,
29+
this.translationDistance = 20,
30+
});
31+
32+
@override
33+
State<HoverFloatEffect> createState() => _HoverFloatEffectState();
34+
}
35+
36+
class _HoverFloatEffectState extends State<HoverFloatEffect> {
37+
Offset _offset = Offset.zero;
38+
39+
/// Returns true if the current platform supports mouse hover effects.
40+
bool get _supportsHover =>
41+
kIsWeb ||
42+
defaultTargetPlatform == TargetPlatform.windows ||
43+
defaultTargetPlatform == TargetPlatform.macOS ||
44+
defaultTargetPlatform == TargetPlatform.linux;
45+
46+
@override
47+
Widget build(BuildContext context) {
48+
// On mobile platforms, just return the child without hover effects
49+
if (!_supportsHover) {
50+
return widget.child;
51+
}
52+
53+
final size = MediaQuery.of(context).size;
54+
return MouseRegion(
55+
onHover: (event) {
56+
// Calculate normalized position (-0.5 to 0.5) and apply translation distance
57+
final dx =
58+
(event.position.dx / size.width - 0.5) * widget.translationDistance;
59+
final dy =
60+
(event.position.dy / size.height - 0.5) *
61+
widget.translationDistance;
62+
setState(() => _offset = Offset(dx, dy));
63+
},
64+
onExit: (_) => setState(() => _offset = Offset.zero),
65+
child: TweenAnimationBuilder(
66+
tween: Tween<Offset>(begin: Offset.zero, end: _offset),
67+
duration: const Duration(milliseconds: 200),
68+
curve: Curves.easeOut,
69+
builder: (context, offset, child) {
70+
return Transform.translate(
71+
offset: offset,
72+
child: Transform.scale(scale: widget.zoomScale, child: child),
73+
);
74+
},
75+
child: widget.child,
76+
),
77+
);
78+
}
79+
}

packages/flyer_chat_reactions/lib/src/widgets/reactions_dialog.dart

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:pull_down_button/pull_down_button.dart'
99
show PullDownMenuEntry, PullDownMenu;
1010

1111
import '../models/default_data.dart';
12+
import '../utils/hover_float_effect.dart';
1213
import '../utils/typedef.dart';
1314

1415
//// Theme values for [ReactionsDialogWidget].
@@ -40,6 +41,7 @@ class ReactionsDialogWidget extends StatefulWidget {
4041
this.menuItemTapAnimationDuration,
4142
this.reactionTapAnimationDuration,
4243
this.reactionPickerFadeLeftAnimationDuration,
44+
this.activateHoverFloatEffect = true,
4345
});
4446

4547
/// The message widget to be displayed in the dialog
@@ -95,6 +97,9 @@ class ReactionsDialogWidget extends StatefulWidget {
9597
/// Animation duration to display the reactions row
9698
final Duration? reactionPickerFadeLeftAnimationDuration;
9799

100+
/// Whether to activate the hover float effect
101+
final bool activateHoverFloatEffect;
102+
98103
@override
99104
State<ReactionsDialogWidget> createState() => _ReactionsDialogWidgetState();
100105
}
@@ -117,15 +122,17 @@ class _ReactionsDialogWidgetState extends State<ReactionsDialogWidget> {
117122
return BackdropFilter(
118123
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
119124
child: Padding(
120-
padding: const EdgeInsets.only(right: 20.0, left: 20.0),
125+
padding: const EdgeInsets.only(right: 32.0, left: 32.0),
121126
child: Column(
122127
mainAxisSize: MainAxisSize.max,
123128
crossAxisAlignment: widget.widgetAlignment,
124129
mainAxisAlignment: MainAxisAlignment.center,
125130
children: [
126131
buildReactionsPicker(context, theme),
127132
const SizedBox(height: 10),
128-
widget.messageWidget,
133+
widget.activateHoverFloatEffect
134+
? HoverFloatEffect(child: widget.messageWidget)
135+
: widget.messageWidget,
129136
if (widget.menuItems != null && widget.menuItems!.isNotEmpty) ...[
130137
const SizedBox(height: 10),
131138
PullDownMenu(items: widget.menuItems!),
@@ -264,6 +271,7 @@ void showReactionsDialog(
264271
Duration? reactionTapAnimationDuration,
265272
Duration? reactionPickerFadeLeftAnimationDuration,
266273
Widget? moreReactionsWidget,
274+
bool activateHoverFloatEffect = true,
267275
}) {
268276
final providers = ChatProviders.from(context);
269277

@@ -308,6 +316,7 @@ void showReactionsDialog(
308316
reactionPickerFadeLeftAnimationDuration:
309317
reactionPickerFadeLeftAnimationDuration,
310318
moreReactionsWidget: moreReactionsWidget,
319+
activateHoverFloatEffect: activateHoverFloatEffect,
311320
),
312321
),
313322
);

0 commit comments

Comments
 (0)