Repository: flutter
Issue: Discrete `Slider` applies thumb padding when using custom Slider shapes
Labels: c: regression,framework,f: material design,has reproducible steps,P2,team-design,triaged-design,found in release: 3.27

### Steps to reproduce

Originally reported in https://github.com/rydmike/flex_color_picker/issues/90 by @rydmike 


Last year, I've updated the `Slider` widget to properly align the thumb shape with the tick marks, which has been last standing visual bug.  However, with the 3.27 release, Mike reported that his discrete `Slider`  with custom shapes cannot make the thumb reach extreme ends.



Such thumb padding is applied if the `Slider` discrete or thumb shape implementation overrides `isRounded` property.

```dart
    final double padding = isDiscrete || _sliderTheme.trackShape!.isRounded ? trackRect.height : 0.0;
```


Thumb padding is essential when the track shape rounded in the `Slider`(indicated by `isRounded`). 


However, we can remove `isDiscrete` when applying thumb padding as this doesn't need to apply to custom shapes by default.. 

Developers should've the flexibility to avoid such padding.  Which can they can do so with `isRounded` flag in the custom shape.
```dart
  @override
  bool get isRounded => true;
```





### Expected results


<img width="526" alt="Image" src="https://github.com/user-attachments/assets/08c9a9c9-1f91-41f2-91de-8698d7f6c39a" />

### Actual results

<img width="514" alt="Image" src="https://github.com/user-attachments/assets/29f5c55b-e44d-4259-b86e-e607134ba881" />


### Code sample

<details open><summary>Code sample</summary>

```dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'dart:math' as math;
import 'dart:ui' as ui;

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

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

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

class _MyAppState extends State<MyApp> {
  double _value = 125;
  ui.Image? _backgroundImage;

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

  Future<void> _loadImage() async {
    const imageProvider = NetworkImage('https://i.imgur.com/hSwyziG.png');
    final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
    final Completer<ui.Image> completer = Completer<ui.Image>();

    stream.addListener(ImageStreamListener((ImageInfo info, bool _) {
      completer.complete(info.image);
    }));

    final ui.Image image = await completer.future;
    setState(() {
      _backgroundImage = image;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_backgroundImage == null) {
      return const Center(child: CircularProgressIndicator());
    }

    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: SizedBox(
            width: 400,
            child: SliderTheme(
              data: SliderThemeData(
                trackHeight: 48,
                trackShape: OpacitySliderTrackShape(
                  color: Colors.red,
                  image: _backgroundImage!,
                ),
                thumbShape: const OpacitySliderThumbShape(color: Colors.yellow),
              ),
              child: Slider(
                value: _value,
                max: 255,
                divisions: 255,
                onChanged: (double value) {
                  setState(() {
                    _value = value;
                  });
                },
              ),
            ),
          ),
        ),
      ),
    );
  }
}

/// A custom slider track for the opacity slider.
///
/// Has rounded edges and a background image that repeats to show the common
/// image pattern used as background on images that has transparency. It
/// results in a nice effect where we can better judge visually how transparent
/// the current opacity value is directly on the slider.
class OpacitySliderTrackShape extends SliderTrackShape {
  /// Constructor for the opacity slider track.
  OpacitySliderTrackShape({
    required this.color,
    this.thumbRadius = 14,
    required this.image,
  }) : bgImagePaint = Paint()
          ..shader = ImageShader(
            image,
            TileMode.repeated,
            TileMode.repeated,
            Matrix4.identity().storage,
          );

  /// Currently selected color.
  final Color color;

  /// The radius of the adjustment thumb on the opacity slider track.
  ///
  /// Defaults to 14.
  final double thumbRadius;

  /// The image used a background image in the slider track.
  final ui.Image image;

  /// Paint used to draw the background image on the slider track.
  final Paint bgImagePaint;

  /// Returns a rect that represents the track bounds that fits within the
  /// [Slider].
  ///
  /// The width is the width of the [Slider] or [RangeSlider], but padded by
  /// the max  of the overlay and thumb radius. The height is defined by the
  /// [SliderThemeData.trackHeight].
  ///
  /// The [Rect] is centered both horizontally and vertically within the slider
  /// bounds.
  @override
  Rect getPreferredRect({
    required RenderBox parentBox,
    Offset offset = Offset.zero,
    required SliderThemeData sliderTheme,
    bool isEnabled = false,
    bool isDiscrete = false,
  }) {
    final double thumbWidth =
        sliderTheme.thumbShape!.getPreferredSize(isEnabled, isDiscrete).width;
    final double overlayWidth =
        sliderTheme.overlayShape!.getPreferredSize(isEnabled, isDiscrete).width;
    final double trackHeight = sliderTheme.trackHeight!;
    assert(overlayWidth >= 0, 'overlayWidth must be >= 0');
    assert(trackHeight >= 0, 'trackHeight must be >= 0');

    final double trackLeft =
        offset.dx + math.max(overlayWidth / 2, thumbWidth / 2);
    final double trackTop =
        offset.dy + (parentBox.size.height - trackHeight) / 2;
    final double trackRight =
        trackLeft + parentBox.size.width - math.max(thumbWidth, overlayWidth);
    final double trackBottom = trackTop + trackHeight;
    // If the parentBox size less than slider's size the trackRight will
    // be less than trackLeft, so switch them.
    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 TextDirection textDirection,
    required Offset thumbCenter,
    bool isDiscrete = false,
    bool isEnabled = false,
    double additionalActiveTrackHeight = 2,
    Offset? secondaryOffset,
  }) {
    assert(sliderTheme.disabledActiveTrackColor != null,
        'disabledActiveTrackColor cannot be null.');
    assert(sliderTheme.disabledInactiveTrackColor != null,
        'disabledInactiveTrackColor cannot be null.');
    assert(sliderTheme.activeTrackColor != null,
        'activeTrackColor cannot be null.');
    assert(sliderTheme.inactiveTrackColor != null,
        'inactiveTrackColor cannot be null.');
    assert(sliderTheme.thumbShape != null, 'thumbShape cannot be null.');

    // If we have no track height, no point in doing anything, no-op exit.
    if ((sliderTheme.trackHeight ?? 0) <= 0) {
      return;
    }

    final Rect trackRect = getPreferredRect(
      parentBox: parentBox,
      offset: offset,
      sliderTheme: sliderTheme,
      isEnabled: isEnabled,
      isDiscrete: isDiscrete,
    );
    final Radius trackRadius = Radius.circular(trackRect.height / 2);
    final Radius activeTrackRadius = Radius.circular(trackRect.height / 2 + 1);

    final Paint activePaint = Paint()..color = Colors.transparent;
    final Paint inactivePaint = Paint()
      ..shader = ui.Gradient.linear(
          Offset.zero,
          Offset(trackRect.width, 0),
          <Color>[color.withOpacity(0), color.withOpacity(1)],
          <double>[0.05, 0.95]);

    Paint leftTrackPaint;
    Paint rightTrackPaint;
    switch (textDirection) {
      case TextDirection.ltr:
        leftTrackPaint = activePaint;
        rightTrackPaint = inactivePaint;
      case TextDirection.rtl:
        leftTrackPaint = inactivePaint;
        rightTrackPaint = activePaint;
    }

    final RRect shapeRect = ui.RRect.fromLTRBAndCorners(
      trackRect.left - thumbRadius,
      (textDirection == TextDirection.ltr)
          ? trackRect.top - (additionalActiveTrackHeight / 2)
          : trackRect.top,
      trackRect.right + thumbRadius,
      (textDirection == TextDirection.ltr)
          ? trackRect.bottom + (additionalActiveTrackHeight / 2)
          : trackRect.bottom,
      topLeft: (textDirection == TextDirection.ltr)
          ? activeTrackRadius
          : trackRadius,
      bottomLeft: (textDirection == TextDirection.ltr)
          ? activeTrackRadius
          : trackRadius,
      topRight: (textDirection == TextDirection.ltr)
          ? activeTrackRadius
          : trackRadius,
      bottomRight: (textDirection == TextDirection.ltr)
          ? activeTrackRadius
          : trackRadius,
    );

    context.canvas.drawRRect(shapeRect, leftTrackPaint);
    context.canvas.drawRRect(shapeRect, bgImagePaint);
    context.canvas.drawRRect(shapeRect, rightTrackPaint);
  }
}

class OpacitySliderThumbShape extends RoundSliderThumbShape {
  /// Create a slider thumb that draws a circle filled with [color]
  /// and shows the slider `value` * 100 in the thumb.
  const OpacitySliderThumbShape({
    required this.color,
    super.enabledThumbRadius = 16.0,
    super.disabledThumbRadius,
    super.elevation,
    super.pressedElevation = 4.0,
  });

  /// Color used to fill the inside of the thumb.
  final Color color;

  double get _disabledThumbRadius => disabledThumbRadius ?? enabledThumbRadius;

  @override
  void paint(
    PaintingContext context,
    Offset center, {
    required Animation<double> activationAnimation,
    required Animation<double> enableAnimation,
    required bool isDiscrete,
    required TextPainter labelPainter,
    required RenderBox parentBox,
    required SliderThemeData sliderTheme,
    required TextDirection textDirection,
    required double value,
    required double textScaleFactor,
    required Size sizeWithOverflow,
  }) {
    assert(sliderTheme.disabledThumbColor != null,
        'disabledThumbColor cannot be null');
    assert(sliderTheme.thumbColor != null, 'thumbColor cannot be null');

    final Canvas canvas = context.canvas;

    final Tween<double> radiusTween = Tween<double>(
      begin: _disabledThumbRadius,
      end: enabledThumbRadius,
    );

    final double radius = radiusTween.evaluate(enableAnimation);
    final Path path = Path()
      ..addArc(
        Rect.fromCenter(
          center: center,
          width: 2 * radius,
          height: 2 * radius,
        ),
        0,
        math.pi * 2,
      );

    canvas.drawShadow(path, Colors.black, 1.5, true);
    canvas.drawCircle(center, radius, Paint()..color = Colors.white);
    canvas.drawCircle(center, radius - 1.8, Paint()..color = color);

    final TextSpan span = TextSpan(
      style: TextStyle(
        fontSize: enabledThumbRadius * 0.78,
        fontWeight: FontWeight.w600,
        color: sliderTheme.thumbColor,
      ),
      text: (value * 100).toStringAsFixed(0),
    );

    final TextPainter textPainter = TextPainter(
      text: span,
      textAlign: TextAlign.center,
      textDirection: TextDirection.ltr,
    );
    textPainter.layout();

    final Offset textCenter = Offset(
      center.dx - (textPainter.width / 2),
      center.dy - (textPainter.height / 2),
    );
    textPainter.paint(canvas, textCenter);
  }
}

```

</details>


### Screenshots or Video

<details open>
<summary>Screenshots / Video demonstration</summary>

[Upload media here]

</details>


### Logs

<details open><summary>Logs</summary>

```console
[Paste your logs here]
```

</details>


### Flutter Doctor output

<details open><summary>Doctor output</summary>

```console
3.27.x
```

</details>