Created
February 11, 2025 20:47
-
-
Save JohanScheepers/36a99e1de0f5f42d6e856333677170c0 to your computer and use it in GitHub Desktop.
Radial Temperature Gauge
This file contains hidden or 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: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