Last active
December 14, 2023 08:42
-
-
Save CoderNamedHendrick/6c8b9925b540c17a2a841b3731689f63 to your computer and use it in GitHub Desktop.
Animate animate animate
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
/// Example Usage | |
class MyWidget extends StatefulWidget { | |
const MyWidget({super.key}); | |
@override | |
State<MyWidget> createState() => _MyWidgetState(); | |
} | |
class _MyWidgetState extends State<MyWidget> { | |
double _progress = 1; | |
static const configSize = 7; | |
@override | |
Widget build(BuildContext context) { | |
return Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: [ | |
AnimatedSpeedometer( | |
duration: const Duration(milliseconds: 700), | |
size: 300, | |
curve: Curves.easeInOutSine, | |
gapBetweenConfigsInDeg: 8, | |
/// calculating individual progresses from a single progress value | |
configs: List.generate( | |
configSize, | |
(index) { | |
final progress = (_progress.clamp( | |
index / configSize, (index + 1) / configSize) - | |
(index / configSize)); | |
return ( | |
color: [ | |
Colors.red, | |
Colors.green, | |
Colors.blue, | |
Colors.orange, | |
Colors.purple, | |
Colors.black, | |
Colors.indigoAccent | |
][index], | |
progress: progress * configSize, | |
); | |
}, | |
), | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
mainAxisAlignment: MainAxisAlignment.end, | |
children: [ | |
Text( | |
'720', | |
style: Theme.of(context).textTheme.headlineLarge, | |
), | |
const SizedBox(height: 8), | |
const Text('Good standing'), | |
], | |
), | |
), | |
const SizedBox(height: 20), | |
TextButton( | |
onPressed: () { | |
setState(() { | |
_progress = Random().nextDouble(); | |
}); | |
}, | |
child: const Text('Click to randomize progress'), | |
), | |
], | |
); | |
} | |
} |
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 'dart:math'; | |
import 'package:flutter/material.dart'; | |
typedef SpeedometerConfig = ({Color color, double progress}); | |
class AnimatedSpeedometer extends ImplicitlyAnimatedWidget { | |
const AnimatedSpeedometer({ | |
super.key, | |
required super.duration, | |
super.curve, | |
this.configs = const [], | |
this.child, | |
this.size = 200, | |
this.start = 0, | |
this.end = 720, | |
this.gapBetweenConfigsInDeg = 8.0, | |
}); | |
final List<SpeedometerConfig> configs; | |
final Widget? child; | |
final double size; | |
final double start; | |
final double end; | |
final double gapBetweenConfigsInDeg; | |
@override | |
ImplicitlyAnimatedWidgetState<AnimatedSpeedometer> createState() => | |
_AnimatedSpeedometerState(); | |
} | |
class _AnimatedSpeedometerState | |
extends ImplicitlyAnimatedWidgetState<AnimatedSpeedometer> { | |
late List<Tween<double>?> _progresses = | |
List.generate(widget.configs.length, (index) => null); | |
late List<Animation<double>> _configAnimations; | |
Tween<double>? _gapTween; | |
late Animation<double> _gapAnimation; | |
@override | |
void initState() { | |
super.initState(); | |
controller.duration = widget.duration * widget.configs.length; | |
controller.reverseDuration = widget.duration * widget.configs.length; | |
_gapAnimation = animation.drive(_gapTween!); | |
_configAnimations = _progresses.map((progress) { | |
final index = _progresses.indexOf(progress); | |
return progress!.animate( | |
CurvedAnimation( | |
parent: animation, | |
curve: Interval( | |
index / _progresses.length, | |
(index + 1) / _progresses.length, | |
curve: widget.curve, | |
), | |
), | |
); | |
}).toList(); | |
controller.forward(); | |
} | |
@override | |
void forEachTween(TweenVisitor<dynamic> visitor) { | |
_gapTween = visitor(_gapTween, widget.gapBetweenConfigsInDeg, | |
(dynamic value) => Tween<double>(begin: value)) as Tween<double>?; | |
_progresses = widget.configs.map((config) { | |
final index = widget.configs.indexOf(config); | |
return visitor( | |
_progresses[index], | |
config.progress, | |
(dynamic value) => Tween<double>(begin: 0, end: value), | |
) as Tween<double>?; | |
}).toList(); | |
} | |
@override | |
void didUpdateTweens() { | |
_gapAnimation = animation.drive(_gapTween!); | |
_configAnimations = _progresses.map((progress) { | |
final index = _progresses.indexOf(progress); | |
return CurvedAnimation( | |
parent: animation, | |
curve: Interval( | |
index / _progresses.length, | |
(index + 1) / _progresses.length, | |
curve: widget.curve, | |
), | |
).drive(progress!); | |
}).toList(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
assert( | |
(widget.configs.fold( | |
0.0, | |
(previousValue, element) => | |
previousValue + element.progress) / | |
widget.configs.length) <= | |
1, | |
'Progress sum must be less than 1', | |
); | |
return Column( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
RepaintBoundary( | |
child: AnimatedBuilder( | |
animation: animation, | |
builder: (context, child) { | |
return CustomPaint( | |
painter: SpeedometerPainter( | |
gapInDeg: _gapAnimation.value, | |
configs: widget.configs.map((config) { | |
final index = widget.configs.indexOf(config); | |
return ( | |
color: config.color, | |
progress: _configAnimations[index].value, | |
); | |
}).toList(), | |
), | |
size: Size(widget.size, widget.size / 2), | |
child: SizedBox.fromSize( | |
size: Size(widget.size, widget.size / 2), | |
child: child, | |
), | |
); | |
}, | |
child: widget.child, | |
), | |
), | |
const SizedBox(height: 20), | |
SizedBox( | |
width: widget.size + 10, | |
child: Row( | |
mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
children: [ | |
Text('${widget.start.toInt()}'), | |
Text('${widget.end.toInt()}+'), | |
], | |
), | |
), | |
], | |
); | |
} | |
} | |
class SpeedometerPainter extends CustomPainter { | |
const SpeedometerPainter({ | |
this.width = 10, | |
this.configs = const [], | |
double gapInDeg = 8.0, | |
}) : _count = configs.length, | |
_gap = gapInDeg, | |
_factorAngleInRad = pi / configs.length; | |
final double width; | |
final List<SpeedometerConfig> configs; | |
final int _count; | |
final double _gap; | |
final double _factorAngleInRad; | |
@override | |
void paint(Canvas canvas, Size size) { | |
assert(_gap < angleFromRad(_factorAngleInRad), | |
'Gap must be less than factor angle[${angleFromRad(_factorAngleInRad)}] for each config'); | |
final foregroundPaint = Paint() | |
..color = Colors.red | |
..strokeWidth = width | |
..style = PaintingStyle.stroke | |
..strokeCap = StrokeCap.round; | |
final backgroundPaint = Paint() | |
..color = Colors.red | |
..strokeWidth = width | |
..style = PaintingStyle.stroke | |
..strokeCap = StrokeCap.round; | |
Offset center = Offset(size.width / 2, size.height); | |
for (int i = 0; i < _count; i++) { | |
final lastQuadStartFactor = i == _count - 1 ? radFromAngle(_gap) : 0; | |
// final startingAngle = | |
final double startingAngle = | |
-pi + (_factorAngleInRad * i) + lastQuadStartFactor; | |
// pi/count = factor degrees, sweeping by [gap] degrees | |
final lastQuadSweepFactor = i == _count - 2 ? radFromAngle(_gap) : 0; | |
final sweepAngle = | |
(_factorAngleInRad - radFromAngle(_gap) + lastQuadSweepFactor); | |
canvas.drawArc( | |
Rect.fromCircle(center: center, radius: size.height), | |
startingAngle, | |
sweepAngle, | |
false, | |
backgroundPaint..color = configs[i].color.withOpacity(0.5), | |
); | |
canvas.drawArc( | |
Rect.fromCircle(center: center, radius: size.height), | |
startingAngle, | |
_sweepProgress(sweepAngle, configs[i].progress), | |
false, | |
foregroundPaint..color = configs[i].color, | |
); | |
} | |
} | |
double _sweepProgress(double sweepAngle, [double progress = 1]) { | |
return sweepAngle * progress; | |
} | |
double angleFromRad(double rad) => | |
double.parse((rad * 180 / pi).abs().toStringAsFixed(0)); | |
double radFromAngle(double angle) => | |
double.parse((angle * pi / 180).toStringAsPrecision(4)); | |
@override | |
bool shouldRepaint(covariant CustomPainter oldDelegate) { | |
if (oldDelegate is! SpeedometerPainter) return false; | |
return true; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hmmm 🤔