Reputation: 570
I need to make a heavily customized slider for an app in Flutter. This slider should be as similar as possible to the one in this picture:
It is a continuous slider where I can select a shade from a gradient. The actual gradients need to include a few colors (like red to orange to yellow to white), so it's not only a gradient of a single color with different brightness but a slider with different colors at the same brightness.
Do you have an idea of how I can make this?
Upvotes: 1
Views: 1212
Reputation: 8383
I am working on something quite similar for one of my projects. I hope this can help you. (!!! It's a work in progress !!!)
It's a generic 1D Selector for a given value
This selector is totally agnostic of the meaning of the [value
] that must be normalized on a {0, 1} range.
The Selector is configured with a [pointerColor
], background [gradientColors
] and [withChecker
], whether or not we should display checkers. The pointer's border is white
by default but if [highContrastPointer
] is used, it becomes black
on light backgrounds.
[onSelect] is called when the User selects a new value.
Here is a sample demo with a Color Hue Selector and another one quite similar to your design:
In the following full source code, you will find the Demo App, the Generic1dSelector
(and its CustomPainter
) as well as three helpers:
isDark
, as an extension on Color, to determine if a Color is dark or lightshowCursorOnHover
, as an extension on Widgets, to display a pointer cursor on a WidgetpaintCheckers
to paint a checkers background if the gradient used by the selector is semi-transparentLet me know what you think and how to further improve it!
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
home: HomePage(),
),
);
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
padding: EdgeInsets.all(16.0),
alignment: Alignment.center,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SelectorOne(),
const SizedBox(height: 16.0),
SelectorTwo(),
],
),
),
);
}
}
class SelectorOne extends HookWidget {
@override
Widget build(BuildContext context) {
final _valueOne = useState<double>(.2);
final _colorsOne = [Color(0xff5bc5f6), Color(0xff5a3738)];
return SizedBox(
width: 200,
height: 30,
child: Generic1DSelector(
direction: Axis.horizontal,
value: _valueOne.value,
pointerColor: Color.fromARGB(
(_colorsOne[0].alpha +
_valueOne.value *
(_colorsOne[1].alpha - _colorsOne[0].alpha))
.round(),
(_colorsOne[0].red +
_valueOne.value *
(_colorsOne[1].red - _colorsOne[0].red))
.round(),
(_colorsOne[0].green +
_valueOne.value *
(_colorsOne[1].green - _colorsOne[0].green))
.round(),
(_colorsOne[0].blue +
_valueOne.value *
(_colorsOne[1].blue - _colorsOne[0].blue))
.round(),
),
gradientColors: _colorsOne,
onSelect: (value) => _valueOne.value = value,
),
);
}
}
class SelectorTwo extends HookWidget {
@override
Widget build(BuildContext context) {
final _valueTwo = useState<double>(.6);
return SizedBox(
width: 200,
height: 30,
child: Generic1DSelector(
direction: Axis.horizontal,
value: _valueTwo.value,
pointerColor:
HSVColor.fromAHSV(1, 360 * _valueTwo.value, 1, 1).toColor(),
gradientColors: List<Color>.generate(
11,
(i) => HSVColor.fromAHSV(1, i * 36.0, 1, 1).toColor(),
),
onSelect: (value) => _valueTwo.value = value,
),
);
}
}
/// 1D Selector for a given value
///
/// This selector is totally agnostic of the meaning of the [value] that must be
/// normalized on a {0, 1} range.
/// The Selector is configured with a [pointerColor], background [gradientColors]
/// and [withChecker], whether or not we should display checkers. The pointer's
/// border is `white` by default but if [highContrastPointer] is used, it becomes
/// `black` on light backgrounds.
///
/// [onSelect] is called when the User select a new value.
class Generic1DSelector extends StatelessWidget {
final double value;
final Axis direction;
final Color pointerColor;
final bool highContrastPointer;
final List<Color> gradientColors;
final bool withCheckers;
final ValueChanged<double> onSelect;
const Generic1DSelector({
Key key,
@required this.value,
this.direction = Axis.vertical,
this.pointerColor = Colors.transparent,
this.highContrastPointer = false,
this.gradientColors = const [Colors.white, Colors.black],
this.withCheckers = false,
this.onSelect,
}) : super(key: key);
void _update(
BuildContext context,
PointerEvent event,
Size size,
) {
if (event.down) {
final value = direction == Axis.vertical
? event.localPosition.dy / size.height
: event.localPosition.dx / size.width;
if (value >= 0 && value <= 1) onSelect(value);
}
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
final size = constraints.biggest;
return Listener(
onPointerDown: (event) => _update(context, event, size),
onPointerMove: (event) => _update(context, event, size),
child: CustomPaint(
painter: _Painter(
value: value,
direction: direction,
gradientColors: gradientColors,
withCheckers: withCheckers,
pointerColor: pointerColor,
highContrastPointer: highContrastPointer,
),
),
);
}).showCursorOnHover();
}
}
class _Painter extends CustomPainter {
final double value;
final Axis direction;
final Color pointerColor;
final bool highContrastPointer;
final bool withCheckers;
final List<Color> gradientColors;
_Painter({
this.value,
this.direction,
this.pointerColor,
this.highContrastPointer,
this.withCheckers,
this.gradientColors,
});
void paintBackground(Canvas canvas, Size size) {
if (withCheckers) {
(direction == Axis.vertical)
? paintcheckers(canvas, size, nbCols: 2)
: paintcheckers(canvas, size, nbRows: 2);
}
final rect = Rect.fromLTWH(0, 0, size.width, size.height);
final rrect =
RRect.fromRectAndRadius(rect, Radius.circular(size.shortestSide / 4));
final LinearGradient gradient = LinearGradient(
colors: gradientColors,
begin: direction == Axis.vertical
? Alignment.topCenter
: Alignment.centerLeft,
end: direction == Axis.vertical
? Alignment.bottomCenter
: Alignment.centerRight,
);
final paint = Paint()..shader = gradient.createShader(rect);
canvas.drawRRect(rrect, paint);
}
void paintPointer(Canvas canvas, Size size) {
final double x =
direction == Axis.vertical ? size.width / 2 : value * size.width;
final double y =
direction == Axis.vertical ? value * size.height : size.height / 2;
final Offset c = Offset(x, y);
final double w = direction == Axis.vertical ? size.width : size.height;
final RRect rrect = RRect.fromRectAndRadius(
Rect.fromCenter(center: c, width: w, height: w),
Radius.circular(w / 4));
final Color pointerBorderColor = highContrastPointer
? pointerColor.isDark()
? Colors.white
: Colors.black
: Colors.grey;
canvas.drawRRect(
rrect,
Paint()
..color = pointerColor
..style = PaintingStyle.fill);
canvas.drawRRect(
rrect,
Paint()
..color = pointerBorderColor
..strokeWidth = w / 8
..style = PaintingStyle.stroke);
}
@override
void paint(Canvas canvas, Size size) {
paintBackground(canvas, size);
paintPointer(canvas, size);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
extension HoverExtensions on Widget {
/// Changes cursor on Hover to [cursor] (default is `SystemMouseCursors.click`).
Widget showCursorOnHover({
SystemMouseCursor cursor = SystemMouseCursors.click,
}) {
return MouseRegion(cursor: cursor, child: this);
}
}
extension ColorX on Color {
/// Determines if the color is dark based on a [luminance] (default is `0.179`).
bool isDark({double luminance = 0.179}) => computeLuminance() < luminance;
}
/// Paints a checker in the [canvas] of [size].
///
/// User may define either the [nbRows] and/or the [nbCols], or the [checkerSize]
/// (default is `kCheckerSize`). The checkers will be display in [darkColor] and
/// [lightCOlor] (default are `kCheckerDarkColor` and `kCheckerLightColor`).
void paintcheckers(
Canvas canvas,
Size size, {
int nbRows,
int nbCols,
double checkerSize = 10,
Color darkColor = const Color(0xff777777),
Color lightColor = const Color(0xffaaaaaa),
}) {
nbRows ??= (nbCols == null)
? size.height ~/ checkerSize
: size.height ~/ (size.width / nbCols);
nbCols ??= size.width ~/ (size.height / nbRows);
final checkerWidth = size.width / nbCols;
final checkerHeight = size.height / nbRows;
final darkPaint = Paint()..color = darkColor;
final lightPaint = Paint()..color = lightColor;
for (var i = 0; i < nbCols; i++) {
for (var j = 0; j < nbRows; j++) {
canvas.drawRect(
Rect.fromLTWH(
i * checkerWidth,
j * checkerHeight,
checkerWidth,
checkerHeight,
),
(i + j) % 2 == 0 ? darkPaint : lightPaint,
);
}
}
}
Upvotes: 2