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
相关产品推荐
相关产品推荐

