import 'package:flutter/material.dart';
import 'dart:ui';
import 'dart:math' as math;
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Container(
width: double.infinity,
height: double.infinity,
alignment: Alignment.center,
child: AutomatedAnimator(
animateToggle: true,
doRepeatAnimation: true,
duration: Duration(seconds: 10),
buildWidget: (double animationPosition) {
return WaveLoadingBubble(
foregroundWaveColor: Color(0xFF6AA0E1),
backgroundWaveColor: Color(0xFF4D90DF),
loadingWheelColor: Color(0xFF77AAEE),
period: animationPosition,
backgroundWaveVerticalOffset: 90 - animationPosition * 200,
foregroundWaveVerticalOffset: 90 +
reversingSplitParameters(
position: animationPosition,
numberBreaks: 6,
parameterBase: 8.0,
parameterVariation: 8.0,
reversalPoint: 0.75,
) -
animationPosition * 200,
waveHeight: reversingSplitParameters(
position: animationPosition,
numberBreaks: 5,
parameterBase: 12,
parameterVariation: 8,
reversalPoint: 0.75,
),
);
},
),
),
);
}
}
class AutomatedAnimator extends StatefulWidget {
AutomatedAnimator({
@required this.buildWidget,
@required this.animateToggle,
this.duration = const Duration(milliseconds: 300),
this.doRepeatAnimation = false,
Key key,
}) : super(key: key);
final Widget Function(double animationValue) buildWidget;
final Duration duration;
final bool animateToggle;
final bool doRepeatAnimation;
@override
_AutomatedAnimatorState createState() => _AutomatedAnimatorState();
}
class _AutomatedAnimatorState extends State<AutomatedAnimator> with SingleTickerProviderStateMixin {
_AutomatedAnimatorState();
AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(vsync: this, duration: widget.duration)..addListener(() => setState(() {}));
if (widget.animateToggle == true) controller.forward();
if (widget.doRepeatAnimation == true) controller.repeat();
}
@override
void didUpdateWidget(AutomatedAnimator oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.animateToggle == true) {
controller.forward();
return;
}
controller.reverse();
}
@override
Widget build(BuildContext context) {
return widget.buildWidget(controller.value);
}
}
//*======================================================================
//* Additional functions to allow custom periodicity of animations
//*======================================================================
//*======================================================================
//* varies (parameterVariation) a paramter (parameterBase) based on an
//* animation position (position), broken into a number of parts
//* (numberBreaks).
//* the animation reverses at the halfway point (0.5)
//*
//* returns a value of 0.0 - 1.0
//*======================================================================
double reversingSplitParameters({
@required double position,
@required double numberBreaks,
@required double parameterBase,
@required double parameterVariation,
@required double reversalPoint,
}) {
assert(reversalPoint <= 1.0 && reversalPoint >= 0.0, "reversalPoint must be a number between 0.0 and 1.0");
final double finalAnimationPosition = breakAnimationPosition(position, numberBreaks);
if (finalAnimationPosition <= 0.5) {
return parameterBase - (finalAnimationPosition * 2 * parameterVariation);
} else {
return parameterBase - ((1 - finalAnimationPosition) * 2 * parameterVariation);
}
}
//*======================================================================
//* Breaks down a long animation controller value into a number of
//* smaller animations,
//* used for creating a single looping animation with multiple
//* sub animations with different periodicites that are able to
//* maintain a consistent unbroken loop
//*
//* Returns a value of 0.0 - 1.0 based on a given animationPosition
//* split into a discrete number of breaks (numberBreaks)
//*======================================================================
double breakAnimationPosition(double position, double numberBreaks) {
double finalAnimationPosition = 0;
final double breakPoint = 1.0 / numberBreaks;
for (var i = 0; i < numberBreaks; i++) {
if (position <= breakPoint * (i + 1)) {
finalAnimationPosition = (position - i * breakPoint) * numberBreaks;
break;
}
}
return finalAnimationPosition;
}
class WaveLoadingBubble extends StatelessWidget {
const WaveLoadingBubble({
this.bubbleDiameter = 200.0,
this.loadingCircleWidth = 10.0,
this.waveInsetWidth = 5.0,
this.waveHeight = 10.0,
this.foregroundWaveColor = Colors.lightBlue,
this.backgroundWaveColor = Colors.blue,
this.loadingWheelColor = Colors.white,
this.foregroundWaveVerticalOffset = 10.0,
this.backgroundWaveVerticalOffset = 0.0,
this.period = 0.0,
Key key,
}) : super(key: key);
final double bubbleDiameter;
final double loadingCircleWidth;
final double waveInsetWidth;
final double waveHeight;
final Color foregroundWaveColor;
final Color backgroundWaveColor;
final Color loadingWheelColor;
final double foregroundWaveVerticalOffset;
final double backgroundWaveVerticalOffset;
final double period;
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: WaveLoadingBubblePainter(
bubbleDiameter: bubbleDiameter,
loadingCircleWidth: loadingCircleWidth,
waveInsetWidth: waveInsetWidth,
waveHeight: waveHeight,
foregroundWaveColor: foregroundWaveColor,
backgroundWaveColor: backgroundWaveColor,
loadingWheelColor: loadingWheelColor,
foregroundWaveVerticalOffset: foregroundWaveVerticalOffset,
backgroundWaveVerticalOffset: backgroundWaveVerticalOffset,
period: period,
),
);
}
}
class WaveLoadingBubblePainter extends CustomPainter {
WaveLoadingBubblePainter({
this.bubbleDiameter,
this.loadingCircleWidth,
this.waveInsetWidth,
this.waveHeight,
this.foregroundWaveColor,
this.backgroundWaveColor,
this.loadingWheelColor,
this.foregroundWaveVerticalOffset,
this.backgroundWaveVerticalOffset,
this.period,
}) : foregroundWavePaint = Paint()..color = foregroundWaveColor,
backgroundWavePaint = Paint()..color = backgroundWaveColor,
loadingCirclePaint = Paint()
..shader = SweepGradient(
colors: [
Colors.transparent,
loadingWheelColor,
Colors.transparent,
],
stops: [0.0, 0.9, 1.0],
startAngle: 0,
endAngle: math.pi * 1,
transform: GradientRotation(period * math.pi * 2 * 5),
).createShader(Rect.fromCircle(
center: Offset(0.0, 0.0),
radius: bubbleDiameter / 2,
));
final double bubbleDiameter;
final double loadingCircleWidth;
final double waveInsetWidth;
final double waveHeight;
final Paint foregroundWavePaint;
final Paint backgroundWavePaint;
final Paint loadingCirclePaint;
final Color foregroundWaveColor;
final Color backgroundWaveColor;
final Color loadingWheelColor;
final double foregroundWaveVerticalOffset;
final double backgroundWaveVerticalOffset;
final double period;
@override
void paint(Canvas canvas, Size size) {
final double loadingBubbleRadius = (bubbleDiameter / 2);
final double insetBubbleRadius = loadingBubbleRadius - waveInsetWidth;
final double waveBubbleRadius = insetBubbleRadius - loadingCircleWidth;
Path backgroundWavePath = WavePathHorizontal(
amplitude: waveHeight,
period: 1.0,
startPoint: Offset(0.0 - waveBubbleRadius, 0.0 + backgroundWaveVerticalOffset),
width: bubbleDiameter,
crossAxisEndPoint: waveBubbleRadius,
doClosePath: true,
phaseShift: period * 2 * 5,
).build();
Path foregroundWavePath = WavePathHorizontal(
amplitude: waveHeight,
period: 1.0,
startPoint: Offset(0.0 - waveBubbleRadius, 0.0 + foregroundWaveVerticalOffset),
width: bubbleDiameter,
crossAxisEndPoint: waveBubbleRadius,
doClosePath: true,
phaseShift: -period * 2 * 5,
).build();
Path circleClip = Path()..addRRect(RRect.fromLTRBXY(-waveBubbleRadius, -waveBubbleRadius, waveBubbleRadius, waveBubbleRadius, waveBubbleRadius, waveBubbleRadius));
//Path insetCirclePath = Path()..addRRect(RRect.fromLTRBXY(-insetBubbleRadius, -insetBubbleRadius, insetBubbleRadius, insetBubbleRadius, insetBubbleRadius, insetBubbleRadius));
//Path loadingCirclePath = Path()..addRRect(RRect.fromLTRBXY(-loadingBubbleRadius, -loadingBubbleRadius, loadingBubbleRadius, loadingBubbleRadius, loadingBubbleRadius, loadingBubbleRadius));
// canvas.drawPath(Path.combine(PathOperation.difference, loadingCirclePath, insetCirclePath), loadingCirclePaint);
canvas.clipPath(circleClip, doAntiAlias: true);
canvas.drawPath(backgroundWavePath, backgroundWavePaint);
canvas.drawPath(foregroundWavePath, foregroundWavePaint);
}
@override
bool shouldRepaint(WaveLoadingBubblePainter oldDelegate) => true;
@override
bool shouldRebuildSemantics(WaveLoadingBubblePainter oldDelegate) => false;
}
class WavePathHorizontal {
WavePathHorizontal({
@required this.width,
@required this.amplitude,
@required this.period,
@required this.startPoint,
this.phaseShift = 0.0,
this.doClosePath = false,
this.crossAxisEndPoint = 0,
}) : assert(crossAxisEndPoint != null || doClosePath == false, "if doClosePath is true you must provide an end point (crossAxisEndPoint)");
final double width;
final double amplitude;
final double period;
final Offset startPoint;
final double crossAxisEndPoint; //*
final double phaseShift; //* shift the starting value of the wave, in radians, repeats every 2 radians
final bool doClosePath;
Path build() {
double startPointX = startPoint.dx;
double startPointY = startPoint.dy;
Path returnPath = new Path();
returnPath.moveTo(startPointX, startPointY);
for (double i = 0; i <= width; i++) {
returnPath.lineTo(
i + startPointX,
startPointY + amplitude * math.sin((i * 2 * period * math.pi / width) + phaseShift * math.pi),
);
}
if (doClosePath == true) {
returnPath.lineTo(startPointX + width, crossAxisEndPoint);
returnPath.lineTo(startPointX, crossAxisEndPoint);
returnPath.close();
}
return returnPath;
}
}
View Compiled
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.