Advanced CustomPaint: Building a Radial Gauge Widget
Aug 6, 2025



Summary
Summary
Summary
Summary
Learn to create a radial gauge widget in Flutter using CustomPaint. This tutorial covers gauge geometry calculations, a CustomPainter implementation, touch interaction with GestureDetector, and smooth needle animations. By following these steps, you’ll produce a reusable, performant meter component for mobile development.
Learn to create a radial gauge widget in Flutter using CustomPaint. This tutorial covers gauge geometry calculations, a CustomPainter implementation, touch interaction with GestureDetector, and smooth needle animations. By following these steps, you’ll produce a reusable, performant meter component for mobile development.
Learn to create a radial gauge widget in Flutter using CustomPaint. This tutorial covers gauge geometry calculations, a CustomPainter implementation, touch interaction with GestureDetector, and smooth needle animations. By following these steps, you’ll produce a reusable, performant meter component for mobile development.
Learn to create a radial gauge widget in Flutter using CustomPaint. This tutorial covers gauge geometry calculations, a CustomPainter implementation, touch interaction with GestureDetector, and smooth needle animations. By following these steps, you’ll produce a reusable, performant meter component for mobile development.
Key insights:
Key insights:
Key insights:
Key insights:
Designing Gauge Geometry: Define inner and outer tick radii, calculate angles, and use canvas transforms for organized tick and label placement.
Implementing CustomPainter: Extend CustomPainter, precompute geometry, draw arc, ticks, labels, and map value to needle angle in paint.
Handling Touch and Interaction: Wrap CustomPaint in GestureDetector, convert touch positions to angles, then clamp and update the value via setState.
Styling and Animations: Use TweenAnimationBuilder or AnimationController for smooth needle motion, customize paints, and manage shouldRepaint for performance.
Introduction
CustomPaint in Flutter gives developers pixel-level control over rendering layered shapes, enabling advanced visualizations. In this tutorial, you’ll build a reusable radial gauge widget to display numeric values on a circular meter. We will define gauge geometry, implement a CustomPainter for rendering, add touch-driven value updates, and introduce smooth pointer animations. By following these steps, you’ll master techniques for sophisticated UI elements in mobile development with Flutter.
Designing Gauge Geometry
A radial gauge consists of an arc, ticks, labels, and a movable needle. Begin by computing the center point and maximum radius from the available Size in paint. Define two radii: innerTickRadius and outerTickRadius for minor and major tick marks. Specify majorTickCount and minorTicksPerSegment, then calculate tickStep = sweepAngle / (majorTickCount * minorTicksPerSegment). Generate a list of angles: startAngle + i * tickStep.
For each angle, decide if it’s a major tick (i % minorTicksPerSegment == 0). Compute start and end points via polar-to-Cartesian: x = cos(angle) * radius + centerX; y = sin(angle) * radius + centerY. Use Paint objects with different strokeWidths for major and minor ticks. For labels, set labelRadius = outerTickRadius + labelOffset and use TextPainter to measure and paint numeric strings at each major tick. To simplify rotation math, call canvas.save(), translate to center, rotate by angle, draw tick at (radius, 0), and draw text translated along the x-axis, then canvas.restore(). Precompute tickAngles, tickPositions, and labelStrings in the painter constructor to avoid redundant calculations on each frame.
Implementing CustomPainter
Create a RadialGaugePainter that extends CustomPainter. Pass in parameters such as currentValue, minValue, maxValue, and styling options. Override paint(Canvas canvas, Size size) and shouldRepaint.
class RadialGaugePainter extends CustomPainter {
final double value, min, max;
final List<double> tickAngles;
RadialGaugePainter(this.value, this.min, this.max, this.tickAngles);
@override
void paint(Canvas canvas, Size size) {
final center = size.center(Offset.zero);
final radius = size.width / 2;
final sweepAngle = 3 * pi / 2;
final startAngle = 3 * pi / 4;
// Draw arc background
final arcPaint = Paint()
..color = Colors.grey.shade300
..style = PaintingStyle.stroke
..strokeWidth = 12;
canvas.drawArc(Rect.fromCircle(center: center, radius: radius), startAngle,
sweepAngle, false, arcPaint);
// Draw ticks and labels using precomputed tickAngles
for (var angle in tickAngles) {
canvas.save();
canvas.translate(center.dx, center.dy);
canvas.rotate(angle);
// draw ticks here
canvas.restore();
}
// Draw needle based on currentValue-to-angle mapping
}
@override
bool shouldRepaint(covariant RadialGaugePainter old) {
return old.value != value;
}
}
Inside paint, map value to angle: angle = startAngle + (value - min)/(max - min)*sweepAngle
. Draw the needle as a line or custom shape from center to radius * 0.9.
Handling Touch and Interaction
Wrap your CustomPaint in a GestureDetector to capture taps and drags. Convert touch positions to angles and update the gauge value in a StatefulWidget:
GestureDetector(
onPanUpdate: (details) {
final box = context.findRenderObject() as RenderBox;
final pos = box.globalToLocal(details.globalPosition);
final dx = pos.dx - box.size.width/2;
final dy = pos.dy - box.size.height/2;
var angle = atan2(dy, dx);
// Normalize angle between startAngle and endAngle
final newValue = angleToValue(angle);
setState(() => currentValue = newValue.clamp(min, max));
},
child: CustomPaint(
painter: RadialGaugePainter(currentValue, min, max, tickAngles),
size: Size(200, 200),
),
)
Implement angleToValue
to convert an angle back into a numeric range, handling wraparounds and clamping. Calling setState triggers a repaint with the updated needle position.
Styling and Animations
Enhance interactivity with smooth needle transitions. Use TweenAnimationBuilder or an AnimationController to interpolate between old and new values:
TweenAnimationBuilder<double>(
tween: Tween(begin: previousValue, end: currentValue),
duration: Duration(milliseconds: 300),
builder: (_, val, __) => CustomPaint(
painter: RadialGaugePainter(val, min, max, tickAngles),
size: Size(200, 200),
),
)
Customize colors, stroke widths, and apply gradient shaders to the arc or needle. Use curved animations for natural easing. Leverage canvas.drawShadow
to add depth. Ensure your painter’s shouldRepaint
logic only returns true when relevant fields change to maintain performance in complex layouts.
Vibe Studio

Vibe Studio, powered by Steve’s advanced AI agents, is a revolutionary no-code, conversational platform that empowers users to quickly and efficiently create full-stack Flutter applications integrated seamlessly with Firebase backend services. Ideal for solo founders, startups, and agile engineering teams, Vibe Studio allows users to visually manage and deploy Flutter apps, greatly accelerating the development process. The intuitive conversational interface simplifies complex development tasks, making app creation accessible even for non-coders.
Conclusion
By combining CustomPaint, precise geometry, touch handling, and animations, you can build a fully interactive radial gauge in Flutter. You’ve explored calculating tick placement, rendering shapes and text, mapping gestures to values, and creating smooth pointer motion. Extend this widget with range highlights, custom pointers, or live data feeds to suit your mobile development needs.
Introduction
CustomPaint in Flutter gives developers pixel-level control over rendering layered shapes, enabling advanced visualizations. In this tutorial, you’ll build a reusable radial gauge widget to display numeric values on a circular meter. We will define gauge geometry, implement a CustomPainter for rendering, add touch-driven value updates, and introduce smooth pointer animations. By following these steps, you’ll master techniques for sophisticated UI elements in mobile development with Flutter.
Designing Gauge Geometry
A radial gauge consists of an arc, ticks, labels, and a movable needle. Begin by computing the center point and maximum radius from the available Size in paint. Define two radii: innerTickRadius and outerTickRadius for minor and major tick marks. Specify majorTickCount and minorTicksPerSegment, then calculate tickStep = sweepAngle / (majorTickCount * minorTicksPerSegment). Generate a list of angles: startAngle + i * tickStep.
For each angle, decide if it’s a major tick (i % minorTicksPerSegment == 0). Compute start and end points via polar-to-Cartesian: x = cos(angle) * radius + centerX; y = sin(angle) * radius + centerY. Use Paint objects with different strokeWidths for major and minor ticks. For labels, set labelRadius = outerTickRadius + labelOffset and use TextPainter to measure and paint numeric strings at each major tick. To simplify rotation math, call canvas.save(), translate to center, rotate by angle, draw tick at (radius, 0), and draw text translated along the x-axis, then canvas.restore(). Precompute tickAngles, tickPositions, and labelStrings in the painter constructor to avoid redundant calculations on each frame.
Implementing CustomPainter
Create a RadialGaugePainter that extends CustomPainter. Pass in parameters such as currentValue, minValue, maxValue, and styling options. Override paint(Canvas canvas, Size size) and shouldRepaint.
class RadialGaugePainter extends CustomPainter {
final double value, min, max;
final List<double> tickAngles;
RadialGaugePainter(this.value, this.min, this.max, this.tickAngles);
@override
void paint(Canvas canvas, Size size) {
final center = size.center(Offset.zero);
final radius = size.width / 2;
final sweepAngle = 3 * pi / 2;
final startAngle = 3 * pi / 4;
// Draw arc background
final arcPaint = Paint()
..color = Colors.grey.shade300
..style = PaintingStyle.stroke
..strokeWidth = 12;
canvas.drawArc(Rect.fromCircle(center: center, radius: radius), startAngle,
sweepAngle, false, arcPaint);
// Draw ticks and labels using precomputed tickAngles
for (var angle in tickAngles) {
canvas.save();
canvas.translate(center.dx, center.dy);
canvas.rotate(angle);
// draw ticks here
canvas.restore();
}
// Draw needle based on currentValue-to-angle mapping
}
@override
bool shouldRepaint(covariant RadialGaugePainter old) {
return old.value != value;
}
}
Inside paint, map value to angle: angle = startAngle + (value - min)/(max - min)*sweepAngle
. Draw the needle as a line or custom shape from center to radius * 0.9.
Handling Touch and Interaction
Wrap your CustomPaint in a GestureDetector to capture taps and drags. Convert touch positions to angles and update the gauge value in a StatefulWidget:
GestureDetector(
onPanUpdate: (details) {
final box = context.findRenderObject() as RenderBox;
final pos = box.globalToLocal(details.globalPosition);
final dx = pos.dx - box.size.width/2;
final dy = pos.dy - box.size.height/2;
var angle = atan2(dy, dx);
// Normalize angle between startAngle and endAngle
final newValue = angleToValue(angle);
setState(() => currentValue = newValue.clamp(min, max));
},
child: CustomPaint(
painter: RadialGaugePainter(currentValue, min, max, tickAngles),
size: Size(200, 200),
),
)
Implement angleToValue
to convert an angle back into a numeric range, handling wraparounds and clamping. Calling setState triggers a repaint with the updated needle position.
Styling and Animations
Enhance interactivity with smooth needle transitions. Use TweenAnimationBuilder or an AnimationController to interpolate between old and new values:
TweenAnimationBuilder<double>(
tween: Tween(begin: previousValue, end: currentValue),
duration: Duration(milliseconds: 300),
builder: (_, val, __) => CustomPaint(
painter: RadialGaugePainter(val, min, max, tickAngles),
size: Size(200, 200),
),
)
Customize colors, stroke widths, and apply gradient shaders to the arc or needle. Use curved animations for natural easing. Leverage canvas.drawShadow
to add depth. Ensure your painter’s shouldRepaint
logic only returns true when relevant fields change to maintain performance in complex layouts.
Vibe Studio

Vibe Studio, powered by Steve’s advanced AI agents, is a revolutionary no-code, conversational platform that empowers users to quickly and efficiently create full-stack Flutter applications integrated seamlessly with Firebase backend services. Ideal for solo founders, startups, and agile engineering teams, Vibe Studio allows users to visually manage and deploy Flutter apps, greatly accelerating the development process. The intuitive conversational interface simplifies complex development tasks, making app creation accessible even for non-coders.
Conclusion
By combining CustomPaint, precise geometry, touch handling, and animations, you can build a fully interactive radial gauge in Flutter. You’ve explored calculating tick placement, rendering shapes and text, mapping gestures to values, and creating smooth pointer motion. Extend this widget with range highlights, custom pointers, or live data feeds to suit your mobile development needs.
Build Flutter Apps Faster with Vibe Studio
Build Flutter Apps Faster with Vibe Studio
Build Flutter Apps Faster with Vibe Studio
Build Flutter Apps Faster with Vibe Studio
Vibe Studio is your AI-powered Flutter development companion. Skip boilerplate, build in real-time, and deploy without hassle. Start creating apps at lightning speed with zero setup.
Vibe Studio is your AI-powered Flutter development companion. Skip boilerplate, build in real-time, and deploy without hassle. Start creating apps at lightning speed with zero setup.
Vibe Studio is your AI-powered Flutter development companion. Skip boilerplate, build in real-time, and deploy without hassle. Start creating apps at lightning speed with zero setup.
Vibe Studio is your AI-powered Flutter development companion. Skip boilerplate, build in real-time, and deploy without hassle. Start creating apps at lightning speed with zero setup.











