flutterdartuisliderflutter-custompainter

How can I implement a custom Flutter slider that gradually increases it's track thickness?


Context

I'm working on a custom slider widget in Flutter where I need the slider track to change its thickness slightly in an increasing way. I have not found any built-in properties in the Slider widget that could help achieve this effect. The thickness should increase as it gets closer to the end and decrease as it moves towards the start.

What I tried

I tried to create a custom SliderTheme and change the trackShape, here's the relevant code:

class CustomSliderTrackShape extends RoundedRectSliderTrackShape {
  final double overlayWidth;

  const CustomSliderTrackShape({
    this.overlayWidth = 30.0,
  });

  @override
  Rect getPreferredRect({
    required RenderBox parentBox,
    Offset offset = Offset.zero,
    required SliderThemeData sliderTheme,
    bool isEnabled = false,
    bool isDiscrete = false,
  }) {
    final double overlayRadius = overlayWidth / 2;

    final double trackLeft = offset.dx + overlayRadius;
    final double trackTop = offset.dy + (parentBox.size.height / 2);
    final double trackHeight = sliderTheme.trackHeight!;

    // --> not sure how to increase the width of the track

    final double trackWidth = parentBox.size.width - (2 * overlayRadius) + 10;

    return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
  }
}

And it's usage:

SliderTheme(
      data: SliderTheme.of(context).copyWith(
        // --> Using it here.
        trackShape: CustomSliderTrackShape(),
      ),
      child: Slider(
        value: 0.4,
        onChanged: (newValue) {},
      ),
    )

which results in (seems like the regular Slider):

enter image description here

However, the desired output is:

enter image description here

Question

How can I modify the getPreferredRect function to create a custom slider that increases its height/thickness as it comes to the end? should I be using CustomPaint instead in order to achieve a slider with varying track thickness?


Here's the code as a fully runnable snippet:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Custom Slider Example')),
        body: MySlider(),
      ),
    );
  }
}

class MySlider extends StatefulWidget {
  @override
  _MySliderState createState() => _MySliderState();
}

class _MySliderState extends State<MySlider> {
  double _value = 50;

  @override
  Widget build(BuildContext context) {
    return SliderTheme(
      data: SliderTheme.of(context).copyWith(
        trackHeight: 6,
        
        trackShape: CustomSliderTrackShape(),
      ),
      child: Slider(
        min: 0,
        max: 100,
        divisions: 100,
        value: _value,
        onChanged: (value) {
          setState(() {
            _value = value;
          });
        },
      ),
    );
  }
}

class CustomSliderTrackShape extends RoundedRectSliderTrackShape {
  final double overlayWidth;

  const CustomSliderTrackShape({
    this.overlayWidth = 30.0,
  });

  @override
  Rect getPreferredRect({
    required RenderBox parentBox,
    Offset offset = Offset.zero,
    required SliderThemeData sliderTheme,
    bool isEnabled = false,
    bool isDiscrete = false,
  }) {
    final double overlayRadius = overlayWidth / 2;

    final double trackLeft = offset.dx + overlayRadius;
    final double trackTop = offset.dy + (parentBox.size.height / 2);
    final double trackHeight = sliderTheme.trackHeight!;

    // --> not sure how to increase the width of the track

    final double trackWidth = parentBox.size.width - (2 * overlayRadius) + 10;

    return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
  }
}

Solution

  • You can play with Paint, hope this help. Also try to find better ratio for your case,

    DartPad

    class CustomSliderTrackShape extends RoundedRectSliderTrackShape {
      @override
      void paint(
        PaintingContext context,
        Offset offset, {
        required RenderBox parentBox,
        required SliderThemeData sliderTheme,
        required Animation<double> enableAnimation,
        required TextDirection textDirection,
        required Offset thumbCenter,
        Offset? secondaryOffset,
        bool isDiscrete = false,
        bool isEnabled = false,
        double additionalActiveTrackHeight = 2,
      }) {
        final Rect trackRect = getPreferredRect(
          parentBox: parentBox,
          offset: offset,
          sliderTheme: sliderTheme,
          isEnabled: isEnabled,
          isDiscrete: isDiscrete,
        );
    
        final radius = trackRect.height * .3; //todo: use your trackHeightRatio
        final thumbWidth =
            sliderTheme.thumbShape!.getPreferredSize(isEnabled, isDiscrete).width;
    
        final Path path = Path()
          ..moveTo(
              radius + trackRect.left, trackRect.top + trackRect.height / 2) //lt
          ..lineTo(trackRect.width + thumbWidth, trackRect.top) //rt
          ..arcToPoint(
            //rb
            Offset(trackRect.width + thumbWidth, trackRect.bottom),
            radius: Radius.circular(radius),
          )
          ..lineTo(trackRect.left, trackRect.bottom - radius) //lb
          ..arcToPoint(
            //lt
            Offset(trackRect.left, trackRect.top + trackRect.height / 2),
            radius: Radius.circular(radius),
          )
          ..close();
    
        final Paint trackPaint = Paint()..color = Colors.grey.shade300;
        context.canvas.drawPath(path, trackPaint);
      }
    }
    
    

    enter image description here