Flutter. A Quarter Round Slider.
Tags: flutter, slider, custompainter
Published: 2020-05-12 17:30:25


###Introduction
This post describes the process of creating a custom slider that can be used to select a value from a range of them. The slider resembles the quarter of a circle instead of the typical linear shape.


###Custom Painter
Obviously I will need to paint a custom component and this should be done using two Flutter API classes. [CustomPainter](https://api.flutter.dev/flutter/rendering/CustomPainter-class.html) and [CustomPaint](https://api.flutter.dev/flutter/widgets/CustomPaint-class.html). 

**CustomPainter** You must extend this class and overwrite the method *void paint(Canvas, Size)* in order to draw the Widget UI and *bool shouldRepaint(CustomPainter)* method to return true if the widget should be repainted when a new instance of CustomPainter is provided.

**CustomPaint** The class *CustomPainter* described above is used through the CustomPaint Widget. CustomPaint takes a constructor parameter named *painter* for this purpose.

The process is quite simple and can be applied to both simple projects like these and larger, more complex components.


###Skeleton example###
The classes used in the example are:
1. MyApp extends StatelessWidget. A Stateless Widget which contains the MaterialApp, Scaffold Widgets of the Application.
2. RoundSlider extends StatefulWidget. Statefull Widget class of the component.
3. _RoundSliderState extends State<RoundSlider> Representing the state of the RoundSlider Widget .
4. RoundPainter extends CustomPainter. The class which paints the UI. 


###Class MyApp###
It's a stateful widget which launches the application and passes to the slider the constructor parameters *title*, *radius* and *maxvalue* which are self-described.
````dart
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
 return   MaterialApp(
     home: Scaffold(
         appBar: AppBar(title: Text("Example")),
         body: Center(
             child: RoundSlider(
                 title: "Volume", radius: 90, maxvalue: 99))));
  }
}
````


###Classes RoundSlider and RoundSliderState###
RoundSlider is the main slider Widget. Its a stateful widget and its state class named RoundSliderState returns into the building method, a GestureDetector which captures drag movements and calculates the proper angle. 

Notice that the angle is limited in code to the range 0 to pi/2

The GestureDetector wraps an instance of RoundPainter that we will analyze later. 


````dart
class RoundSlider extends StatefulWidget {
  final double radius;
  final double maxvalue;
  final String title;

  RoundSlider({this.radius, this.maxvalue, this.title});
  @override
  _RoundSliderState createState() => _RoundSliderState();
}

class _RoundSliderState extends State<RoundSlider> {
  double angle = 0;

  void _update(u) {
    setState(() {
      double testAngle = atan2(u.localPosition.dy, u.localPosition.dx);

      if (testAngle >= 0 && testAngle <= pi / 2) {
        setState(() {
          angle = testAngle;
        });
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
        onHorizontalDragUpdate: (update) {
          _update(update);
        },
        onVerticalDragUpdate: (update) {
          _update(update);
        },
        child: Column(children: <Widget>[
          Text(widget.title,textScaleFactor: widget.radius/60,),
          Container(
            padding: EdgeInsets.all(widget.radius * 0.20),
            width: widget.radius + widget.radius * 0.20,
            height: widget.radius + widget.radius * 0.20,
            decoration: BoxDecoration(border: Border.all()),
            child: CustomPaint(
                painter: RoundPainter(angle: angle, maxvalue: widget.maxvalue)),
          ),
        ]));
  }
}
```` 


###Class RoundPainter###
The constructor takes the parameter angle in order to calculate the coordinates x,y of the selector and the parameter maxvalue to calculate the actual value selected.

Values like strokeWidth, textScaleFactor, etc...are calculated proportionally to width value of the component.

````dart
class RoundPainter extends CustomPainter {
  double angle;
  double maxvalue;
  Paint strokePaint = Paint()..style = PaintingStyle.stroke;

  Paint fillPaint = Paint()
    ..style = PaintingStyle.fill
    ..color = Colors.white;

  RoundPainter({this.angle, this.maxvalue});

  Offset offset = Offset(0, 0);

  @override
  void paint(Canvas canvas, Size size) {
    strokePaint.strokeWidth = size.width / 25;

    canvas.drawArc(Rect.fromCircle(center: offset, radius: size.width), 0,
        pi / 2, false, strokePaint);

    _drawSelector(canvas, size, angle);
    _drawValue(canvas, size, (maxvalue * angle / pi).round());
  }

  void _drawValue(Canvas canvas, Size size, int value) {
    TextSpan span = new TextSpan(
        style: new TextStyle(color: Colors.black), text: value.toString());
    TextPainter tp = TextPainter(
        text: span,
        textDirection: TextDirection.ltr,
        textAlign: TextAlign.left,
        textScaleFactor: size.width / 40);
    tp.layout(minWidth: 0);
    Offset newOffset = Offset(offset.dx, offset.dy);
    tp.paint(canvas, newOffset);
  }

  void _drawSelector(Canvas canvas, Size size, double angle) {
    strokePaint.strokeWidth = 1;
    double x = size.width * cos(angle);
    double y = size.height * sin(angle);
    canvas.drawCircle(Offset(x, y), size.width / 10, fillPaint);
    canvas.drawCircle(Offset(x, y), size.height / 10, strokePaint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}
````

###All the code
The rest of the code...
````dart
import 'package:flutter/material.dart';
import 'dart:math';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
 return   MaterialApp(
     home: Scaffold(
         appBar: AppBar(title: Text("Example")),
         body: Center(
             child: RoundSlider(
                 title: "Volume", radius: 90, maxvalue: 99))));
  }
}


class RoundSlider extends StatefulWidget {
  double radius;
  double maxvalue;
  String title;

  RoundSlider({this.radius, this.maxvalue, this.title});
  @override
  _RoundSliderState createState() => _RoundSliderState();
}

class _RoundSliderState extends State<RoundSlider> {
  double angle = 0;

  void _update(u) {
    setState(() {
      double testAngle = atan2(u.localPosition.dy, u.localPosition.dx);

      if (testAngle >= 0 && testAngle <= pi / 2) {
        setState(() {
          angle = testAngle;
        });
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
        onHorizontalDragUpdate: (update) {
          _update(update);
        },
        onVerticalDragUpdate: (update) {
          _update(update);
        },
        child: Column(children: <Widget>[
          Text(widget.title,textScaleFactor: widget.radius/60,),
          Container(
            padding: EdgeInsets.all(widget.radius * 0.20),
            width: widget.radius + widget.radius * 0.20,
            height: widget.radius + widget.radius * 0.20,
            decoration: BoxDecoration(border: Border.all()),
            child: CustomPaint(
                painter: RoundPainter(angle: angle, maxvalue: widget.maxvalue)),
          ),
        ]));
  }
}

class RoundPainter extends CustomPainter {
  double angle;
  double maxvalue;
  Paint strokePaint = Paint()..style = PaintingStyle.stroke;

  Paint fillPaint = Paint()
    ..style = PaintingStyle.fill
    ..color = Colors.white;

  RoundPainter({this.angle, this.maxvalue});

  Offset offset = Offset(0, 0);

  @override
  void paint(Canvas canvas, Size size) {
    strokePaint.strokeWidth = size.width / 25;

    canvas.drawArc(Rect.fromCircle(center: offset, radius: size.width), 0,
        pi / 2, false, strokePaint);

    _drawSelector(canvas, size, angle);
    _drawValue(canvas, size, (maxvalue * angle / pi).round());
  }

  void _drawValue(Canvas canvas, Size size, int value) {
    TextSpan span = new TextSpan(
        style: new TextStyle(color: Colors.black), text: value.toString());
    TextPainter tp = TextPainter(
        text: span,
        textDirection: TextDirection.ltr,
        textAlign: TextAlign.left,
        textScaleFactor: size.width / 40);
    tp.layout(minWidth: 0);
    Offset newOffset = Offset(offset.dx, offset.dy);
    tp.paint(canvas, newOffset);
  }

  void _drawSelector(Canvas canvas, Size size, double angle) {
    strokePaint.strokeWidth = 1;
    double x = size.width * cos(angle);
    double y = size.height * sin(angle);
    canvas.drawCircle(Offset(x, y), size.width / 10, fillPaint);
    canvas.drawCircle(Offset(x, y), size.height / 10, strokePaint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}
````