7 changed files with 755 additions and 562 deletions
@ -0,0 +1,104 @@ |
|||
import 'dart:ui'; |
|||
import 'package:flutter/material.dart'; |
|||
|
|||
/// 使用Shader实现的麦克风流动动画组件 |
|||
class ShaderMicFlow extends StatefulWidget { |
|||
final double size; |
|||
final double amplitude; |
|||
|
|||
const ShaderMicFlow({ |
|||
super.key, |
|||
required this.size, |
|||
this.amplitude = 0.0, |
|||
}); |
|||
|
|||
@override |
|||
State<ShaderMicFlow> createState() => _ShaderMicFlowState(); |
|||
} |
|||
|
|||
class _ShaderMicFlowState extends State<ShaderMicFlow> |
|||
with SingleTickerProviderStateMixin { |
|||
late AnimationController _controller; |
|||
FragmentShader? _shader; |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
_controller = AnimationController( |
|||
duration: const Duration(seconds: 60), |
|||
vsync: this, |
|||
)..repeat(); |
|||
|
|||
_loadShader(); |
|||
} |
|||
|
|||
Future<void> _loadShader() async { |
|||
try { |
|||
final program = await FragmentProgram.fromAsset('shaders/mic_flow.frag'); |
|||
setState(() { |
|||
_shader = program.fragmentShader(); |
|||
}); |
|||
} catch (e) { |
|||
debugPrint('Failed to load shader: $e'); |
|||
} |
|||
} |
|||
|
|||
@override |
|||
void dispose() { |
|||
_controller.dispose(); |
|||
_shader?.dispose(); |
|||
super.dispose(); |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return SizedBox( |
|||
width: widget.size, |
|||
height: widget.size, |
|||
child: AnimatedBuilder( |
|||
animation: _controller, |
|||
builder: (context, child) { |
|||
return CustomPaint( |
|||
painter: _shader != null |
|||
? _ShaderPainter( |
|||
shader: _shader!, |
|||
time: _controller.value * 60.0, |
|||
amplitude: widget.amplitude, |
|||
) |
|||
: null, |
|||
size: Size.square(widget.size), |
|||
); |
|||
}, |
|||
), |
|||
); |
|||
} |
|||
} |
|||
|
|||
class _ShaderPainter extends CustomPainter { |
|||
final FragmentShader shader; |
|||
final double time; |
|||
final double amplitude; |
|||
|
|||
_ShaderPainter({ |
|||
required this.shader, |
|||
required this.time, |
|||
required this.amplitude, |
|||
}); |
|||
|
|||
@override |
|||
void paint(Canvas canvas, Size size) { |
|||
// 设置shader uniforms |
|||
shader.setFloat(0, size.width); // u_resolution.x |
|||
shader.setFloat(1, size.height); // u_resolution.y |
|||
shader.setFloat(2, time); // u_time |
|||
shader.setFloat(3, amplitude); // u_amp |
|||
|
|||
final paint = Paint()..shader = shader; |
|||
canvas.drawRect(Offset.zero & size, paint); |
|||
} |
|||
|
|||
@override |
|||
bool shouldRepaint(covariant _ShaderPainter oldDelegate) { |
|||
return oldDelegate.time != time || oldDelegate.amplitude != amplitude; |
|||
} |
|||
} |
|||
@ -1,89 +0,0 @@ |
|||
import 'dart:ui'; |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:flutter_shaders/flutter_shaders.dart'; |
|||
|
|||
class VoiceMicFlow extends StatefulWidget { |
|||
const VoiceMicFlow({super.key, this.size = 280, this.amp = 0.0}); |
|||
final double size; |
|||
final double amp; // 音量振幅 0.0-1.0 |
|||
|
|||
@override |
|||
State<VoiceMicFlow> createState() => _VoiceMicFlowState(); |
|||
} |
|||
|
|||
class _VoiceMicFlowState extends State<VoiceMicFlow> |
|||
with SingleTickerProviderStateMixin { |
|||
late final AnimationController _ctl; |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
_ctl = AnimationController( |
|||
duration: const Duration(seconds: 100), // 100秒的长周期 |
|||
vsync: this, |
|||
)..repeat(); |
|||
} |
|||
|
|||
@override |
|||
void dispose() { |
|||
_ctl.dispose(); |
|||
super.dispose(); |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return ClipOval( |
|||
child: ShaderBuilder( |
|||
assetKey: 'shaders/mic_flow.frag', |
|||
(context, shader, child) { |
|||
return AnimatedBuilder( |
|||
animation: _ctl, |
|||
builder: (_, __) { |
|||
// 将0.0-1.0的动画值映射到更大的时间范围 |
|||
final time = _ctl.value * 100.0; |
|||
shader |
|||
..setFloat(0, widget.size) // u_resolution.x |
|||
..setFloat(1, widget.size) // u_resolution.y |
|||
..setFloat(2, time) // u_time |
|||
..setFloat(3, widget.amp); // u_amp (音量振幅) |
|||
return CustomPaint( |
|||
size: Size.square(widget.size), |
|||
painter: _ShaderPainter(shader), |
|||
); |
|||
}, |
|||
); |
|||
}, |
|||
// 着色器编译时的占位符 |
|||
child: Container( |
|||
width: widget.size, |
|||
height: widget.size, |
|||
decoration: const BoxDecoration( |
|||
shape: BoxShape.circle, |
|||
gradient: LinearGradient( |
|||
begin: Alignment.topLeft, |
|||
end: Alignment.bottomRight, |
|||
colors: [ |
|||
Color(0xFF1565C0), // 深蓝 |
|||
Color(0xFFE3F2FD), // 浅蓝 |
|||
], |
|||
), |
|||
), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
|
|||
class _ShaderPainter extends CustomPainter { |
|||
_ShaderPainter(this.shader); |
|||
final FragmentShader shader; |
|||
|
|||
@override |
|||
void paint(Canvas canvas, Size size) { |
|||
final paint = Paint()..shader = shader; |
|||
canvas.drawRect(Offset.zero & size, paint); |
|||
} |
|||
|
|||
@override |
|||
bool shouldRepaint(covariant _ShaderPainter oldDelegate) => true; |
|||
} |
|||
Loading…
Reference in new issue