color_scale 0.0.8 copy "color_scale: ^0.0.8" to clipboard
color_scale: ^0.0.8 copied to clipboard

Color scale widget that tries to resemble what we have on google sheets

example/lib/main.dart

import 'dart:math';

import 'package:color_scale/color_scale.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({
    super.key,
  });

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  ColorScaleTypeEnum colorScaleTypeEnum = ColorScaleTypeEnum.hsluv;

  @override
  Widget build(BuildContext context) => MaterialApp(
        debugShowCheckedModeBanner: false,
        title: 'Color Scale Demo',
        theme: ThemeData(
          brightness: Brightness.dark,
          colorScheme: ColorScheme.fromSeed(
            seedColor: const Color(0xff8b6df6),
            brightness: Brightness.dark,
          ),
          scaffoldBackgroundColor: const Color(0xff0f0d16),
          appBarTheme: const AppBarTheme(
            backgroundColor: Color(0xff0f0d16),
            elevation: 0,
            centerTitle: true,
          ),
          inputDecorationTheme: InputDecorationTheme(
            filled: true,
            fillColor: const Color(0xff2a2733),
            border: OutlineInputBorder(
              borderRadius: BorderRadius.circular(12),
              borderSide: BorderSide.none,
            ),
            labelStyle: const TextStyle(color: Colors.white70),
            contentPadding:
                const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
          ),
          sliderTheme: const SliderThemeData(
            activeTrackColor: Color(0xff8b6df6),
            inactiveTrackColor: Color(0x55ffffff),
            thumbColor: Color(0xffc4b1ff),
          ),
          textTheme: const TextTheme(
            headlineMedium: TextStyle(
              fontWeight: FontWeight.w600,
              letterSpacing: 0.5,
            ),
            titleMedium: TextStyle(
              fontWeight: FontWeight.w600,
              color: Colors.white70,
            ),
            bodyMedium: TextStyle(
              color: Colors.white70,
            ),
          ),
        ),
        home: Scaffold(
          appBar: AppBar(
            title: const Text('Color scale'),
          ),
          body: SafeArea(
            child: SingleChildScrollView(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  DropdownButtonFormField<ColorScaleTypeEnum>(
                    initialValue: colorScaleTypeEnum,
                    onChanged: (ColorScaleTypeEnum? newValue) {
                      setState(() {
                        colorScaleTypeEnum = newValue!;
                      });
                    },
                    decoration: const InputDecoration(
                      labelText: 'Color space',
                    ),
                    items: ColorScaleTypeEnum.values
                        .map<DropdownMenuItem<ColorScaleTypeEnum>>(
                          (ColorScaleTypeEnum value) =>
                              DropdownMenuItem<ColorScaleTypeEnum>(
                            value: value,
                            child: Text(value.toString().split('.').last),
                          ),
                        )
                        .toList(),
                  ),
                  const SizedBox(height: 24),
                  const SectionHeader(text: 'Example with stops'),
                  StopsValueAndColorsWidget(
                    key: UniqueKey(),
                    colorStops: <double, Color>{
                      -5: Colors.red,
                      0: Colors.white,
                      5: Colors.green,
                    },
                    colorScaleTypeEnum: colorScaleTypeEnum,
                  ),
                  const SizedBox(height: 24),
                  const SectionHeader(text: 'Example with slider'),
                  const ExampleWithSlider(
                    text: 'Slide between min and max color',
                  ),
                  const SizedBox(height: 32),
                  const SectionHeader(text: 'Color scale presets'),
                  TestColorScale(
                    text: 'Colors from red to green',
                    values: const [-20, -15, -10, -5, 0, 5, 10, 15],
                    colorScaleTypeEnum: colorScaleTypeEnum,
                  ),
                  TestColorScale(
                    text: 'Colors from blue to green',
                    values: const [-20, -15, -10, -5, 0, 5, 10, 15],
                    minColor: Colors.blue,
                    colorScaleTypeEnum: colorScaleTypeEnum,
                  ),
                  TestColorScale(
                    text: 'Colors from red to yellow',
                    values: const [-20, -15, -10, -5, 0, 5, 10, 15],
                    maxColor: Colors.yellow,
                    colorScaleTypeEnum: colorScaleTypeEnum,
                  ),
                  const SizedBox(height: 24),
                  const SectionHeader(text: 'Childless examples'),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      ClipRRect(
                        borderRadius:
                            const BorderRadius.all(Radius.circular(10)),
                        child: SizedBox(
                          width: 60,
                          height: 60,
                          child: ColorScaleWidget(
                            value: 0,
                            minColor: Colors.white,
                            maxColor: Colors.black,
                            colorScaleTypeEnum: colorScaleTypeEnum,
                          ),
                        ),
                      ),
                      const SizedBox(width: 20),
                      ClipRRect(
                        borderRadius:
                            const BorderRadius.all(Radius.circular(10)),
                        child: SizedBox(
                          width: 60,
                          height: 60,
                          child: ColorScaleStopsWidget(
                            value: 0,
                            colorStops: <double, Color>{
                              -20: Colors.red,
                              0: Colors.yellow,
                              20: Colors.green,
                            },
                            colorScaleTypeEnum: colorScaleTypeEnum,
                          ),
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 20),
                  const SectionHeader(
                      text: 'Example with border radius and padding'),
                  ColorScaleStopsWidget(
                    borderRadius: const BorderRadius.all(Radius.circular(12)),
                    padding: const EdgeInsets.all(12),
                    value: 0,
                    colorStops: <double, Color>{
                      -20: Colors.red,
                      0: Colors.orange,
                      20: Colors.green,
                    },
                    colorScaleTypeEnum: colorScaleTypeEnum,
                    child: const Text(
                      'P',
                      style: TextStyle(fontWeight: FontWeight.bold),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      );
}

class ExampleWithSlider extends StatefulWidget {
  final String text;

  final double minValue;
  final Color minColor;

  final double maxValue;
  final Color maxColor;

  final ColorScaleTypeEnum colorScaleTypeEnum;
  const ExampleWithSlider({
    this.text = '',
    this.minValue = -20,
    this.minColor = Colors.red,
    this.maxValue = 20,
    this.maxColor = Colors.green,
    this.colorScaleTypeEnum = ColorScaleTypeEnum.hsluv,
    super.key,
  });

  @override
  State<ExampleWithSlider> createState() => _ExampleWithSliderState();
}

class _ExampleWithSliderState extends State<ExampleWithSlider> {
  Color minColor = Colors.red;
  double minValue = -20;

  Color maxColor = Colors.green;
  double maxValue = 20;

  double value = 0;
  late final TextEditingController minController;
  late final TextEditingController maxController;

  @override
  void initState() {
    super.initState();

    minColor = widget.minColor;
    minValue = widget.minValue;

    maxColor = widget.maxColor;
    maxValue = widget.maxValue;

    minController = TextEditingController(text: minValue.toString());
    maxController = TextEditingController(text: maxValue.toString());
  }

  @override
  void dispose() {
    minController.dispose();
    maxController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => Card(
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(10),
        ),
        elevation: 5,
        child: Container(
          margin: const EdgeInsets.all(10),
          child: Column(
            children: [
              Column(
                children: [
                  const SizedBox(
                    height: 20,
                  ),
                  const Text('Minimum Color'),
                  MyColorPicker(
                    onSelectColor: (color) => setState(() => minColor = color),
                    initialColor: minColor,
                    availableColors: [
                      Colors.red,
                      Colors.orange,
                      Colors.amberAccent,
                      Colors.purple.withValues(alpha: 0.25),
                      Colors.pink.withValues(alpha: 0.5)
                    ],
                  ),
                  const Text('Minimum Value'),
                  TextField(
                    keyboardType: const TextInputType.numberWithOptions(
                      signed: true,
                      decimal: true,
                    ),
                    controller: minController,
                    onChanged: (inputValue) => setState(() {
                      final double? parsedValue = double.tryParse(inputValue);
                      if (parsedValue == null) {
                        return;
                      }
                      minValue = parsedValue;
                      value = max(value, minValue);
                    }),
                  ),
                  const Text('Maximum Color'),
                  MyColorPicker(
                    onSelectColor: (color) => setState(() => maxColor = color),
                    initialColor: maxColor,
                    availableColors: [
                      Colors.green,
                      Colors.greenAccent,
                      Colors.blue,
                      Colors.cyan.withValues(alpha: 0.25),
                      Colors.teal.withValues(alpha: 0.5)
                    ],
                  ),
                  const Text('Maximum Value'),
                  TextField(
                    keyboardType: const TextInputType.numberWithOptions(
                      signed: true,
                      decimal: true,
                    ),
                    controller: maxController,
                    onChanged: (inputValue) => setState(() {
                      final double? parsedValue = double.tryParse(inputValue);
                      if (parsedValue == null) {
                        return;
                      }
                      maxValue = parsedValue;
                      value = min(value, maxValue);
                    }),
                  ),
                  Slider(
                      min: minValue,
                      max: maxValue,
                      value: value,
                      onChanged: onSliderMove),
                  ClipRRect(
                    borderRadius: const BorderRadius.all(Radius.circular(10)),
                    child: ColorScaleWidget(
                      value: value,
                      minValue: minValue,
                      minColor: minColor,
                      maxValue: maxValue,
                      maxColor: maxColor,
                      colorScaleTypeEnum: widget.colorScaleTypeEnum,
                      child: Container(
                        margin: const EdgeInsets.all(5),
                        child: Column(
                          children: [
                            Text(widget.text),
                            Text('value: ${value.toStringAsFixed(2)}')
                          ],
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ],
          ),
        ),
      );

  void onSliderMove(double value) {
    setState(() {
      this.value = value;
    });
  }
}

class MyColorPicker extends StatefulWidget {
  // This function sends the selected color to outside
  final void Function(Color) onSelectColor;

  // List of pickable colors
  final List<Color> availableColors;

  // The default picked color
  final Color initialColor;

  // Determine shapes of color cells
  final bool circleItem;

  const MyColorPicker({
    required this.onSelectColor,
    required this.availableColors,
    required this.initialColor,
    this.circleItem = true,
    super.key,
  });

  @override
  _MyColorPickerState createState() => _MyColorPickerState();
}

class _MyColorPickerState extends State<MyColorPicker> {
  // This variable used to determine where the checkmark will be
  late Color _pickedColor;

  @override
  void initState() {
    _pickedColor = widget.initialColor;
    super.initState();
  }

  @override
  void didUpdateWidget(covariant MyColorPicker oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.initialColor != widget.initialColor) {
      _pickedColor = widget.initialColor;
    }
  }

  @override
  Widget build(BuildContext context) => SizedBox(
        width: double.infinity,
        height: 80,
        child: GridView.builder(
          gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
              maxCrossAxisExtent: 50,
              crossAxisSpacing: 10,
              mainAxisSpacing: 10),
          itemCount: widget.availableColors.length,
          itemBuilder: (context, index) {
            final Color itemColor = widget.availableColors[index];
            return InkWell(
              onTap: () {
                widget.onSelectColor(itemColor);
                setState(() {
                  _pickedColor = itemColor;
                });
              },
              child: Container(
                width: 50,
                height: 50,
                decoration: BoxDecoration(
                    color: itemColor,
                    shape: widget.circleItem == true
                        ? BoxShape.circle
                        : BoxShape.rectangle,
                    border: Border.all(color: Colors.grey.shade300)),
                child: itemColor == _pickedColor
                    ? const Center(
                        child: Icon(
                          Icons.check,
                          color: Colors.white,
                        ),
                      )
                    : Container(),
              ),
            );
          },
        ),
      );
}

class TestColorScale extends StatelessWidget {
  final String text;
  final List<double> values;

  final double minValue;
  final Color minColor;

  final double maxValue;
  final Color maxColor;

  final ColorScaleTypeEnum colorScaleTypeEnum;

  const TestColorScale({
    this.text = '',
    this.values = const [],
    this.minValue = -20,
    this.minColor = Colors.red,
    this.maxValue = 20,
    this.maxColor = Colors.green,
    this.colorScaleTypeEnum = ColorScaleTypeEnum.hsluv,
    super.key,
  });

  @override
  Widget build(BuildContext context) => Column(
        children: [
          Text(text),
          Wrap(
            children: values
                .map(
                  (value) => Container(
                    margin: const EdgeInsets.all(10),
                    child: ClipRRect(
                      borderRadius: const BorderRadius.all(Radius.circular(10)),
                      child: Container(
                        alignment: Alignment.bottomRight,
                        width: 60,
                        child: ColorScaleWidget(
                          value: value,
                          minValue: minValue,
                          minColor: minColor,
                          maxValue: maxValue,
                          maxColor: maxColor,
                          colorScaleTypeEnum: colorScaleTypeEnum,
                          child: Container(
                            margin: const EdgeInsets.all(5),
                            child: Wrap(
                              crossAxisAlignment: WrapCrossAlignment.center,
                              alignment: WrapAlignment.end,
                              children: [
                                Text(
                                  'Value: ',
                                  style: Theme.of(context)
                                      .textTheme
                                      .bodyMedium!
                                      .copyWith(fontSize: 10),
                                ),
                                Center(
                                  child: FittedBox(
                                    fit: BoxFit.scaleDown,
                                    child: Text(
                                      '${value.toStringAsFixed(2)}%',
                                    ),
                                  ),
                                ),
                              ],
                            ),
                          ),
                        ),
                      ),
                    ),
                  ),
                )
                .toList(),
          ),
          const SizedBox(
            height: 50,
          )
        ],
      );
}

class StopsValueAndColorsWidget extends StatefulWidget {
  final Map<double, Color> colorStops;
  final ColorScaleTypeEnum colorScaleTypeEnum;

  const StopsValueAndColorsWidget({
    required this.colorStops,
    required this.colorScaleTypeEnum,
    super.key,
  });

  @override
  State<StopsValueAndColorsWidget> createState() =>
      _StopsValueAndColorsWidgetState();
}

class _StopsValueAndColorsWidgetState extends State<StopsValueAndColorsWidget> {
  final List<_StopEntry> stops = [];
  double value = 0;
  late final List<Color> _baseAvailableColors;

  @override
  void initState() {
    super.initState();

    // Create stable entries once; they can reorder visually without losing state.
    stops.addAll(
      widget.colorStops.entries.map(
        (entry) => _StopEntry(
          id: UniqueKey().toString(),
          value: entry.key,
          color: entry.value,
        ),
      ),
    );
    // Keep a stable palette so previously available colors don't disappear.
    _baseAvailableColors = {
      ...widget.colorStops.values,
      Colors.purple.withValues(alpha: 0.25),
      Colors.pink.withValues(alpha: 0.5),
    }.toList();
  }

  @override
  void dispose() {
    for (final stop in stops) {
      stop.dispose();
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final List<_StopEntry> sortedStops = [...stops]
      ..sort((a, b) => a.value.compareTo(b.value));

    final Map<double, Color> orderedStops = {
      for (final stop in sortedStops) stop.value: stop.color,
    };

    // Stable palette + current stop colors (deduped) so options don't disappear.
    final List<Color> availableColors = {
      ..._baseAvailableColors,
      ...orderedStops.values,
    }.toList();

    final double minStop = sortedStops.first.value;
    final double maxStop = sortedStops.last.value;
    final double clampedValue = value.clamp(minStop, maxStop);

    return Card(
      child: Container(
        margin: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Column(
              children: sortedStops.asMap().entries.map((entry) {
                final int index = entry.key + 1;
                final _StopEntry stop = entry.value;

                return Padding(
                  key: ValueKey(stop.id),
                  padding: const EdgeInsets.only(bottom: 16),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      MyColorPicker(
                        key: ValueKey('picker_${stop.id}'),
                        onSelectColor: (color) => setState(() {
                          stop.color = color;
                        }),
                        initialColor: stop.color,
                        availableColors: availableColors,
                      ),
                      Container(
                        width: 140,
                        margin: const EdgeInsets.only(bottom: 10),
                        child: TextField(
                          key: ValueKey('field_${stop.id}'),
                          keyboardType: const TextInputType.numberWithOptions(
                            signed: true,
                            decimal: true,
                          ),
                          controller: stop.controller,
                          onChanged: (inputValue) => setState(() {
                            final double? newValue =
                                double.tryParse(inputValue);
                            if (newValue == null) {
                              return;
                            }
                            stop.value = newValue;
                          }),
                          decoration: InputDecoration(
                            label: Text('Stop $index'),
                          ),
                        ),
                      ),
                    ],
                  ),
                );
              }).toList(),
            ),
            Slider(
              min: minStop,
              max: maxStop,
              value: clampedValue,
              onChanged: onSliderMove,
            ),
            ClipRRect(
              borderRadius: const BorderRadius.all(Radius.circular(12)),
              child: ColorScaleStopsWidget(
                value: clampedValue,
                colorStops: orderedStops,
                colorScaleTypeEnum: widget.colorScaleTypeEnum,
                child: Container(
                  margin: const EdgeInsets.all(10),
                  child: Column(
                    children: [
                      Text(
                        'Slide between different stops',
                        style: Theme.of(context)
                            .textTheme
                            .bodyMedium
                            ?.copyWith(color: Colors.black),
                      ),
                      Text('Value: ${clampedValue.toStringAsFixed(2)}',
                          style: Theme.of(context)
                              .textTheme
                              .bodyMedium
                              ?.copyWith(color: Colors.black)),
                    ],
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  void onSliderMove(double value) {
    setState(() {
      this.value = value;
    });
  }
}

class SectionHeader extends StatelessWidget {
  final String text;
  const SectionHeader({required this.text, super.key});

  @override
  Widget build(BuildContext context) => Padding(
        padding: const EdgeInsets.only(bottom: 8),
        child: Text(
          text,
          style: Theme.of(context).textTheme.titleMedium,
          textAlign: TextAlign.center,
        ),
      );
}

class _StopEntry {
  _StopEntry({required this.id, required this.value, required this.color})
      : controller = TextEditingController(
          text: value.toStringAsFixed(1),
        );

  final String id;
  double value;
  Color color;
  final TextEditingController controller;

  void dispose() => controller.dispose();
}
6
likes
140
points
136
downloads

Publisher

verified publisherincaview.com

Weekly Downloads

Color scale widget that tries to resemble what we have on google sheets

Homepage
Repository (GitHub)
View/report issues

Documentation

API reference

License

Apache-2.0 (license)

Dependencies

flutter, hsluv, oklch

More

Packages that depend on color_scale