Wave
import 'package:flutter/material.dart'; | |
import 'dart:async'; | |
import 'dart:math'; | |
import 'dart:ui'; | |
void main() => runApp(WaveDemoApp()); | |
class WaveDemoApp extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
title: 'Wave Demo', | |
theme: ThemeData( | |
primarySwatch: Colors.blue, | |
), | |
home: WaveDemoHomePage(title: 'Wave Demo'), | |
); | |
} | |
} | |
class WaveDemoHomePage extends StatefulWidget { | |
WaveDemoHomePage({Key key, this.title}) : super(key: key); | |
final String title; | |
@override | |
_WaveDemoHomePageState createState() => _WaveDemoHomePageState(); | |
} | |
class _WaveDemoHomePageState extends State<WaveDemoHomePage> { | |
_buildCard({ | |
Config config, | |
Color backgroundColor = Colors.transparent, | |
DecorationImage backgroundImage, | |
double height = 152.0, | |
}) { | |
return Container( | |
height: height, | |
width: double.infinity, | |
child: Card( | |
elevation: 12.0, | |
margin: EdgeInsets.only(right: 16.0, left: 16.0, bottom: 16.0), | |
clipBehavior: Clip.antiAlias, | |
shape: RoundedRectangleBorder( | |
borderRadius: BorderRadius.all(Radius.circular(16.0))), | |
child: WaveWidget( | |
config: config, | |
backgroundColor: backgroundColor, | |
backgroundImage: backgroundImage, | |
size: Size(double.infinity, double.infinity), | |
waveAmplitude: 0, | |
), | |
), | |
); | |
} | |
MaskFilter _blur; | |
final List<MaskFilter> _blurs = [ | |
null, | |
MaskFilter.blur(BlurStyle.normal, 10.0), | |
MaskFilter.blur(BlurStyle.inner, 10.0), | |
MaskFilter.blur(BlurStyle.outer, 10.0), | |
MaskFilter.blur(BlurStyle.solid, 16.0), | |
]; | |
int _blurIndex = 0; | |
MaskFilter _nextBlur() { | |
if (_blurIndex == _blurs.length - 1) { | |
_blurIndex = 0; | |
} else { | |
_blurIndex = _blurIndex + 1; | |
} | |
_blur = _blurs[_blurIndex]; | |
return _blurs[_blurIndex]; | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: Text(widget.title), | |
elevation: 10.0, | |
backgroundColor: Colors.blueGrey[800], | |
actions: <Widget>[ | |
IconButton( | |
icon: Icon(_blur == null ? Icons.blur_off : Icons.blur_on), | |
onPressed: () { | |
setState(() { | |
_blur = _nextBlur(); | |
}); | |
}, | |
) | |
], | |
), | |
body: Center( | |
child: ListView( | |
children: <Widget>[ | |
SizedBox(height: 16.0), | |
_buildCard( | |
backgroundColor: Colors.purpleAccent, | |
config: CustomConfig( | |
gradients: [ | |
[Colors.red, Color(0xEEF44336)], | |
[Colors.red[800], Color(0x77E57373)], | |
[Colors.orange, Color(0x66FF9800)], | |
[Colors.yellow, Color(0x55FFEB3B)] | |
], | |
durations: [35000, 19440, 10800, 6000], | |
heightPercentages: [0.20, 0.23, 0.25, 0.30], | |
blur: _blur, | |
gradientBegin: Alignment.bottomLeft, | |
gradientEnd: Alignment.topRight, | |
), | |
), | |
_buildCard( | |
height: 256.0, | |
backgroundImage: DecorationImage( | |
image: NetworkImage( | |
'https://images.unsplash.com/photo-1600107363560-a2a891080c31?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=672&q=80', | |
), | |
fit: BoxFit.cover, | |
colorFilter: | |
ColorFilter.mode(Colors.white, BlendMode.softLight), | |
), | |
config: CustomConfig( | |
colors: [ | |
Colors.pink[400], | |
Colors.pink[300], | |
Colors.pink[200], | |
Colors.pink[100] | |
], | |
durations: [18000, 8000, 5000, 12000], | |
heightPercentages: [0.85, 0.86, 0.88, 0.90], | |
blur: _blur, | |
), | |
), | |
_buildCard( | |
config: CustomConfig( | |
colors: [ | |
Colors.white70, | |
Colors.white54, | |
Colors.white30, | |
Colors.white24, | |
], | |
durations: [32000, 21000, 18000, 5000], | |
heightPercentages: [0.25, 0.26, 0.28, 0.31], | |
blur: _blur, | |
), | |
backgroundColor: Colors.blue[600]), | |
], | |
), | |
), | |
); | |
} | |
} | |
class WaveWidget extends StatefulWidget { | |
final Config config; | |
final Size size; | |
final double waveAmplitude; | |
final double wavePhase; | |
final double waveFrequency; | |
final double heightPercentange; | |
final int duration; | |
final Color backgroundColor; | |
final DecorationImage backgroundImage; | |
final bool isLoop; | |
WaveWidget({ | |
@required this.config, | |
@required this.size, | |
this.waveAmplitude = 20.0, | |
this.wavePhase = 10.0, | |
this.waveFrequency = 1.6, | |
this.heightPercentange = 0.2, | |
this.duration = 6000, | |
this.backgroundColor, | |
this.backgroundImage, | |
this.isLoop = true, | |
}); | |
@override | |
State<StatefulWidget> createState() => _WaveWidgetState(); | |
} | |
class _WaveWidgetState extends State<WaveWidget> with TickerProviderStateMixin { | |
List<AnimationController> _waveControllers; | |
List<Animation<double>> _wavePhaseValues; | |
List<double> _waveAmplitudes = []; | |
Map<Animation<double>, AnimationController> valueList; | |
Timer _endAnimationTimer; | |
_initAnimations() { | |
if (widget.config.colorMode == ColorMode.custom) { | |
_waveControllers = | |
(widget.config as CustomConfig).durations.map((duration) { | |
_waveAmplitudes.add(widget.waveAmplitude + 10); | |
return AnimationController( | |
vsync: this, duration: Duration(milliseconds: duration)); | |
}).toList(); | |
_wavePhaseValues = _waveControllers.map((controller) { | |
CurvedAnimation _curve = | |
CurvedAnimation(parent: controller, curve: Curves.easeInOut); | |
Animation<double> value = Tween( | |
begin: widget.wavePhase, | |
end: 360 + widget.wavePhase, | |
).animate( | |
_curve, | |
); | |
value.addStatusListener((status) { | |
switch (status) { | |
case AnimationStatus.completed: | |
controller.reverse(); | |
break; | |
case AnimationStatus.dismissed: | |
controller.forward(); | |
break; | |
default: | |
break; | |
} | |
}); | |
controller.forward(); | |
return value; | |
}).toList(); | |
// If isLoop is false, stop the animation after the specified duration. | |
if (!widget.isLoop) { | |
_endAnimationTimer = Timer(Duration(milliseconds: widget.duration), () { | |
for (AnimationController waveController in _waveControllers) { | |
waveController.stop(); | |
} | |
}); | |
} | |
} | |
} | |
_buildPaints() { | |
List<Widget> paints = []; | |
if (widget.config.colorMode == ColorMode.custom) { | |
List<Color> _colors = (widget.config as CustomConfig).colors; | |
List<List<Color>> _gradients = (widget.config as CustomConfig).gradients; | |
Alignment begin = (widget.config as CustomConfig).gradientBegin; | |
Alignment end = (widget.config as CustomConfig).gradientEnd; | |
for (int i = 0; i < _wavePhaseValues.length; i++) { | |
paints.add( | |
Container( | |
child: CustomPaint( | |
painter: _CustomWavePainter( | |
color: _colors == null ? null : _colors[i], | |
gradient: _gradients == null ? null : _gradients[i], | |
gradientBegin: begin, | |
gradientEnd: end, | |
heightPercentange: | |
(widget.config as CustomConfig).heightPercentages[i], | |
repaint: _waveControllers[i], | |
waveFrequency: widget.waveFrequency, | |
wavePhaseValue: _wavePhaseValues[i], | |
waveAmplitude: _waveAmplitudes[i], | |
blur: (widget.config as CustomConfig).blur, | |
), | |
size: widget.size, | |
), | |
), | |
); | |
} | |
} | |
return paints; | |
} | |
_disposeAnimations() { | |
_waveControllers.forEach((controller) { | |
controller.dispose(); | |
}); | |
} | |
@override | |
void initState() { | |
super.initState(); | |
_initAnimations(); | |
} | |
@override | |
void dispose() { | |
_disposeAnimations(); | |
_endAnimationTimer?.cancel(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Container( | |
decoration: BoxDecoration( | |
color: widget.backgroundColor, | |
image: widget.backgroundImage, | |
), | |
child: Stack( | |
children: _buildPaints(), | |
), | |
); | |
} | |
} | |
/// Meta data of layer | |
class Layer { | |
final Color color; | |
final List<Color> gradient; | |
final MaskFilter blur; | |
final Path path; | |
final double amplitude; | |
final double phase; | |
Layer({ | |
this.color, | |
this.gradient, | |
this.blur, | |
this.path, | |
this.amplitude, | |
this.phase, | |
}); | |
} | |
class _CustomWavePainter extends CustomPainter { | |
final ColorMode colorMode; | |
final Color color; | |
final List<Color> gradient; | |
final Alignment gradientBegin; | |
final Alignment gradientEnd; | |
final MaskFilter blur; | |
double waveAmplitude; | |
Animation<double> wavePhaseValue; | |
double waveFrequency; | |
double heightPercentange; | |
double _tempA = 0.0; | |
double _tempB = 0.0; | |
double viewWidth = 0.0; | |
Paint _paint = Paint(); | |
_CustomWavePainter( | |
{this.colorMode, | |
this.color, | |
this.gradient, | |
this.gradientBegin, | |
this.gradientEnd, | |
this.blur, | |
this.heightPercentange, | |
this.waveFrequency, | |
this.wavePhaseValue, | |
this.waveAmplitude, | |
Listenable repaint}) | |
: super(repaint: repaint); | |
_setPaths(double viewCenterY, Size size, Canvas canvas) { | |
Layer _layer = Layer( | |
path: Path(), | |
color: color, | |
gradient: gradient, | |
blur: blur, | |
amplitude: (-1.6 + 0.8) * waveAmplitude, | |
phase: wavePhaseValue.value * 2 + 30, | |
); | |
_layer.path.reset(); | |
_layer.path.moveTo( | |
0.0, | |
viewCenterY + | |
_layer.amplitude * _getSinY(_layer.phase, waveFrequency, -1)); | |
for (int i = 1; i < size.width + 1; i++) { | |
_layer.path.lineTo( | |
i.toDouble(), | |
viewCenterY + | |
_layer.amplitude * _getSinY(_layer.phase, waveFrequency, i)); | |
} | |
_layer.path.lineTo(size.width, size.height); | |
_layer.path.lineTo(0.0, size.height); | |
_layer.path.close(); | |
if (_layer.color != null) { | |
_paint.color = _layer.color; | |
} | |
if (_layer.gradient != null) { | |
var rect = Offset.zero & | |
Size(size.width, size.height - viewCenterY * heightPercentange); | |
_paint.shader = LinearGradient( | |
begin: gradientBegin == null | |
? Alignment.bottomCenter | |
: gradientBegin, | |
end: gradientEnd == null ? Alignment.topCenter : gradientEnd, | |
colors: _layer.gradient) | |
.createShader(rect); | |
} | |
if (_layer.blur != null) { | |
_paint.maskFilter = _layer.blur; | |
} | |
_paint.style = PaintingStyle.fill; | |
canvas.drawPath(_layer.path, _paint); | |
} | |
@override | |
void paint(Canvas canvas, Size size) { | |
double viewCenterY = size.height * (heightPercentange + 0.1); | |
viewWidth = size.width; | |
_setPaths(viewCenterY, size, canvas); | |
} | |
@override | |
bool shouldRepaint(CustomPainter oldDelegate) { | |
return false; | |
} | |
double _getSinY( | |
double startradius, double waveFrequency, int currentposition) { | |
if (_tempA == 0) { | |
_tempA = pi / viewWidth; | |
} | |
if (_tempB == 0) { | |
_tempB = 2 * pi / 360.0; | |
} | |
return (sin( | |
_tempA * waveFrequency * (currentposition + 1) + startradius * _tempB)); | |
} | |
} | |
enum ColorMode { | |
/// Waves with *single* **color** but different **alpha** and **amplitude**. | |
single, | |
/// Waves using *random* **color**, **alpha** and **amplitude**. | |
random, | |
/// Waves' colors must be set, and [colors]'s length must equal with [layers] | |
custom, | |
} | |
abstract class Config { | |
final ColorMode colorMode; | |
Config({this.colorMode}); | |
void throwNullError(String colorModeStr, String configStr) { | |
throw FlutterError( | |
'When using `ColorMode.$colorModeStr`, `$configStr` must be set.'); | |
} | |
} | |
class CustomConfig extends Config { | |
final List<Color> colors; | |
final List<List<Color>> gradients; | |
final Alignment gradientBegin; | |
final Alignment gradientEnd; | |
final List<int> durations; | |
final List<double> heightPercentages; | |
final MaskFilter blur; | |
CustomConfig({ | |
this.colors, | |
this.gradients, | |
this.gradientBegin, | |
this.gradientEnd, | |
@required this.durations, | |
@required this.heightPercentages, | |
this.blur, | |
}) : assert(() { | |
if (colors == null && gradients == null) { | |
throwNullError('custom', 'colors` or `gradients'); | |
} | |
return true; | |
}()), | |
assert(() { | |
if (gradients == null && | |
(gradientBegin != null || gradientEnd != null)) { | |
throw FlutterError( | |
'You set a gradient direction but forgot setting `gradients`.'); | |
} | |
return true; | |
}()), | |
assert(() { | |
if (durations == null) { | |
throwNullError('custom', 'durations'); | |
} | |
return true; | |
}()), | |
assert(() { | |
if (heightPercentages == null) { | |
throwNullError('custom', 'heightPercentages'); | |
} | |
return true; | |
}()), | |
assert(() { | |
if (colors != null) { | |
if (colors.length != durations.length || | |
colors.length != heightPercentages.length) { | |
throw FlutterError( | |
'Length of `colors`, `durations` and `heightPercentages` must be equal.'); | |
} | |
} | |
return true; | |
}()), | |
assert(colors == null || gradients == null, | |
'Cannot provide both colors and gradients.'), | |
super(colorMode: ColorMode.custom); | |
} | |
/// todo | |
class RandomConfig extends Config { | |
RandomConfig() : super(colorMode: ColorMode.random); | |
} | |
/// todo | |
class SingleConfig extends Config { | |
SingleConfig() : super(colorMode: ColorMode.single); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment