-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Description
Steps to reproduce
Originally reported in rydmike/flex_color_picker#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.
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.
@override
bool get isRounded => true;Expected results
Actual results
Code sample
Code sample
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);
}
}
Screenshots or Video
Screenshots / Video demonstration
[Upload media here]
Logs
Logs
[Paste your logs here]Flutter Doctor output
Doctor output
3.27.xMetadata
Metadata
Assignees
Labels
Type
Projects
Status