You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter实现组件后方自定义形状涟漪动画问题求助

解决方案:实现与子Widget形状一致的后方涟漪效果

核心思路

  • 用Stack布局将涟漪绘制层置于子Widget下方,实现"后方呈现"的视觉层级
  • 通过GlobalKey获取子Widget的渲染边界与形状参数,确保涟漪形状与子Widget完全匹配
  • 结合CustomPainter与动画控制器,绘制从中心向外扩散的同形状涟漪
  • 封装通用组件,支持适配圆角矩形、自定义形状等任意子Widget

可复用的RippleAnimation组件代码(ripple_effect.dart)

import 'package:flutter/material.dart';

class RippleAnimation extends StatefulWidget {
  final Widget child;
  final Duration duration;
  final Color rippleColor;
  final BorderRadius? borderRadius; // 适配圆角矩形
  final CustomClipper<Path>? customClipper; // 适配自定义形状

  const RippleAnimation({
    super.key,
    required this.child,
    this.duration = const Duration(milliseconds: 1500),
    this.rippleColor = Colors.grey,
    this.borderRadius,
    this.customClipper,
  });

  @override
  State<RippleAnimation> createState() => _RippleAnimationState();
}

class _RippleAnimationState extends State<RippleAnimation> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _scaleAnimation;
  final GlobalKey _childKey = GlobalKey();
  bool _isAnimating = false;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: widget.duration);
    _scaleAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeOut),
    );
    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        setState(() => _isAnimating = false);
        _controller.reset();
      }
    });
  }

  void _startRipple() {
    if (!_isAnimating) {
      setState(() => _isAnimating = true);
      _controller.forward();
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _startRipple,
      child: Stack(
        alignment: Alignment.center,
        children: [
          // 涟漪绘制层(置于子Widget下方)
          if (_isAnimating)
            AnimatedBuilder(
              animation: _scaleAnimation,
              builder: (context, child) {
                final renderBox = _childKey.currentContext?.findRenderObject() as RenderBox?;
                if (renderBox == null) return const SizedBox.shrink();
                final size = renderBox.size;

                return CustomPaint(
                  size: size,
                  painter: _RipplePainter(
                    scale: _scaleAnimation.value,
                    rippleColor: widget.rippleColor,
                    borderRadius: widget.borderRadius,
                    customClipper: widget.customClipper,
                  ),
                );
              },
            ),
          // 目标子Widget,通过GlobalKey获取尺寸参数
          Container(
            key: _childKey,
            child: widget.child,
          ),
        ],
      ),
    );
  }
}

class _RipplePainter extends CustomPainter {
  final double scale;
  final Color rippleColor;
  final BorderRadius? borderRadius;
  final CustomClipper<Path>? customClipper;

  _RipplePainter({
    required this.scale,
    required this.rippleColor,
    this.borderRadius,
    this.customClipper,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = rippleColor.withOpacity(0.3 * (1 - scale)) // 随扩散降低透明度
      ..style = PaintingStyle.fill;

    Path path;
    if (customClipper != null) {
      // 自定义形状使用传入的Clipper生成路径
      path = customClipper!.getClip(size);
    } else if (borderRadius != null) {
      // 圆角矩形生成对应路径
      path = Path()
        ..addRRect(RRect.fromRectAndCorners(
          Rect.fromLTWH(0, 0, size.width, size.height),
          topLeft: borderRadius!.topLeft,
          topRight: borderRadius!.topRight,
          bottomLeft: borderRadius!.bottomLeft,
          bottomRight: borderRadius!.bottomRight,
        ));
    } else {
      // 默认矩形路径
      path = Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
    }

    // 以Widget中心为原点缩放路径,实现从中心扩散的效果
    final center = Offset(size.width / 2, size.height / 2);
    final matrix = Matrix4.identity()
      ..translate(center.dx, center.dy)
      ..scale(scale)
      ..translate(-center.dx, -center.dy);
    final scaledPath = path.transform(matrix.storage);

    canvas.drawPath(scaledPath, paint);
  }

  @override
  bool shouldRepaint(covariant _RipplePainter oldDelegate) {
    return oldDelegate.scale != scale ||
        oldDelegate.rippleColor != rippleColor ||
        oldDelegate.borderRadius != borderRadius ||
        oldDelegate.customClipper != customClipper;
  }
}

测试页面使用示例(testpage.dart)

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

class TestRipplePage extends StatelessWidget {
  const TestRipplePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("自定义涟漪效果测试")),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // 圆角方形按钮示例
            RippleAnimation(
              borderRadius: BorderRadius.circular(12),
              rippleColor: Colors.blue,
              child: Container(
                width: 150,
                height: 50,
                decoration: BoxDecoration(
                  color: Colors.white,
                  borderRadius: BorderRadius.circular(12),
                  border: Border.all(color: Colors.blue),
                ),
                alignment: Alignment.center,
                child: const Text("圆角方形按钮", style: TextStyle(color: Colors.blue)),
              ),
            ),
            const SizedBox(height: 30),
            // 圆形按钮示例
            RippleAnimation(
              customClipper: _CircleClipper(),
              rippleColor: Colors.red,
              child: Container(
                width: 100,
                height: 100,
                decoration: const BoxDecoration(
                  color: Colors.white,
                  shape: BoxShape.circle,
                  border: Border.all(color: Colors.red),
                ),
                alignment: Alignment.center,
                child: const Text("圆形按钮", style: TextStyle(color: Colors.red)),
              ),
            ),
            const SizedBox(height: 30),
            // 普通矩形按钮示例
            RippleAnimation(
              rippleColor: Colors.green,
              child: Container(
                width: 150,
                height: 50,
                decoration: BoxDecoration(
                  color: Colors.white,
                  border: Border.all(color: Colors.green),
                ),
                alignment: Alignment.center,
                child: const Text("矩形按钮", style: TextStyle(color: Colors.green)),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// 自定义圆形Clipper
class _CircleClipper extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    return Path()..addOval(Rect.fromLTWH(0, 0, size.width, size.height));
  }

  @override
  bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
}

关键说明

  • 后方视觉效果:通过Stack的渲染顺序实现涟漪在子Widget下方展示
  • 形状适配:支持三种场景:默认矩形、指定圆角的矩形、自定义Clipper的任意形状
  • 动画逻辑:点击触发从中心向外缩放的涟漪,同时透明度随扩散逐渐降低,模拟自然水波效果
  • 复用性:仅需传入子Widget和对应形状参数,即可快速为任意Widget添加匹配的涟漪效果

内容的提问来源于stack exchange,提问作者Freshcart Engineering

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 11:20:56