DEV Community

Cover image for Flutter. A Quarter Round Slider.
Pablo L
Pablo L

Posted on

Flutter. A Quarter Round Slider.

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 and CustomPaint.

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 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.

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))));
  }
}
Enter fullscreen mode Exit fullscreen mode


`

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.

`

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)),
          ),
        ]));
  }
}
Enter fullscreen mode Exit fullscreen mode


`

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.

`

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;
  }
}
Enter fullscreen mode Exit fullscreen mode


`

All the code

The rest of the code...
`

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;
  }
}
Enter fullscreen mode Exit fullscreen mode


`

Oldest comments (0)