Mouse Follower for Flutter Applications

Mohamed Okaily
5 min readOct 15, 2023

--

Mouse Follower is a powerful package that empowers you to create custom mouse followers for your applications, breaking through design limitations. With additional features such as the flexibility to design any shape you want, uncomplicated properties, and ease of use, it offers unparalleled flexibility and creativity in enhancing user interactions.

https://pub.dev/packages/mouse_follower

Mouse Follower, Mohamed Okaily
Mouse Follower

In this article we will go through the basics and understand its implementation then we will some fun animated styles, as in this website

🔩 Installation

Add to your pubspec.yaml:

dependencies:
mouse_follower:

import the library path:

import 'package:mouse_follower/mouse_follower.dart';

⚙️ Configuration app

Add the MouseFollower widget like in the example.

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MouseFollower(
showDefaultMouse: true,
child:MyHomePage(),
)
);
}
Default Style

Just as simple as that, now you are using the default style, but we want to create our own animated styles:

Applied Applications for this article

Let’s start with the Beat Ring:

import 'package:flutter/material.dart';

class BeatRing extends StatefulWidget {
final double size;
final Color color;

const BeatRing({
Key? key,
required this.color,
required this.size,
}) : super(key: key);

@override
State<BeatRing> createState() => _BeatState();
}

class _BeatState extends State<BeatRing> with SingleTickerProviderStateMixin {
late AnimationController _animationController;

/// init the controller with duration of 1 seconds
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1000),
)..repeat();
}

@override
Widget build(BuildContext context) {
final double size = widget.size;
final Color color = widget.color;

return AnimatedBuilder(
animation: _animationController,
builder: (_, __) {
final double value = _animationController.value;
final double strokeWidth = size / 15;
final bool isVisible = value <= 0.7;

return SizedBox(
width: size,
height: size,
child: Stack(
alignment: Alignment.center,
children: [
/// Draw the animated ring start from point middle and expand
if (isVisible)
Visibility(
visible: isVisible,
child: Transform.scale(
scale: Tween(begin: 0.15, end: 1.0)
.animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.7, curve: Curves.easeInCubic),
))
.value,
child: Opacity(
opacity: Tween(begin: 0.0, end: 1.0)
.animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.2),
))
.value,
child: Ring(
color: color,
strokeWidth: strokeWidth,
size: size,
),
),
),
),
Ring(
color: color,
strokeWidth: strokeWidth,
size: size,
),
/// Add the small expand and shrink effect
if (value <= 0.8 && value >= 0.7)
Visibility(
visible: value <= 0.8 && value >= 0.7,
child: Transform.scale(
scale: Tween(begin: 1.0, end: 1.15)
.animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.7, 0.8),
))
.value,
child: Ring(
color: color,
strokeWidth: strokeWidth,
size: size,
),
),
),
if (value >= 0.8)
Visibility(
visible: value >= 0.8,
child: Transform.scale(
scale: Tween(begin: 1.15, end: 1.0)
.animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.8, 0.9),
))
.value,
child: Ring(
color: color,
strokeWidth: strokeWidth,
size: size,
),
),
),
],
),
);
},
);
}

@override
void dispose() {
_animationController.dispose();
super.dispose();
}
}

/// Ring Shape
class Ring extends StatelessWidget {
final Color color;
final double strokeWidth;
final double size;

const Ring({super.key,
required this.color,
required this.strokeWidth,
required this.size,
});

@override
Widget build(BuildContext context) {
return CustomPaint(
size: Size(size, size),
painter: _RingPainter(color: color, strokeWidth: strokeWidth),
);
}
}

class _RingPainter extends CustomPainter {
final Color color;
final double strokeWidth;

_RingPainter({
required this.color,
required this.strokeWidth,
});

@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth;
canvas.drawCircle(
Offset(size.width / 2, size.height / 2),
size.height / 2,
paint,
);
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

Now let’s implement it as the default mouse follower

...
/// change the size, color and opacity as you want
home: const MouseFollower(mouseStylesStack: [
MouseStyle(
size: Size(7, 7),
latency: Duration(milliseconds: 25),
opacity: 0.9,
decoration: BoxDecoration(color: Colors.deepPurple, shape: BoxShape.circle),
visibleOnHover: false),
MouseStyle(
size: Size(26, 26),
latency: Duration(milliseconds: 75),
visibleOnHover: false,
opacity: 0.7,
child: BeatRing(color: Colors.deepPurple, size: 26),
),
], child: MyHomePage()),
...
Beat Ring Style

Now let’s add the default OnHover style

home: const MouseFollower(
onHoverMouseCursor: SystemMouseCursors.click,
mouseStylesStack: [
....
],
onHoverMouseStylesStack: [
MouseStyle(
opacity: 0.4,
size: Size(70, 70),
decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.deepPurple),
)
],
child: MyHomePage()),

To use this style you have to wrap any widget you want with MouseOnHoverEvent

        MouseOnHoverEvent(
child: const Text(
"OnHover No Animation",
textAlign: TextAlign.center,
)),
Default OnHover Style

Let’s create the latest animation which is BeatAnimationCirclebecause it’s a circle that expands and shrinks.

import 'package:flutter/material.dart';

class BeatAnimationCircle extends StatefulWidget {
final Color color;
final Color borderColor;

const BeatAnimationCircle({super.key, this.color = Colors.transparent, this.borderColor = Colors.transparent});

@override
State<BeatAnimationCircle> createState() => _BeatAnimationCircleState();
}

class _BeatAnimationCircleState extends State<BeatAnimationCircle>
with SingleTickerProviderStateMixin {

late AnimationController _controller;
late Animation<double> _scaleAnimation;

@override
void initState() {
super.initState();
/// simply you have to add a reserve duration with lower duration
/// to give the feeling of beating
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 600),
reverseDuration: const Duration(milliseconds: 300),
)..repeat(reverse: true);

/// init a scale animation to change it's scale with easeInOut animation
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.2).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Container(
decoration: BoxDecoration(
color: widget.color,
shape: BoxShape.circle,
border: Border.all(color:widget.borderColor)
),
),
);
},
);
}
}
class AnimatedOnHoverStyle extends StatelessWidget {
final Widget child;
const AnimatedOnHoverStyle({super.key, required this.child});

@override
Widget build(BuildContext context) {
return MouseOnHoverEvent(
onHoverMouseCursor: SystemMouseCursors.click,
customOnHoverMouseStylesStack: const [
MouseStyle(
opacity: 0.4,
size: Size(70, 70),
latency: Duration(milliseconds: 25),
child: BeatAnimationCircle(color: Colors.deepPurple),
),
],
child: child,
);
}
}

Now we will use AnimatedOnHoverStyle immediately and wrap any widget we want to add the circle beats effect with:

              AnimatedOnHoverStyle(
child: const Text(
"OnHover Animation",
textAlign: TextAlign.center,
)),

Conclusion

The limitation of this package only depends on you, as you can almost wrap any kind of widget with this package, I recommend taking your time and exploring more options you have :)

If you liked the package I will be happy if you support me with a coffee :)

## Donations

## Follow me on
https://www.linkedin.com/in/mokaily/

--

--

Mohamed Okaily

Hello, I bring over 5 years of expertise as a Flutter developer, coupled with proficiency in UI/UX design. Additionally, I delve into junior data science.