import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) => MaterialApp(theme: ThemeData(), home: MyHomePage());
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTapDown: generateRipplePointer,
      child: Scaffold(
        body: Stack(children: ripplePointerList),
      ),
    );
  }

  List<RipplePointer> ripplePointerList = <RipplePointer>[];
  Future<void> generateRipplePointer(TapDownDetails details) async {
    const duration = const Duration(milliseconds: 800);
    final ripplePointer = RipplePointer(
      key: UniqueKey(),
      offset: details.globalPosition,
      duration: duration,
    );
    setState(() {
      ripplePointerList.add(ripplePointer);
    });
    await Future<void>.delayed(duration);
    setState(() {
      ripplePointerList.removeAt(0);
    });
  }
}

class RipplePointer extends StatefulWidget {
  const RipplePointer({Key key, @required this.offset, @required this.duration}) : super(key: key);
  final Offset offset;
  final Duration duration;

  @override
  _RipplePointerState createState() => _RipplePointerState();
}

class _RipplePointerState extends State<RipplePointer> with SingleTickerProviderStateMixin {
  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: RipplePainter(controller: controller, offset: widget.offset),
    );
  }

  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      vsync: this,
      duration: widget.duration,
    );
    controller.forward();
  }

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

class RipplePainter extends CustomPainter {
  RipplePainter({@required this.controller, @required this.offset}) : super(repaint: controller);
  final Offset offset;
  final Animation<double> controller;

  @override
  void paint(Canvas canvas, Size size) {
    final circleValue = Tween<double>(begin: 8, end: 80)
        .animate(
          controller.drive(
            CurveTween(
              curve: Curves.easeOutExpo,
            ),
          ),
        )
        .value;
    final widthValue = Tween<double>(begin: 12, end: 2)
        .animate(
          controller.drive(
            CurveTween(
              curve: Curves.easeInOut,
            ),
          ),
        )
        .value;
    final opacityValue = Tween<double>(begin: 1, end: 0)
        .animate(
          controller.drive(
            CurveTween(
              curve: Curves.easeInOut,
            ),
          ),
        )
        .value;

    final paint = Paint()
      ..style = PaintingStyle.stroke
      ..color = Colors.blue.withOpacity(opacityValue)
      ..strokeWidth = widthValue;
    canvas.drawCircle(offset, circleValue, paint);
  }

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


View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.