I am trying to develop an effect similar to a ripple, but it is very smooth and pleasant. I have tried develop it using Animated and Custom Paint, but it doesn't turn out the same. Has anyone been able to achieve such an effect? Can you help me with any possible approaches?
Thank you so much!
This is my code using mixin animation:
class CircleAnimateWidget extends StatefulWidget {
final double sizeMin;
final double sizeMax;
final double containerSize;
final int duration;
final int reverseDuration;
const CircleAnimateWidget({super.key, required this.sizeMin, required this.duration, required this.sizeMax, required this.containerSize, required this.reverseDuration});
@override
State<StatefulWidget> createState() {
return _State();
}
}
class _State extends State<CircleAnimateWidget>
with TickerProviderStateMixin {
late Animation _sizeAnimation;
late AnimationController _animationController;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: widget.duration),
reverseDuration: Duration(milliseconds: widget.reverseDuration));
_sizeAnimation = Tween(begin: widget.sizeMin, end: widget.sizeMax).animate(CurvedAnimation(
curve: Curves.elasticOut,
reverseCurve: Curves.ease,
parent: _animationController));
_animationController.addStatusListener(
(AnimationStatus status) async {
if (status == AnimationStatus.completed) {
await Future.delayed(const Duration(milliseconds: 50));
_animationController.reverse();
} else if (status == AnimationStatus.dismissed) {
_animationController.forward();
}
},
);
_animationController.forward();
}
@override
void dispose() {
_animationController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Container(
width: widget.containerSize,
height: widget.containerSize,
alignment: Alignment.center,
child: SizedBox(
width: _sizeAnimation.value,
height: _sizeAnimation.value,
child: CircleAvatar(
backgroundColor: Colors.green.withOpacity(0.3),
child: const SizedBox(),
),
),
);
},
),
);
}
}
And put it to the stack:
[Positioned(
child: CircleAnimateWidget(
sizeMin: maxCircleSize - 20,
sizeMax: maxCircleSize,
duration: 1500,
reverseDuration: 500,
containerSize: maxCircleSize + 50,
)
),
Positioned(
child: CircleAnimateWidget(
sizeMin: maxCircleSize - 50,
sizeMax: maxCircleSize - 30,
duration: 1500,
reverseDuration: 500,
containerSize: maxCircleSize + 50,
)
),
Positioned(
child: CircleAnimateWidget(
sizeMin: maxCircleSize - 90,
sizeMax: maxCircleSize - 60,
duration: 1500,
reverseDuration: 500,
containerSize: maxCircleSize + 50,
)
)]
you can use a custom CustomPainter
like this:
class _TrueCallerPainter extends CustomPainter {
_TrueCallerPainter(this.animation) : super(repaint: animation);
final Animation<double> animation;
final ringData = [
(_curve(0.0), Colors.green.shade900.withOpacity(0.4)),
(_curve(0.5), Colors.green.shade900.withOpacity(0.6)),
(_curve(1.0), Colors.green.shade900),
];
static Curve _curve(double t) {
const delay = 0.25;
return Interval(lerpDouble(delay, 0, t)!, lerpDouble(1, 1 - delay, t)!);
}
@override
void paint(Canvas canvas, Size size) {
// timeDilation = 10;
final r = 0.85 * size.shortestSide / 2;
final paint = Paint();
double factor = 1;
for (final (curve, color) in ringData) {
final radius = r * factor - sin(curve.transform(animation.value) * 2 * pi) * r * 0.05;
canvas.drawCircle(size.center(Offset.zero), radius, paint..color = color);
factor -= 0.15;
}
}
@override
bool shouldRepaint(_TrueCallerPainter oldDelegate) => false;
}
and use it with:
class TrueCaller extends StatefulWidget {
@override
State<TrueCaller> createState() => _TrueCallerState();
}
class _TrueCallerState extends State<TrueCaller> with TickerProviderStateMixin {
late final controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 750));
@override
void initState() {
super.initState();
_loop();
}
_loop() async {
int i = 0;
while (i++ < 8) {
await Future.delayed(const Duration(seconds: 1));
await controller.forward(from: 0);
}
}
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(colors: [Colors.black, Colors.black87]),
),
child: CustomPaint(
painter: _TrueCallerPainter(controller),
child: SizedBox.expand(
child: Transform.scale(
scale: 0.5,
child: const FittedBox(child: Icon(Icons.shield, color: Colors.black45)),
),
),
),
);
}
}
note that it uses dart v3.0 and if you have older version replace ringData
's records with some Ring
class instances holding just two data fields: Curve
and Color
EDIT
if you dont want to use a CustomPaint
you can always use a workaround with three ScaleTransition
s:
class TrueCaller extends StatefulWidget {
@override
State<TrueCaller> createState() => _TrueCallerState();
}
class _TrueCallerState extends State<TrueCaller> with TickerProviderStateMixin {
late final controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 750));
final _curves = [0.0, 0.5, 1.0].map(_curve).toList();
@override
void initState() {
super.initState();
_loop();
}
static Curve _curve(double t) {
const delay = 0.25;
return Interval(lerpDouble(delay, 0, t)!, lerpDouble(1, 1 - delay, t)!);
}
_scale(Curve c, double f, double t) => f - sin(c.transform(t) * 2 * pi) * 0.05;
Widget _ring(Color color) => DecoratedBox(
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
);
_loop() async {
int i = 0;
while (i++ < 8) {
await Future.delayed(const Duration(seconds: 1));
await controller.forward(from: 0);
}
}
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(colors: [Colors.black, Colors.black87]),
),
child: Stack(
fit: StackFit.expand,
children: [
ScaleTransition(
scale: Animation.fromValueListenable(controller, transformer: (t) => _scale(_curves[0], 0.8, t)),
child: _ring(Colors.green.withOpacity(0.25)),
),
ScaleTransition(
scale: Animation.fromValueListenable(controller, transformer: (t) => _scale(_curves[1], 0.7, t)),
child: _ring(Colors.green.withOpacity(0.25)),
),
ScaleTransition(
scale: Animation.fromValueListenable(controller, transformer: (t) => _scale(_curves[2], 0.6, t)),
child: _ring(Colors.green.shade800),
),
Transform.scale(
scale: 0.5,
child: const FittedBox(child: Icon(Icons.shield, color: Colors.black45)),
),
],
),
);
}
}