// MIT License // // Copyright (c) 2024 Simon Lightfoot // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import 'dart:math' as math; import 'package:flutter/material.dart'; void main() { runApp(App()); } class App extends StatelessWidget { const App({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: Home(), ); } } class Home extends StatefulWidget { const Home({super.key}); @override State<Home> createState() => _HomeState(); } class _HomeState extends State<Home> { double _value = 50; @override Widget build(BuildContext context) { return Material( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( '$_value', style: TextStyle(fontSize: 32.0), ), const SizedBox(height: 32), SliderTheme( data: Theme.of(context).sliderTheme.copyWith( trackHeight: 12.0, ), child: LabelledSlider( buildLabel: (int index) { if (index == 2) { return 'This is second division'; } return 'Division $index'; }, onChanged: (double value) { setState(() => _value = value); }, divisions: 5, min: 0, max: 100, value: _value, padding: EdgeInsets.only(right: 25.0), ), ), ], ), ); } } class LabelledSlider extends StatelessWidget { const LabelledSlider({ super.key, required this.onChanged, required this.buildLabel, required this.divisions, required this.min, required this.max, required this.value, required this.padding, }); final ValueChanged<double>? onChanged; final String Function(int index) buildLabel; final int divisions; final double min; final double max; final double value; final EdgeInsets padding; @override Widget build(BuildContext context) { return CustomPaint( painter: _SliderLabelPainter( SliderTheme.of(context), DefaultTextStyle.of(context), MediaQuery.textScalerOf(context), this, ), child: Slider( onChanged: onChanged, divisions: divisions, min: min, max: max, value: value, ), ); } } class _SliderLabelPainter extends CustomPainter { _SliderLabelPainter( this.sliderTheme, this.defaultTextStyle, this.textScaler, this.widget, ); final SliderThemeData sliderTheme; final DefaultTextStyle defaultTextStyle; final TextScaler textScaler; final LabelledSlider widget; @override void paint(Canvas canvas, Size size) { final trackRect = Offset(8.0, 8.0) & Size(size.width - 18.0, size.height - 8.0); final padding = trackRect.height; final adjustedTrackWidth = trackRect.width - padding; final labelPainter = TextPainter( maxLines: 1, textScaler: textScaler, textDirection: TextDirection.ltr, textAlign: defaultTextStyle.textAlign ?? TextAlign.left, textHeightBehavior: defaultTextStyle.textHeightBehavior, textWidthBasis: defaultTextStyle.textWidthBasis, ); var largestLabel = Size.zero; for (int i = 0; i <= widget.divisions; i++) { labelPainter.text = TextSpan( text: widget.buildLabel(i), style: defaultTextStyle.style, ); labelPainter.layout(); final width = labelPainter.size.width + widget.padding.horizontal; final height = labelPainter.size.height + widget.padding.vertical; if (width > largestLabel.width) { largestLabel = Size(width, largestLabel.height); } if (height > largestLabel.height) { largestLabel = Size(largestLabel.width, height); } } final dy = trackRect.center.dy; for (int i = 0; i <= widget.divisions; i++) { final value = i / widget.divisions; // The ticks are mapped to be within the track, so the tick mark width // must be subtracted from the track width. final dx = trackRect.left + value * adjustedTrackWidth + padding / 2; final tickMarkOffset = Offset(dx, dy); canvas.drawLine( tickMarkOffset, tickMarkOffset + Offset(0, largestLabel.width), Paint() // ..color = (sliderTheme.inactiveTickMarkColor ?? Colors.grey) ..strokeWidth = 2.0, ); labelPainter.text = TextSpan( text: widget.buildLabel(i), style: defaultTextStyle.style, ); labelPainter.layout(); canvas.save(); canvas.translate( tickMarkOffset.dx - labelPainter.size.height + widget.padding.vertical, tickMarkOffset.dy + labelPainter.size.width + widget.padding.horizontal, ); canvas.rotate(-math.pi / 2); labelPainter.paint(canvas, Offset.zero); canvas.restore(); } } @override bool shouldRepaint(covariant _SliderLabelPainter oldDelegate) { return sliderTheme != oldDelegate.sliderTheme || defaultTextStyle != oldDelegate.defaultTextStyle || textScaler != oldDelegate.textScaler || widget.buildLabel != oldDelegate.widget.buildLabel || widget.divisions != oldDelegate.widget.divisions || widget.min != oldDelegate.widget.min || widget.max != oldDelegate.widget.max || widget.value != oldDelegate.widget.value; } }