Skip to content

Instantly share code, notes, and snippets.

@slightfoot
Created December 18, 2024 20:09
Show Gist options
  • Save slightfoot/332150c13cc48b2a2e50475a61b3198b to your computer and use it in GitHub Desktop.
Save slightfoot/332150c13cc48b2a2e50475a61b3198b to your computer and use it in GitHub Desktop.
Labelled Slider - by Simon Lightfoot :: #HumpdayQandA on 18th December 2024 :: https://www.youtube.com/watch?v=kMVayNJwtsU
// 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;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment