Skip to content

Instantly share code, notes, and snippets.

@JohanScheepers
Created February 11, 2025 20:47
Show Gist options
  • Save JohanScheepers/36a99e1de0f5f42d6e856333677170c0 to your computer and use it in GitHub Desktop.
Save JohanScheepers/36a99e1de0f5f42d6e856333677170c0 to your computer and use it in GitHub Desktop.
Radial Temperature Gauge
import 'package:flutter/material.dart';
import 'dart:math';
import 'dart:async';
import 'package:vector_math/vector_math_64.dart' show radians;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Temperature Gauge',
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: const TemperatureGauge(),
);
}
}
class TemperatureGauge extends StatefulWidget {
const TemperatureGauge({super.key});
@override
State<TemperatureGauge> createState() => _TemperatureGaugeState();
}
class _TemperatureGaugeState extends State<TemperatureGauge> {
double _temperature = 25.0;
Timer? _timer;
final Color _needleColor = Colors.black;
final Color _indicatorColor = Colors.blue;
final double _needleBaseWidth = 30.0;
@override
void initState() {
super.initState();
_timer = Timer.periodic(const Duration(seconds: 2), (timer) {
_updateTemperature();
});
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
void _updateTemperature() {
final random = Random();
final change = random.nextDouble() * 5 - 2.5;
double newTemperature = (_temperature + change).clamp(-15.0, 45.0);
setState(() {
_temperature = newTemperature;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Temperature Gauge'),
),
body: Flex(
direction: Axis.vertical,
children: [
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
return Center(
child: SizedBox(
width: constraints.maxWidth * 0.8,
height: constraints.maxHeight * 0.8,
child: RadialGauge(
temperature: _temperature,
needleColor: _needleColor,
indicatorColor: _indicatorColor,
needleBaseWidth: _needleBaseWidth,
),
),
);
},
),
),
],
),
);
}
}
class RadialGauge extends StatefulWidget {
final double temperature;
final Color needleColor;
final Color? indicatorColor;
final double? needleBaseWidth;
const RadialGauge(
{super.key,
required this.temperature,
required this.needleColor,
this.indicatorColor,
this.needleBaseWidth});
@override
State<RadialGauge> createState() => _RadialGaugeState();
}
class _RadialGaugeState extends State<RadialGauge>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
_animation =
Tween<double>(begin: widget.temperature, end: widget.temperature)
.animate(_animationController); // Initialize animation
_animationController.addListener(() {
setState(() {
// Trigger repaint on animation change
});
});
_animation = Tween<double>(begin: -15, end: widget.temperature)
.animate(_animationController);
_animationController.forward();
}
@override
void didUpdateWidget(covariant RadialGauge oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.temperature != widget.temperature) {
_animation =
Tween<double>(begin: oldWidget.temperature, end: widget.temperature)
.animate(_animationController);
_animationController.reset();
_animationController.forward();
}
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return CustomPaint(
painter: GaugePainter(
temperature: _animation.value,
needleColor: widget.needleColor,
indicatorColor: widget.indicatorColor,
needleBaseWidth: widget.needleBaseWidth),
size: Size(MediaQuery.of(context).size.width * 0.8,
MediaQuery.of(context).size.width * 0.8),
);
},
);
}
}
class GaugePainter extends CustomPainter {
final double temperature;
final Color needleColor;
final Color? indicatorColor;
final double? needleBaseWidth;
GaugePainter(
{required this.temperature,
required this.needleColor,
this.indicatorColor,
this.needleBaseWidth});
double _temperatureToAngle(double temperature) {
// Map temperature (-15-45) to angle (0-270)
double angle = (temperature + 15) / (45 + 15) * 270;
return angle;
}
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = min(size.width, size.height) / 2;
final indicatorAngle = _temperatureToAngle(temperature);
// Define the colors for the gradient
const List<Color> gradientColors = [
Colors.purple,
Colors.blue,
Colors.green,
Colors.red,
];
// Draw the gauge arc
final arcPaint = Paint()
..shader = LinearGradient(
colors: gradientColors,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
).createShader(Rect.fromCircle(center: center, radius: radius))
..strokeWidth = 10
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
Rect arcRect = Rect.fromCircle(center: center, radius: radius);
canvas.drawArc(
arcRect,
radians(135), // Start angle
radians(270), // Sweep angle (270 degrees for half-circle)
false,
arcPaint,
);
//Draw the temperature indicator
final indicatorPaint = Paint()
..color = Colors.blue
..strokeWidth = 10
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
canvas.drawArc(
arcRect,
radians(135), // Start angle
radians(indicatorAngle),
false,
indicatorPaint,
);
// Draw the triangular pointer (thick to thin)
final needleLength = radius;
final needleTip = Offset(
center.dx + needleLength * cos(radians(135 + indicatorAngle)),
center.dy + needleLength * sin(radians(135 + indicatorAngle)));
// Calculate points for the base of the triangle
final baseAngle1 = radians(135 + indicatorAngle) + pi / 2;
final baseAngle2 = radians(135 + indicatorAngle) - pi / 2;
// Use the provided needleBaseWidth or default to 15.0
final double baseWidth = needleBaseWidth ?? 15.0;
final basePoint1 = Offset(center.dx + baseWidth / 2 * cos(baseAngle1),
center.dy + baseWidth / 2 * sin(baseAngle1));
final basePoint2 = Offset(center.dx + baseWidth / 2 * cos(baseAngle2),
center.dy + baseWidth / 2 * sin(baseAngle2));
final trianglePath = Path();
trianglePath.moveTo(center.dx, center.dy); // Start at the center
trianglePath.lineTo(needleTip.dx, needleTip.dy); // Go to the tip
trianglePath.lineTo(basePoint2.dx, basePoint2.dy);
trianglePath.lineTo(basePoint1.dx, basePoint1.dy);
trianglePath.close(); // Close the triangle
final trianglePaint = Paint()
..color = needleColor // Use the needleColor parameter
..style = PaintingStyle.fill;
canvas.drawPath(trianglePath, trianglePaint);
// Draw indicator circle
if (indicatorColor != null) {
final indicatorRadius = 8.0; // Adjust the radius as needed
//Correct calculation for position on the ARC:
final indicatorX =
center.dx + radius * cos(radians(135 + indicatorAngle));
final indicatorY =
center.dy + radius * sin(radians(135 + indicatorAngle));
final indicatorPaint = Paint()..color = indicatorColor!;
canvas.drawCircle(
Offset(indicatorX, indicatorY), indicatorRadius, indicatorPaint);
}
// Draw the temperature text at the bottom
final textPainter = TextPainter(
text: TextSpan(
text: '${temperature.toStringAsFixed(1)} °C',
style: const TextStyle(
color: Colors.black,
fontSize: 20,
),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(center.dx - textPainter.width / 2, size.height - 50),
);
// Draw labels in 10-degree increments
for (int i = -15; i <= 45; i += 10) {
double angle = (i + 15) / (45 + 15) * 270;
double labelX = center.dx + (radius + 20) * cos(radians(135 + angle));
double labelY = center.dy + (radius + 20) * sin(radians(135 + angle));
final labelPainter = TextPainter(
text: TextSpan(
text: '$i °C',
style: const TextStyle(
color: Colors.grey,
fontSize: 12,
),
),
textDirection: TextDirection.ltr,
);
labelPainter.layout();
labelPainter.paint(
canvas,
Offset(
labelX - labelPainter.width / 2, labelY - labelPainter.height / 2),
);
}
}
@override
bool shouldRepaint(covariant GaugePainter oldDelegate) {
return oldDelegate.temperature != temperature ||
oldDelegate.needleColor != needleColor ||
oldDelegate.indicatorColor != indicatorColor ||
oldDelegate.needleBaseWidth != needleBaseWidth;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment