admin管理员组

文章数量:1302317

I am currently struggling to create a ripple effect behind a given widget. I am only able to create circular ripples.

The requirements are as such

  • The ripple should appear as if coming from behind the center of the widget, in my case the rounded square button
  • The ripple should take the shape of the child widget, in my case the rounded square button.

I am trying to make the ripple_effect reusable such that any shape can become the child widget and the ripples will take the form and shape and render accordingly.

Any help will be highly appreciated.

ripple_effect.dart

import 'dart:async';
import 'package:flutter/material.dart';

class RippleAnimation extends StatefulWidget {
  const RippleAnimation({
    required this.child,
    this.color = Colors.black,
    this.delay = Duration.zero,
    this.repeat = false,
    this.minRadius = 10,
    this.maxRadius = 30,
    this.ripplesCount = 3,
    this.duration = const Duration(milliseconds: 2400),
    super.key,
  });

  final Widget child;
  final Duration delay;
  final double minRadius;
  final double maxRadius;
  final Color color;
  final int ripplesCount;
  final Duration duration;
  final bool repeat;

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

class RippleAnimationState extends State<RippleAnimation>
    with TickerProviderStateMixin<RippleAnimation> {
  late AnimationController _controller;
  late GlobalKey _childKey;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: widget.duration,
      vsync: this,
    );
    _childKey = GlobalKey();

    Timer(widget.delay, () {
      if (mounted) {
        widget.repeat ? _controller.repeat() : _controller.forward();
      }
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        return Stack(
          alignment: Alignment.center,
          children: [
            AnimatedBuilder(
              animation: _controller,
              builder: (context, child) {
                return CustomPaint(
                  size: Size(constraints.maxWidth, constraints.maxHeight),
                  painter: RipplePainter(
                    progress: _controller.value,
                    color: widget.color,
                    minRadius: widget.minRadius,
                    maxRadius: widget.maxRadius,
                    ripplesCount: widget.ripplesCount,
                    childKey: _childKey,
                  ),
                );
              },
            ),
            Container(key: _childKey, child: widget.child),
          ],
        );
      },
    );
  }
}

class RipplePainter extends CustomPainter {
  final double progress;
  final Color color;
  final double minRadius;
  final double maxRadius;
  final int ripplesCount;
  final GlobalKey childKey;

  RipplePainter({
    required this.progress,
    required this.color,
    required this.minRadius,
    required this.maxRadius,
    required this.ripplesCount,
    required this.childKey,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final RenderBox? childBox =
        childKey.currentContext?.findRenderObject() as RenderBox?;
    if (childBox == null) return;

    final Offset childPosition =
        childBox.localToGlobal(Offset.zero) - childBox.size.center(Offset.zero);
    final Path path = Path();
    path.addRect(Rect.fromLTWH(childPosition.dx, childPosition.dy,
        childBox.size.width, childBox.size.height));

    for (int i = 0; i < ripplesCount; i++) {
      final rippleProgress = (progress - (i / ripplesCount)).clamp(0.0, 1.0);
      final radius = minRadius + (maxRadius - minRadius) * rippleProgress;
      final opacity = 1.0 - rippleProgress;

      final paint = Paint()
        ..color = color.withValues(alpha: opacity)
        ..style = PaintingStyle.stroke
        ..strokeWidth = 2.0;

      canvas.save();
      canvas.clipPath(path);
      canvas.drawCircle(childBox.size.center(Offset.zero), radius, paint);
      canvas.restore();
    }
  }

  @override
  bool shouldRepaint(RipplePainter oldDelegate) {
    return oldDelegate.progress != progress;
  }
}

Usage within testpage.dart

import 'package:flutter/material.dart';
import 'package:freshcart/components/colors.dart';
import 'package:freshcart/components/ripple_effect.dart';
import 'package:freshcart/components/static_decoration.dart';
import 'package:freshcart/pages/home/widgets/drawer_widget.dart';
import 'package:freshcart/pages/home/widgets/sqbuttons_widgets.dart';

class PlaygroundPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20),
        child: Stack(
          children: [_buildBackButton(), _buildPlaygroundContent()],
        ),
      ),
    );
  }

  Widget _buildBackButton() {
    return AppBarButton(
      onTap: () async {
        await drawerController.open!();
      },
      svgPath: "assets/images/svg/menu.svg",
    );
  }

  Widget _buildPlaygroundContent() {
    return Align(
      alignment: Alignment.center,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text("Playground content"),
          height20,
          RippleAnimation(
            child: AppBarButton(
              svgPath: "assets/images/svg/basket.svg",
              onTap: () {},
            ),
            color: primaryAppColor,
            delay: const Duration(milliseconds: 500),
            repeat: true,
            minRadius: 10,
            maxRadius: 30,
            ripplesCount: 3,
            duration: const Duration(milliseconds: 6 * 400),
          ),
          height20,
        ],
      ),
    );
  }
}

My output so far

I have tried different varieties of animations / painters in flutter, but to no avail. LLMs are not helpful as well.

I am currently struggling to create a ripple effect behind a given widget. I am only able to create circular ripples.

The requirements are as such

  • The ripple should appear as if coming from behind the center of the widget, in my case the rounded square button
  • The ripple should take the shape of the child widget, in my case the rounded square button.

I am trying to make the ripple_effect reusable such that any shape can become the child widget and the ripples will take the form and shape and render accordingly.

Any help will be highly appreciated.

ripple_effect.dart

import 'dart:async';
import 'package:flutter/material.dart';

class RippleAnimation extends StatefulWidget {
  const RippleAnimation({
    required this.child,
    this.color = Colors.black,
    this.delay = Duration.zero,
    this.repeat = false,
    this.minRadius = 10,
    this.maxRadius = 30,
    this.ripplesCount = 3,
    this.duration = const Duration(milliseconds: 2400),
    super.key,
  });

  final Widget child;
  final Duration delay;
  final double minRadius;
  final double maxRadius;
  final Color color;
  final int ripplesCount;
  final Duration duration;
  final bool repeat;

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

class RippleAnimationState extends State<RippleAnimation>
    with TickerProviderStateMixin<RippleAnimation> {
  late AnimationController _controller;
  late GlobalKey _childKey;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: widget.duration,
      vsync: this,
    );
    _childKey = GlobalKey();

    Timer(widget.delay, () {
      if (mounted) {
        widget.repeat ? _controller.repeat() : _controller.forward();
      }
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        return Stack(
          alignment: Alignment.center,
          children: [
            AnimatedBuilder(
              animation: _controller,
              builder: (context, child) {
                return CustomPaint(
                  size: Size(constraints.maxWidth, constraints.maxHeight),
                  painter: RipplePainter(
                    progress: _controller.value,
                    color: widget.color,
                    minRadius: widget.minRadius,
                    maxRadius: widget.maxRadius,
                    ripplesCount: widget.ripplesCount,
                    childKey: _childKey,
                  ),
                );
              },
            ),
            Container(key: _childKey, child: widget.child),
          ],
        );
      },
    );
  }
}

class RipplePainter extends CustomPainter {
  final double progress;
  final Color color;
  final double minRadius;
  final double maxRadius;
  final int ripplesCount;
  final GlobalKey childKey;

  RipplePainter({
    required this.progress,
    required this.color,
    required this.minRadius,
    required this.maxRadius,
    required this.ripplesCount,
    required this.childKey,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final RenderBox? childBox =
        childKey.currentContext?.findRenderObject() as RenderBox?;
    if (childBox == null) return;

    final Offset childPosition =
        childBox.localToGlobal(Offset.zero) - childBox.size.center(Offset.zero);
    final Path path = Path();
    path.addRect(Rect.fromLTWH(childPosition.dx, childPosition.dy,
        childBox.size.width, childBox.size.height));

    for (int i = 0; i < ripplesCount; i++) {
      final rippleProgress = (progress - (i / ripplesCount)).clamp(0.0, 1.0);
      final radius = minRadius + (maxRadius - minRadius) * rippleProgress;
      final opacity = 1.0 - rippleProgress;

      final paint = Paint()
        ..color = color.withValues(alpha: opacity)
        ..style = PaintingStyle.stroke
        ..strokeWidth = 2.0;

      canvas.save();
      canvas.clipPath(path);
      canvas.drawCircle(childBox.size.center(Offset.zero), radius, paint);
      canvas.restore();
    }
  }

  @override
  bool shouldRepaint(RipplePainter oldDelegate) {
    return oldDelegate.progress != progress;
  }
}

Usage within testpage.dart

import 'package:flutter/material.dart';
import 'package:freshcart/components/colors.dart';
import 'package:freshcart/components/ripple_effect.dart';
import 'package:freshcart/components/static_decoration.dart';
import 'package:freshcart/pages/home/widgets/drawer_widget.dart';
import 'package:freshcart/pages/home/widgets/sqbuttons_widgets.dart';

class PlaygroundPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20),
        child: Stack(
          children: [_buildBackButton(), _buildPlaygroundContent()],
        ),
      ),
    );
  }

  Widget _buildBackButton() {
    return AppBarButton(
      onTap: () async {
        await drawerController.open!();
      },
      svgPath: "assets/images/svg/menu.svg",
    );
  }

  Widget _buildPlaygroundContent() {
    return Align(
      alignment: Alignment.center,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text("Playground content"),
          height20,
          RippleAnimation(
            child: AppBarButton(
              svgPath: "assets/images/svg/basket.svg",
              onTap: () {},
            ),
            color: primaryAppColor,
            delay: const Duration(milliseconds: 500),
            repeat: true,
            minRadius: 10,
            maxRadius: 30,
            ripplesCount: 3,
            duration: const Duration(milliseconds: 6 * 400),
          ),
          height20,
        ],
      ),
    );
  }
}

My output so far

I have tried different varieties of animations / painters in flutter, but to no avail. LLMs are not helpful as well.

Share asked Feb 11 at 6:32 Freshcart EngineeringFreshcart Engineering 111 silver badge1 bronze badge
Add a comment  | 

1 Answer 1

Reset to default 1

solved your error check it.

in simple_ripple_animation.dart

import 'dart:async';
import 'package:flutter/material.dart';

class RippleAnimation extends StatefulWidget {
  const RippleAnimation({
    required this.child,
    this.color = Colors.black,
    this.delay = Duration.zero,
    this.repeat = false,
    this.minRadius = 60,
    this.maxRadius = 120,
    this.ripplesCount = 5,
    this.borderRadius = 25.0, // Added border radius
    this.duration = const Duration(milliseconds: 2300),
    super.key,
  });

  final Widget child;
  final Duration delay;
  final double minRadius;
  final double maxRadius;
  final Color color;
  final int ripplesCount;
  final Duration duration;
  final bool repeat;
  final double borderRadius; // Border radius for rounded square

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

class RippleAnimationState extends State<RippleAnimation>
    with TickerProviderStateMixin<RippleAnimation> {
  AnimationController? _controller;

  @override
  void initState() {
    _controller = AnimationController(
      duration: widget.duration,
      vsync: this,
    );

    Timer? animationTimer;
    animationTimer = Timer(widget.delay, () {
      if (_controller != null && mounted) {
        widget.repeat ? _controller!.repeat() : _controller!.forward();
      }
      animationTimer?.cancel();
    });

    super.initState();
  }

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

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: SquareRipplePainter(
        _controller,
        color: widget.color,
        minRadius: widget.minRadius,
        maxRadius: widget.maxRadius,
        wavesCount: widget.ripplesCount + 2,
        borderRadius: widget.borderRadius, // Pass border radius
      ),
      child: widget.child,
    );
  }
}

/// Creating a Rounded Square Painter
class SquareRipplePainter extends CustomPainter {
  SquareRipplePainter(
      this.animation, {
        required this.wavesCount,
        required this.color,
        this.minRadius,
        this.maxRadius,
        this.borderRadius = 20.0, // Default rounded corners
      }) : super(repaint: animation);

  final Color color;
  final double? minRadius;
  final double? maxRadius;
  final int wavesCount;
  final double borderRadius; // Border radius for square ripples
  final Animation<double>? animation;

  @override
  void paint(Canvas canvas, Size size) {
    final Rect rect = Rect.fromLTRB(0, 0, size.width, size.height);
    for (int wave = 0; wave <= wavesCount; wave++) {
      drawSquareRipple(
        canvas,
        rect,
        minRadius,
        maxRadius,
        wave,
        animation!.value,
        wavesCount,
        color,
      );
    }
  }

  /// Creating animated rounded square ripples
  void drawSquareRipple(
      Canvas canvas,
      Rect rect,
      double? minRadius,
      double? maxRadius,
      int wave,
      double value,
      int? length,
      Color rippleColor,
      ) {
    if (wave != 0) {
      final double opacity =
      (1 - ((wave - 1) / length!) - value).clamp(0.0, 1.0);
      final Color fadedColor = rippleColor.withOpacity(opacity);

      final double sizeFactor = minRadius! + ((maxRadius! - minRadius) * value);
      final double rippleSize = sizeFactor * (1 + (wave * value)) * value;

      final Paint paint = Paint()..color = fadedColor;

      final RRect roundedSquare = RRect.fromRectAndRadius(
        Rect.fromCenter(
          center: rect.center,
          width: rippleSize,
          height: rippleSize,
        ),
        Radius.circular(borderRadius), // Rounded corners
      );

      canvas.drawRRect(roundedSquare, paint);
    }
  }

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

in testpage.dart

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:simple_ripple_animation/simple_ripple_animation.dart';

class PlaygroundPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20),
        child: Stack(
          children: [_buildPlaygroundContent()],
        ),
      ),
    );
  }

  Widget _buildPlaygroundContent() {
    return Align(
      alignment: Alignment.center,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          RippleAnimation(
            color: Colors.blueGrey,
            delay: const Duration(milliseconds: 500),
            repeat: true,
            minRadius: 30,
            maxRadius: 70, 
            ripplesCount: 4,
            duration: const Duration(milliseconds: 6 * 400),
            child: Container(
              width: 60.r, 
              height: 60.r, 
              decoration: BoxDecoration(
                color: Colors.white, 
                borderRadius: BorderRadius.circular(15.r), 
                boxShadow: [
                  BoxShadow(
                    color: Colors.black26,
                    blurRadius: 5,
                    spreadRadius: 2,
                  ),
                ],
              ),
              child: IconButton(
                onPressed: () {},
                icon: Image.asset(
                  'assets/pnb.png',
                  height: 40.r,
                  width: 40.r,
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

本文标签: flutterRipple effect behind a widget (no press)Stack Overflow