gbos
gbos

Reputation: 570

How to make a custom slider with color gradient?

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:

enter image description here

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

Answers (1)

Thierry
Thierry

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:

enter image description here

In the following full source code, you will find the Demo App, the Generic1dSelector (and its CustomPainter) as well as three helpers:

  1. isDark, as an extension on Color, to determine if a Color is dark or light
  2. showCursorOnHover, as an extension on Widgets, to display a pointer cursor on a Widget
  3. paintCheckers to paint a checkers background if the gradient used by the selector is semi-transparent

Let me know what you think and how to further improve it!

Full source code

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

Related Questions