Skip to content

Instantly share code, notes, and snippets.

@CoderNamedHendrick
Last active December 14, 2023 08:42
Show Gist options
  • Save CoderNamedHendrick/6c8b9925b540c17a2a841b3731689f63 to your computer and use it in GitHub Desktop.
Save CoderNamedHendrick/6c8b9925b540c17a2a841b3731689f63 to your computer and use it in GitHub Desktop.
Animate animate animate
/// 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'),
),
],
);
}
}
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;
}
}
@prmpsmart
Copy link

Hmmm 🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment