Created
September 16, 2020 03:55
-
-
Save RockerFlower/dc501f25a71c9aceceb83ba4ba198f31 to your computer and use it in GitHub Desktop.
Wave
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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