flutterslider

Flutter 2: How to create a Gradient Range Slider


I am trying to create a Range Slider with a gradient body.

I have based my code on a Gradient Slider code from this response: Flutter slider with gradient

I adjusted the Paint override depending on the Interface of the object.

So this is the situation:

Error

======== Exception caught by rendering library =====================================================
The following _CastError was thrown during paint():
Null check operator used on a null value

The relevant error-causing widget was: 
  RangeSlider RangeSlider:file:///[PATH]flutter/lib/source/shared_components/common/slider_range/slider_range.item.dart:19:14
When the exception was thrown, this was the stack: 
#0      BaseSliderTrackShape.getPreferredRect (package:flutter/src/material/slider_theme.dart:1484:53)
#1      _RenderRangeSlider.paint (package:flutter/src/material/range_slider.dart:1267:58)
#2      RenderObject._paintWithContext (package:flutter/src/rendering/object.dart:2451:7)
#3      PaintingContext.paintChild (package:flutter/src/rendering/object.dart:187:13)
#4      RenderProxyBoxMixin.paint (package:flutter/src/rendering/proxy_box.dart:140:15)
#5      PaintingContext.pushLayer (package:flutter/src/rendering/object.dart:387:12)
#6      RenderLeaderLayer.paint (package:flutter/src/rendering/proxy_box.dart:5138:13)
#7      RenderObject._paintWithContext (package:flutter/src/rendering/object.dart:2451:7)
#8      PaintingContext.paintChild (package:flutter/src/rendering/object.dart:187:13)
#9      RenderBoxContainerDefaultsMixin.defaultPaint (package:flutter/src/rendering/box.dart:2844:15)
#10     RenderFlex.paint (package:flutter/src/rendering/flex.dart:1078:7)
#11     RenderObject._paintWithContext (package:flutter/src/rendering/object.dart:2451:7)
#12     PaintingContext.paintChild (package:flutter/src/rendering/object.dart:187:13)
#13     RenderShiftedBox.paint (package:flutter/src/rendering/shifted_box.dart:79:15)
#14     RenderObject._paintWithContext (package:flutter/src/rendering/object.dart:2451:7)
#15     PaintingContext.paintChild (package:flutter/src/rendering/object.dart:187:13)
#16     RenderProxyBoxMixin.paint (package:flutter/src/rendering/proxy_box.dart:140:15)
#17     RenderDecoratedBox.paint (package:flutter/src/rendering/proxy_box.dart:2169:11)
#18     RenderObject._paintWithContext (package:flutter/src/rendering/object.dart:2451:7)
#19     PaintingContext.paintChild (package:flutter/src/rendering/object.dart:187:13)
#20     RenderShiftedBox.paint (package:flutter/src/rendering/shifted_box.dart:79:15)
#21     RenderObject._paintWithContext (package:flutter/src/rendering/object.dart:2451:7)
#22     PaintingContext.paintChild (package:flutter/src/rendering/object.dart:187:13)
#23     RenderProxyBoxMixin.paint (package:flutter/src/rendering/proxy_box.dart:140:15)
#24     RenderObject._paintWithContext (package:flutter/src/rendering/object.dart:2451:7)
#25     PaintingContext.paintChild (package:flutter/src/rendering/object.dart:187:13)
#26     RenderProxyBoxMixin.paint (package:flutter/src/rendering/proxy_box.dart:140:15)
#27     RenderObject._paintWithContext (package:flutter/src/rendering/object.dart:2451:7)
#28     PaintingContext.paintChild (package:flutter/src/rendering/object.dart:187:13)
#29     RenderProxyBoxMixin.paint (package:flutter/src/rendering/proxy_box.dart:140:15)
#30     RenderObject._paintWithContext (package:flutter/src/rendering/object.dart:2451:7)
#31     PaintingContext.paintChild (package:flutter/src/rendering/object.dart:187:13)
#32     RenderProxyBoxMixin.paint (package:flutter/src/rendering/proxy_box.dart:140:15)
#33     RenderObject._paintWithContext (package:flutter/src/rendering/object.dart:2451:7)
#34     PaintingContext.paintChild (package:flutter/src/rendering/object.dart:187:13)
#35     RenderBoxContainerDefaultsMixin.defaultPaint (package:flutter/src/rendering/box.dart:2844:15)
#36     RenderFlex.paint (package:flutter/src/rendering/flex.dart:1078:7)
#37     RenderObject._paintWithContext (package:flutter/src/rendering/object.dart:2451:7)
#38     PaintingContext.paintChild (package:flutter/src/rendering/object.dart:187:13)
#39     RenderShiftedBox.paint (package:flutter/src/rendering/shifted_box.dart:79:15)
#40     RenderObject._paintWithContext (package:flutter/src/rendering/object.dart:2451:7)
#41     PaintingContext.paintChild (package:flutter/src/rendering/object.dart:187:13)
#42     _RenderSingleChildViewport.paint.paintContents (package:flutter/src/widgets/single_child_scroll_view.dart:542:17)
#43     _RenderSingleChildViewport.paint (package:flutter/src/widgets/single_child_scroll_view.dart:556:9)
#44     RenderObject._paintWithContext (package:flutter/src/rendering/object.dart:2451:7)
#45     PaintingContext._repaintCompositedChild (package:flutter/src/rendering/object.dart:141:11)
#46     PaintingContext.repaintCompositedChild (package:flutter/src/rendering/object.dart:100:5)
#47     PipelineOwner.flushPaint (package:flutter/src/rendering/object.dart:995:29)
#48     RendererBinding.drawFrame (package:flutter/src/rendering/binding.dart:499:19)
#49     WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:883:13)
#50     RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:363:5)
#51     SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1144:15)
#52     SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1081:9)
#53     SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:995:5)
#57     _invoke (dart:ui/hooks.dart:151:10)
#58     PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:308:5)
#59     _drawFrame (dart:ui/hooks.dart:115:31)
(elided 3 frames from dart:async)
The following RenderObject was being processed when the exception was fired: _RenderRangeSlider#59316
...  parentData: <none> (can use size)
...  constraints: BoxConstraints(0.0<=w<=367.4, 0.0<=h<=Infinity)
...  semantic boundary
...  size: Size(367.4, 48.0)
RenderObject: _RenderRangeSlider#59316
  parentData: <none> (can use size)
  constraints: BoxConstraints(0.0<=w<=367.4, 0.0<=h<=Infinity)
  semantic boundary
  size: Size(367.4, 48.0)
====================================================================================================

Track

import 'package:flutter/material.dart';

class GradientRectRangeSliderTrackShape extends RangeSliderTrackShape
    with BaseSliderTrackShape {
  const GradientRectRangeSliderTrackShape({
    this.gradient = const LinearGradient(
      colors: [
        Colors.red,
        Colors.yellow,
      ],
    ),
    this.darkenInactive = true,
  });

  final LinearGradient gradient;
  final bool darkenInactive;

  @override
  void paint(
      PaintingContext context,
      Offset offset,
      {
        required RenderBox parentBox,
        required SliderThemeData sliderTheme,
        required Animation<double> enableAnimation,
        required TextDirection textDirection,
        required Offset startThumbCenter,
        required Offset endThumbCenter,
        bool isDiscrete = false,
        bool isEnabled = false,
        double additionalActiveTrackHeight = 2,
      }
    ) {
    assert(
      sliderTheme.disabledActiveTrackColor != null,
      'sliderTheme.disabledActiveTrackColor is required'
    );
    assert(
      sliderTheme.disabledInactiveTrackColor != null,
      'sliderTheme.disabledInactiveTrackColor is required'
    );
    assert(
      sliderTheme.activeTrackColor != null,
      'sliderTheme.activeTrackColor is required'
    );
    assert(
      sliderTheme.inactiveTrackColor != null,
      'sliderTheme.inactiveTrackColor'
    );
    assert(
      sliderTheme.thumbShape != null,
      'sliderTheme.thumbShape is required'
    );
    assert(
      sliderTheme.trackHeight != null && sliderTheme.trackHeight! > 0,
      'sliderTheme.trackHeight != null and sliderTheme.trackHeight! > 0'
      'are required'
    );

    final Rect trackRect = getPreferredRect(
      parentBox: parentBox,
      offset: offset,
      sliderTheme: sliderTheme,
      isEnabled: isEnabled,
      isDiscrete: isDiscrete,
    );

    final activeGradientRect = Rect.fromLTRB(
      startThumbCenter.dx,
      (textDirection == TextDirection.ltr)
          ? trackRect.top - (additionalActiveTrackHeight / 2)
          : trackRect.top,
      endThumbCenter.dx,
      (textDirection == TextDirection.ltr)
          ? trackRect.bottom + (additionalActiveTrackHeight / 2)
          : trackRect.bottom,
    );

    // Assign the track segment paints, which are leading: active and
    // trailing: inactive.
    final ColorTween activeTrackColorTween = ColorTween(
        begin: sliderTheme.disabledActiveTrackColor,
        end: sliderTheme.activeTrackColor);
    final ColorTween inactiveTrackColorTween = darkenInactive
        ? ColorTween(
        begin: sliderTheme.disabledInactiveTrackColor,
        end: sliderTheme.inactiveTrackColor
    )
        : activeTrackColorTween;
    final Paint activePaint = Paint()
      ..shader = gradient.createShader(activeGradientRect)
      ..color = activeTrackColorTween.evaluate(enableAnimation)!;
    final Paint inactivePaint = Paint()
      ..color = inactiveTrackColorTween.evaluate(enableAnimation)!;
    final Paint leftTrackPaint;
    final Paint rightTrackPaint;
    switch (textDirection) {
      case TextDirection.ltr:
        leftTrackPaint = activePaint;
        rightTrackPaint = inactivePaint;
        break;
      case TextDirection.rtl:
        leftTrackPaint = inactivePaint;
        rightTrackPaint = activePaint;
        break;
    }

    final Radius trackRadius = Radius.circular(trackRect.height / 2);
    final Radius activeTrackRadius = Radius.circular(trackRect.height / 2 + 1);

    context.canvas.drawRRect(
      RRect.fromLTRBAndCorners(
        startThumbCenter.dx,
        (textDirection == TextDirection.ltr)
            ? trackRect.top - (additionalActiveTrackHeight / 2)
            : trackRect.top,
        endThumbCenter.dx,
        (textDirection == TextDirection.ltr)
            ? trackRect.bottom + (additionalActiveTrackHeight / 2)
            : trackRect.bottom,
        topLeft: (textDirection == TextDirection.ltr)
            ? activeTrackRadius
            : trackRadius,
        bottomLeft: (textDirection == TextDirection.ltr)
            ? activeTrackRadius
            : trackRadius,
      ),
      leftTrackPaint,
    );
    context.canvas.drawRRect(
      RRect.fromLTRBAndCorners(
        startThumbCenter.dx,
        (textDirection == TextDirection.rtl)
            ? trackRect.top - (additionalActiveTrackHeight / 2)
            : trackRect.top,
        endThumbCenter.dx,
        (textDirection == TextDirection.rtl)
            ? trackRect.bottom + (additionalActiveTrackHeight / 2)
            : trackRect.bottom,
        topRight: (textDirection == TextDirection.rtl)
            ? activeTrackRadius
            : trackRadius,
        bottomRight: (textDirection == TextDirection.rtl)
            ? activeTrackRadius
            : trackRadius,
      ),
      rightTrackPaint,
    );
  }
}

Widget

SliderTheme(
  data: SliderThemeData(
    rangeTrackShape: GradientRectRangeSliderTrackShape()
  ),
  child: RangeSlider(
    onChanged: onChanged,
    values: data.values,
    min: data.min,
    max: data.max,
    divisions: data.labels.length
  )
)

Solution

  • This is the code to create a gradient slider range

    Track It can be put directly in the Theme of your app

    import 'package:flutter/material.dart';
    import 'dart:math' as math;
    
    class GradientRectRangeSliderTrackShape extends RangeSliderTrackShape {
      const GradientRectRangeSliderTrackShape({
        this.gradient = const LinearGradient(
          colors: [
            Colors.red,
            Colors.yellow,
          ],
        ),
        this.darkenInactive = true,
      });
    
      final LinearGradient gradient;
      final bool darkenInactive;
    
      @override
      Rect getPreferredRect({
        required RenderBox parentBox,
        Offset offset = Offset.zero,
        required SliderThemeData sliderTheme,
        bool isEnabled = false,
        bool isDiscrete = false,
      }) {
        assert(
          sliderTheme.overlayShape != null,
          'sliderTheme.overlayShape is required'
        );
        assert(
          sliderTheme.trackHeight != null,
          'sliderTheme.trackHeight is required'
        );
    
        final double overlayWidth = sliderTheme.overlayShape!
          .getPreferredSize(isEnabled, isDiscrete).width;
        final double trackHeight = sliderTheme.trackHeight!;
        assert(overlayWidth >= 0);
        assert(trackHeight >= 0);
    
        final double trackLeft = offset.dx + overlayWidth / 2;
        final double trackTop = offset.dy
            + (parentBox.size.height - trackHeight) / 2;
        final double trackRight = trackLeft + parentBox.size.width - overlayWidth;
        final double trackBottom = trackTop + trackHeight;
        return Rect.fromLTRB(
          math.min(
            trackLeft,
            trackRight
          ),
          trackTop,
          math.max(
            trackLeft,
            trackRight
          ),
          trackBottom
        );
      }
    
      @override
      void paint(
          PaintingContext context,
          Offset offset, {
            required RenderBox parentBox,
            required SliderThemeData sliderTheme,
            required Animation<double> enableAnimation,
            required Offset startThumbCenter,
            required Offset endThumbCenter,
            bool isEnabled = false,
            bool isDiscrete = false,
            required TextDirection textDirection,
            double additionalActiveTrackHeight = 2,
          }) {
        assert(
          sliderTheme.disabledActiveTrackColor != null,
          'sliderTheme.disabledActiveTrackColor is required'
        );
        assert(
          sliderTheme.disabledInactiveTrackColor != null,
          'sliderTheme.disabledInactiveTrackColor is required'
        );
        assert(
          sliderTheme.activeTrackColor != null,
          'sliderTheme.activeTrackColor is required'
        );
        assert(
          sliderTheme.inactiveTrackColor != null,
          'sliderTheme.inactiveTrackColor is required'
        );
        assert(
          sliderTheme.rangeThumbShape != null,
          'sliderTheme.rangeThumbShape iss required'
        );
        assert(
          sliderTheme.trackHeight != null && sliderTheme.trackHeight! > 0,
          'sliderTheme.trackHeight != null and sliderTheme.trackHeight! > 0'
            'are required'
        );
    
        final Rect trackRect = getPreferredRect(
          parentBox: parentBox,
          offset: offset,
          sliderTheme: sliderTheme,
          isEnabled: isEnabled,
          isDiscrete: isDiscrete,
        );
    
        final ColorTween activeTrackColorTween = ColorTween(
          begin: sliderTheme.disabledActiveTrackColor,
          end: sliderTheme.activeTrackColor,
        );
        final ColorTween inactiveTrackColorTween = darkenInactive
            ? ColorTween(
              begin: sliderTheme.disabledInactiveTrackColor,
              end: sliderTheme.inactiveTrackColor,
            )
            : activeTrackColorTween;
        final Paint activePaint = Paint()
          ..shader = gradient.createShader(trackRect)
          ..color = activeTrackColorTween.evaluate(enableAnimation)!;
        final Paint inactivePaint = Paint()
          ..color = inactiveTrackColorTween.evaluate(enableAnimation)!;
    
        final Offset leftThumbOffset;
        final Offset rightThumbOffset;
        switch (textDirection) {
          case TextDirection.ltr:
            leftThumbOffset = startThumbCenter;
            rightThumbOffset = endThumbCenter;
            break;
          case TextDirection.rtl:
            leftThumbOffset = endThumbCenter;
            rightThumbOffset = startThumbCenter;
            break;
        }
        final Size thumbSize = sliderTheme.rangeThumbShape!
          .getPreferredSize(
            isEnabled,
            isDiscrete
          );
        final double thumbRadius = thumbSize.width / 2;
        assert(thumbRadius > 0);
    
        final Radius trackRadius = Radius.circular(trackRect.height / 2);
    
        context.canvas.drawRRect(
          RRect.fromLTRBAndCorners(
            trackRect.left,
            trackRect.top,
            leftThumbOffset.dx,
            trackRect.bottom,
            topLeft: trackRadius,
            bottomLeft: trackRadius,
          ),
          inactivePaint,
        );
        context.canvas.drawRect(
          Rect.fromLTRB(
            leftThumbOffset.dx,
            trackRect.top - (additionalActiveTrackHeight / 2),
            rightThumbOffset.dx,
            trackRect.bottom + (additionalActiveTrackHeight / 2),
          ),
          activePaint,
        );
        context.canvas.drawRRect(
          RRect.fromLTRBAndCorners(
            rightThumbOffset.dx,
            trackRect.top,
            trackRect.right,
            trackRect.bottom,
            topRight: trackRadius,
            bottomRight: trackRadius,
          ),
          inactivePaint,
        );
      }
    }
    

    Widget

    SliderTheme(
      data: SliderThemeData(
        rangeTrackShape: GradientRectRangeSliderTrackShape()
      ),
      child: RangeSlider(
        onChanged: onChanged,
        values: data.values,
        min: data.min,
        max: data.max,
        divisions: data.labels.length
      )
    )