How to set custom tickMarkShape in flutter slider widget - flutter

I am trying to make custom tickMarkShape in my flutter project. From docs, I read that it is possible to control tick mark shape with SliderTickMarkShape. But it gives me only one option SliderTickMarkShape.noTickMark which removes the tick mark shape, but I would like to make it like a stick. So how can I set custom tick mark shape in flutter?. Thanks in advance.

In order to customize the tick as the following image:
You need to extend SliderTickMarkShape. In this case I called LineSliderTickMarkShape. Below is the source code but it is basically a copy of RoundSliderTickMarkShape, it just changes the context.canvas.drawLine line to customize the shape of the mark. RoundSliderTickMarkShape is the default one.
class LineSliderTickMarkShape extends SliderTickMarkShape {
const LineSliderTickMarkShape({
this.tickMarkRadius,
});
final double? tickMarkRadius;
#override
Size getPreferredSize({
required SliderThemeData sliderTheme,
required bool isEnabled,
}) {
assert(sliderTheme != null);
assert(sliderTheme.trackHeight != null);
assert(isEnabled != null);
return Size.fromRadius(tickMarkRadius ?? sliderTheme.trackHeight! / 4);
}
#override
void paint(
PaintingContext context,
Offset center, {
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required Animation<double> enableAnimation,
required TextDirection textDirection,
required Offset thumbCenter,
required bool isEnabled,
}) {
Color? begin;
Color? end;
switch (textDirection) {
case TextDirection.ltr:
final bool isTickMarkRightOfThumb = center.dx > thumbCenter.dx;
begin = isTickMarkRightOfThumb ? sliderTheme.disabledInactiveTickMarkColor : sliderTheme.disabledActiveTickMarkColor;
end = isTickMarkRightOfThumb ? sliderTheme.inactiveTickMarkColor : sliderTheme.activeTickMarkColor;
break;
case TextDirection.rtl:
final bool isTickMarkLeftOfThumb = center.dx < thumbCenter.dx;
begin = isTickMarkLeftOfThumb ? sliderTheme.disabledInactiveTickMarkColor : sliderTheme.disabledActiveTickMarkColor;
end = isTickMarkLeftOfThumb ? sliderTheme.inactiveTickMarkColor : sliderTheme.activeTickMarkColor;
break;
}
final Paint paint = Paint()..color = ColorTween(begin: begin, end: end).evaluate(enableAnimation)!;
final double tickMarkRadius = getPreferredSize(
isEnabled: isEnabled,
sliderTheme: sliderTheme,
).width / 2;
if (tickMarkRadius > 0) {
context.canvas.drawLine(Offset(center.dx - 5, center.dy - 5), Offset(center.dx + 5, center.dy + 5), paint);
}
}
}
Once LineSliderTickMarkShape is defined, you can use it to customize your Slider like this:
#override
Widget build(BuildContext context) {
return SliderTheme(
data: SliderTheme.of(context).copyWith(
trackHeight: 20,
tickMarkShape: const LineSliderTickMarkShape()
),
child: Slider(
...

Related

How to display track mark at 0th position in flutter Slider?

In my application I have a slider going from -40 to 100.
Since the 0th position is in an odd position I want to display a track mark at 0th position only.
I have modified to display a custom track mark with the following code. But I can't seems to find a way to display track mark on 0th position and hide other marks.
Can anyone provide some idea how to do this ?
import 'package:flutter/material.dart';
class LineSliderTickMarkShape extends SliderTickMarkShape {
const LineSliderTickMarkShape({
this.tickMarkRadius,
});
final double tickMarkRadius;
#override
Size getPreferredSize({
#required SliderThemeData sliderTheme,
#required bool isEnabled,
}) {
return Size.fromRadius(sliderTheme.trackHeight / 2);
}
#override
void paint(
PaintingContext context,
Offset center, {
#required RenderBox parentBox,
#required SliderThemeData sliderTheme,
#required Animation<double> enableAnimation,
#required TextDirection textDirection,
#required Offset thumbCenter,
#required bool isEnabled,
}) {
final Paint paint = Paint()
..color = sliderTheme.thumbColor
..strokeWidth = sliderTheme.trackHeight;
context.canvas.drawLine(
Offset(center.dx, center.dy - sliderTheme.trackHeight * 4),
Offset(center.dx, center.dy + sliderTheme.trackHeight * 4),
paint);
}
}

Flutter - Custom slider value indicator. How to get the slider's center position

I am trying to implement a custom progress indicator for flutter slider. As for now, I have the given code that draws the indicator.
class InsideSliderValueIndicator extends SliderComponentShape {
#override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return Size.zero;
}
#override
void paint(
PaintingContext context,
Offset center, {
Animation<double> activationAnimation,
Animation<double> enableAnimation,
bool isDiscrete,
TextPainter labelPainter,
RenderBox parentBox,
SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
}) {
final sliderWidth = parentBox.size.width;
final sliderBoxOffset = parentBox.localToGlobal(Offset.zero);
labelPainter.paint(
context.canvas,
Offset(sliderBoxOffset.dx + sliderWidth / 2 - labelPainter.width / 2,
center.dy - labelPainter.height / 2));
}
}
I have also customized the slider to remove the thumb. Here's what it looks like:
As you can see, the text is not centered in the slider.
The problem with this code is that sliderBoxOffset doesn't return the box of the "slider", but of the parent card, and center is actually the position of your thumb on the slider relative to the parent.
So my question is: How can I get the center of the slider from the given parameters ?
Thank you for your help
I found a weird solution that works for my use-case
final sliderWidth = parentBox.size.width;
final halfSliderWidth = sliderWidth / 2;
final translateX = value < 0.5 ? halfSliderWidth - (value * sliderWidth) : halfSliderWidth - (value * sliderWidth);
labelPainter.paint(context.canvas, center.translate(translateX - labelPainter.width / 2, -labelPainter.height / 2));
I'm however pretty sure this won't work if the slider is not centered in its parent. I'm still wondering if there is a less dirty way to do this.

Slider with continuous track which snaps to a specific value

I want to create the custom Slider from the deign below with a continuous track which would snap to a specific value which is shown below as a black rectangle.
I was able to recreate the custom Slider by setting defining custom trackShape and thumbShape.
I'm not sure how to snap the thumb when it's very close to the small black rectangle and how to make the black rectangle clickable.
The behavior is very similar to the Slider with discrete divisions but it should be continuous and the small black rectangle must be clickable.
I ended up implementing it by myself with some inspiration from https://github.com/tomwyr/step-slider:
class SnapSlider extends StatefulWidget {
SnapSlider({
Key key,
this.sliderKey,
this.snapValues = const {},
this.value,
this.onSnapValueChanged,
this.snapDistance = 0.05,
this.animCurve: Curves.fastOutSlowIn,
this.animDuration: const Duration(milliseconds: 350),
this.min: 0.0,
this.max: 1.0,
this.label,
this.divisions,
this.onChanged,
this.onChangeEnd,
this.onChangeStart,
this.activeColor,
this.inactiveColor,
this.semanticFormatterCallback,
}) : assert(snapValues != null),
assert(snapValues.every((it) => it >= min && it <= max),
'Each snap value needs to be within slider values range.'),
super(key: key);
final Key sliderKey;
final Set<double> snapValues;
final double value;
final ValueChanged<double> onSnapValueChanged;
final double snapDistance;
final Curve animCurve;
final Duration animDuration;
final double min;
final double max;
final String label;
final int divisions;
final Color activeColor;
final Color inactiveColor;
final ValueChanged<double> onChanged;
final ValueChanged<double> onChangeEnd;
final ValueChanged<double> onChangeStart;
final SemanticFormatterCallback semanticFormatterCallback;
#override
_StepSliderState createState() => _StepSliderState();
}
class _StepSliderState extends State<SnapSlider>
with SingleTickerProviderStateMixin {
AnimationController _animator;
CurvedAnimation _baseAnim;
Animation<double> _animation;
double _lastSnapValue;
#override
void didUpdateWidget(SnapSlider oldWidget) {
super.didUpdateWidget(oldWidget);
_animator.duration = widget.animDuration;
_baseAnim.curve = widget.animCurve;
}
#override
void initState() {
super.initState();
_animator = AnimationController(
vsync: this, duration: widget.animDuration, value: 1.0);
_baseAnim = CurvedAnimation(parent: _animator, curve: widget.animCurve);
_recreateAnimation(widget.value, widget.value);
_animation.addListener(() {
_onSliderChanged(_animation.value);
widget.onChanged?.call(_animation.value);
});
}
#override
void dispose() {
_animator.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animator,
builder: (_, __) => Slider(
key: widget.sliderKey,
min: widget.min,
max: widget.max,
label: widget.label,
divisions: widget.divisions,
activeColor: widget.activeColor,
inactiveColor: widget.inactiveColor,
semanticFormatterCallback: widget.semanticFormatterCallback,
value: widget.value,
onChangeStart: (it) {
_animator.stop();
_onSliderChangeStart(it);
widget.onChangeStart?.call(it);
},
onChangeEnd: (it) {
_onSliderChangeEnd(it);
widget.onChangeEnd?.call(it);
},
onChanged: (it) {
_onSliderChanged(it);
widget.onChanged?.call(it);
},
),
);
}
void _onSliderChangeStart(double value) {
}
void _onSliderChangeEnd(double value) {
double snapValue = _closestSnapValue(value);
var distance = (value - snapValue).abs();
if (snapValue != _lastSnapValue) {
if (distance <= widget.snapDistance) {
_animateTo(widget.value, snapValue, true);
widget.onSnapValueChanged?.call(widget.value);
_lastSnapValue = snapValue;
}
} else {
if (distance > widget.snapDistance) {
_lastSnapValue = null;
}
}
}
double _closestSnapValue(double value) {
return widget.snapValues.reduce((a, b) {
var distanceA = (value - a).abs();
var distanceB = (value - b).abs();
return distanceA < distanceB ? a : b;
});
}
void _onSliderChanged(double value) {
}
void _animateTo(double start, double end, bool restart) {
_recreateAnimation(start, end);
_animator.forward(from: 0.0);
}
void _recreateAnimation(double start, double end) {
_animation = Tween(begin: start ?? end, end: end).animate(_baseAnim);
}
}

How can I round the corners of a slider in Flutter

I'm using the basic slider, and I found how to update just the parts of the slider theme data that I want to change like trackHeight, but unfortunately I'm not sure how to update the field for "trackShape". Here is what I do in the main app to update track height for example:
final SliderThemeData tweakedSliderTheme = Theme.of(context).sliderTheme.copyWith(
trackHeight: 22.0,
//trackShape: RectangularSliderTrackShape(), // How do I update this??
);
I did try using ClipRRect() around the slider widget and that had no effect.
Here is a simple page for one slider:
import 'package:flutter/material.dart';
class RoomControl extends StatefulWidget {
#override
_RoomControlState createState() => _RoomControlState();
}
class _RoomControlState extends State<RoomControl> {
double _value = 0.0;
void _setvalue(double value) => setState(() => _value = value);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Name here'),
),
//hit Ctrl+space in intellij to know what are the options you can use in flutter widgets
body: Container(
padding: EdgeInsets.all(32.0),
child: Center(
child: Column(
children: <Widget>[
Text('Value: ${(_value * 100).round()}'),
ClipRRect(
borderRadius: BorderRadius.circular(5.0),
child:Slider(
value: _value,
onChanged: _setvalue,
divisions: 10,
)
)
],
),
),
),
);
}
}
Here is what that slider looks like:
UPDATE:
After getting the answer, I was able to create something like this easily by updating the tick mark shape, and the thumb shape:
final SliderThemeData tweakedSliderTheme = Theme.of(context).sliderTheme.copyWith(
trackHeight: 20.0,
thumbShape: MyRoundSliderThumbShape(enabledThumbRadius: 13.0, disabledThumbRadius: 13.0),
trackShape: MyRoundSliderTrackShape(), // TODO: this is hard coded right now for 20 track height
inactiveTrackColor: lightGreySliderColor,
activeTickMarkColor: Color(blackPrimaryValue),
inactiveTickMarkColor: colorizedMenuColor,
tickMarkShape: MyRectSliderTickMarkShape(tickMarkRadius: 4.0),
);
There was a bit of a trick to the tick mark shape. If you make it too big it just skips painting it! Probably makes sense but I didn't know much about paint/rendering so it took me a while to learn how to get the tick marks (Rect) to show up properly
I've copied the base code from RectangularSliderTrackShape into a new class called RoundSliderTrackShape.
round_slider_track_shape.dart
import 'dart:math';
import 'package:flutter/material.dart';
class RoundSliderTrackShape extends SliderTrackShape {
/// Create a slider track that draws 2 rectangles.
const RoundSliderTrackShape({ this.disabledThumbGapWidth = 2.0 });
/// Horizontal spacing, or gap, between the disabled thumb and the track.
///
/// This is only used when the slider is disabled. There is no gap around
/// the thumb and any part of the track when the slider is enabled. The
/// Material spec defaults this gap width 2, which is half of the disabled
/// thumb radius.
final double disabledThumbGapWidth;
#override
Rect getPreferredRect({
RenderBox parentBox,
Offset offset = Offset.zero,
SliderThemeData sliderTheme,
bool isEnabled,
bool isDiscrete,
}) {
final double overlayWidth = sliderTheme.overlayShape.getPreferredSize(isEnabled, isDiscrete).width;
final double trackHeight = sliderTheme.trackHeight;
assert(overlayWidth >= 0);
assert(trackHeight >= 0);
assert(parentBox.size.width >= overlayWidth);
assert(parentBox.size.height >= trackHeight);
final double trackLeft = offset.dx + overlayWidth / 2;
final double trackTop = offset.dy + (parentBox.size.height - trackHeight) / 2;
// TODO(clocksmith): Although this works for a material, perhaps the default
// rectangular track should be padded not just by the overlay, but by the
// max of the thumb and the overlay, in case there is no overlay.
final double trackWidth = parentBox.size.width - overlayWidth;
return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
}
#override
void paint(
PaintingContext context,
Offset offset, {
RenderBox parentBox,
SliderThemeData sliderTheme,
Animation<double> enableAnimation,
TextDirection textDirection,
Offset thumbCenter,
bool isDiscrete,
bool isEnabled,
}) {
// If the slider track height is 0, then it makes no difference whether the
// track is painted or not, therefore the painting can be a no-op.
if (sliderTheme.trackHeight == 0) {
return;
}
// Assign the track segment paints, which are left: active, right: inactive,
// but reversed for right to left text.
final ColorTween activeTrackColorTween = ColorTween(begin: sliderTheme.disabledActiveTrackColor , end: sliderTheme.activeTrackColor);
final ColorTween inactiveTrackColorTween = ColorTween(begin: sliderTheme.disabledInactiveTrackColor , end: sliderTheme.inactiveTrackColor);
final Paint activePaint = Paint()..color = activeTrackColorTween.evaluate(enableAnimation);
final Paint inactivePaint = Paint()..color = inactiveTrackColorTween.evaluate(enableAnimation);
Paint leftTrackPaint;
Paint rightTrackPaint;
switch (textDirection) {
case TextDirection.ltr:
leftTrackPaint = activePaint;
rightTrackPaint = inactivePaint;
break;
case TextDirection.rtl:
leftTrackPaint = inactivePaint;
rightTrackPaint = activePaint;
break;
}
// Used to create a gap around the thumb iff the slider is disabled.
// If the slider is enabled, the track can be drawn beneath the thumb
// without a gap. But when the slider is disabled, the track is shortened
// and this gap helps determine how much shorter it should be.
// TODO(clocksmith): The new Material spec has a gray circle in place of this gap.
double horizontalAdjustment = 0.0;
if (!isEnabled) {
final double disabledThumbRadius = sliderTheme.thumbShape.getPreferredSize(false, isDiscrete).width / 2.0;
final double gap = disabledThumbGapWidth * (1.0 - enableAnimation.value);
horizontalAdjustment = disabledThumbRadius + gap;
}
final Rect trackRect = getPreferredRect(
parentBox: parentBox,
offset: offset,
sliderTheme: sliderTheme,
isEnabled: isEnabled,
isDiscrete: isDiscrete,
);
final Rect leftTrackSegment = Rect.fromLTRB(trackRect.left, trackRect.top, thumbCenter.dx - horizontalAdjustment, trackRect.bottom);
// Left Arc
context.canvas.drawArc(
Rect.fromCircle(center: Offset(trackRect.left, trackRect.top + 11.0), radius: 11.0),
-pi * 3 / 2, // -270 degrees
pi, // 180 degrees
false,
trackRect.left - thumbCenter.dx == 0.0 ? rightTrackPaint : leftTrackPaint
);
// Right Arc
context.canvas.drawArc(
Rect.fromCircle(center: Offset(trackRect.right, trackRect.top + 11.0), radius: 11.0),
-pi / 2, // -90 degrees
pi, // 180 degrees
false,
trackRect.right - thumbCenter.dx == 0.0 ? leftTrackPaint : rightTrackPaint
);
context.canvas.drawRect(leftTrackSegment, leftTrackPaint);
final Rect rightTrackSegment = Rect.fromLTRB(thumbCenter.dx + horizontalAdjustment, trackRect.top, trackRect.right, trackRect.bottom);
context.canvas.drawRect(rightTrackSegment, rightTrackPaint);
}
}
The following is how the SliderTheme is setup.
import 'package:flutter_stackoverflow/round_slider_track_shape.dart';
...
sliderTheme: Theme.of(context).sliderTheme.copyWith(
trackHeight: 22.0,
trackShape: RoundSliderTrackShape(),
activeTrackColor: Colors.green,
// trackShape: RectangularSliderTrackShape(),
),
What was added was two circle arcs to the side of the SliderTrack
// Left Arc
context.canvas.drawArc(
Rect.fromCircle(center: Offset(trackRect.left, trackRect.top + 11.0), radius: 11.0),
-pi * 3 / 2, // -270 degrees
pi, // 180 degrees
false,
trackRect.left - thumbCenter.dx == 0.0 ? rightTrackPaint : leftTrackPaint
);
// Right Arc
context.canvas.drawArc(
Rect.fromCircle(center: Offset(trackRect.right, trackRect.top + 11.0), radius: 11.0),
-pi / 2, // -90 degrees
pi, // 180 degrees
false,
trackRect.right - thumbCenter.dx == 0.0 ? leftTrackPaint : rightTrackPaint
);
Answer by #Jun Xiang seems to work but I did a minor change in the following code:
// Left Arc
context.canvas.drawArc(
Rect.fromCircle(center: Offset(trackRect.left, trackRect.top + 11.0), radius: 11.0),
-pi * 3 / 2, // -270 degrees
pi, // 180 degrees
false,
trackRect.left - thumbCenter.dx == 0.0 ? rightTrackPaint : leftTrackPaint
);
// Right Arc
context.canvas.drawArc(
Rect.fromCircle(center: Offset(trackRect.right, trackRect.top + 11.0), radius: 11.0),
-pi / 2, // -90 degrees
pi, // 180 degrees
false,
trackRect.right - thumbCenter.dx == 0.0 ? leftTrackPaint : rightTrackPaint
);
Instead of using 11.0 I used sliderTheme.trackHeight * 1/2 and seems to work for all track heights entered (not only 22).
PS: I couldn't comment so I posted an answer.
I have a better solution
import 'package:flutter/material.dart';
class RoundSliderTrackShape extends SliderTrackShape {
const RoundSliderTrackShape({this.disabledThumbGapWidth = 2.0, this.radius = 0});
final double disabledThumbGapWidth;
final double radius;
#override
Rect getPreferredRect({
RenderBox parentBox,
Offset offset = Offset.zero,
SliderThemeData sliderTheme,
bool isEnabled,
bool isDiscrete,
}) {
final double overlayWidth = sliderTheme.overlayShape.getPreferredSize(isEnabled, isDiscrete).width;
final double trackHeight = sliderTheme.trackHeight;
assert(overlayWidth >= 0);
assert(trackHeight >= 0);
assert(parentBox.size.width >= overlayWidth);
assert(parentBox.size.height >= trackHeight);
final double trackLeft = offset.dx + overlayWidth / 2;
final double trackTop = offset.dy + (parentBox.size.height - trackHeight) / 2;
final double trackWidth = parentBox.size.width - overlayWidth;
return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
}
#override
void paint(
PaintingContext context,
Offset offset, {
RenderBox parentBox,
SliderThemeData sliderTheme,
Animation<double> enableAnimation,
TextDirection textDirection,
Offset thumbCenter,
bool isDiscrete,
bool isEnabled,
}) {
if (sliderTheme.trackHeight == 0) {
return;
}
final ColorTween activeTrackColorTween =
ColorTween(begin: sliderTheme.disabledActiveTrackColor, end: sliderTheme.activeTrackColor);
final ColorTween inactiveTrackColorTween =
ColorTween(begin: sliderTheme.disabledInactiveTrackColor, end: sliderTheme.inactiveTrackColor);
final Paint activePaint = Paint()..color = activeTrackColorTween.evaluate(enableAnimation);
final Paint inactivePaint = Paint()..color = inactiveTrackColorTween.evaluate(enableAnimation);
Paint leftTrackPaint;
Paint rightTrackPaint;
switch (textDirection) {
case TextDirection.ltr:
leftTrackPaint = activePaint;
rightTrackPaint = inactivePaint;
break;
case TextDirection.rtl:
leftTrackPaint = inactivePaint;
rightTrackPaint = activePaint;
break;
}
double horizontalAdjustment = 0.0;
if (!isEnabled) {
final double disabledThumbRadius =
sliderTheme.thumbShape.getPreferredSize(false, isDiscrete).width / 2.0;
final double gap = disabledThumbGapWidth * (1.0 - enableAnimation.value);
horizontalAdjustment = disabledThumbRadius + gap;
}
final Rect trackRect = getPreferredRect(
parentBox: parentBox,
offset: offset,
sliderTheme: sliderTheme,
isEnabled: isEnabled,
isDiscrete: isDiscrete,
);
//Modify this side
final RRect leftTrackSegment = RRect.fromLTRBR(trackRect.left, trackRect.top,
thumbCenter.dx - horizontalAdjustment, trackRect.bottom, Radius.circular(radius));
context.canvas.drawRRect(leftTrackSegment, leftTrackPaint);
final RRect rightTrackSegment = RRect.fromLTRBR(thumbCenter.dx + horizontalAdjustment, trackRect.top,
trackRect.right, trackRect.bottom, Radius.circular(radius));
context.canvas.drawRRect(rightTrackSegment, rightTrackPaint);
}
}
use:
trackShape: RoundSliderTrackShape(radius: 8)
The default trackShape for slider is now a RoundedRectSliderTrackShape in master branch:
https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/material/slider_theme.dart#L462
Does that work for you?
RectangularSliderTrackShape can be preserved by overriding the trackShape in the SliderThemeData.
Create custom shape like this, In this i am drawing 2 circles on thumb
class SliderThumbShape extends SliderComponentShape {
/// Create a slider thumb that draws a circle.
const SliderThumbShape({
this.enabledThumbRadius = 10.0,
this.disabledThumbRadius,
this.elevation = 1.0,
this.pressedElevation = 6.0,
});
/// The preferred radius of the round thumb shape when the slider is enabled.
///
/// If it is not provided, then the material default of 10 is used.
final double enabledThumbRadius;
/// The preferred radius of the round thumb shape when the slider is disabled.
///
/// If no disabledRadius is provided, then it is equal to the
/// [enabledThumbRadius]
final double disabledThumbRadius;
double get _disabledThumbRadius => disabledThumbRadius ?? enabledThumbRadius;
/// The resting elevation adds shadow to the unpressed thumb.
///
/// The default is 1.
///
/// Use 0 for no shadow. The higher the value, the larger the shadow. For
/// example, a value of 12 will create a very large shadow.
///
final double elevation;
/// The pressed elevation adds shadow to the pressed thumb.
///
/// The default is 6.
///
/// Use 0 for no shadow. The higher the value, the larger the shadow. For
/// example, a value of 12 will create a very large shadow.
final double pressedElevation;
#override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return Size.fromRadius(isEnabled == true ? enabledThumbRadius : _disabledThumbRadius);
}
#override
void paint(
PaintingContext context,
Offset center, {
Animation<double> activationAnimation,
#required Animation<double> enableAnimation,
bool isDiscrete,
TextPainter labelPainter,
RenderBox parentBox,
#required SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
double textScaleFactor,
Size sizeWithOverflow,
}) {
assert(context != null);
assert(center != null);
assert(enableAnimation != null);
assert(sliderTheme != null);
assert(sliderTheme.disabledThumbColor != null);
assert(sliderTheme.thumbColor != null);
assert(!sizeWithOverflow.isEmpty);
final Canvas canvas = context.canvas;
final Tween<double> radiusTween = Tween<double>(
begin: _disabledThumbRadius,
end: enabledThumbRadius,
);
final double radius = radiusTween.evaluate(enableAnimation);
final Tween<double> elevationTween = Tween<double>(
begin: elevation,
end: pressedElevation,
);
final double evaluatedElevation = elevationTween.evaluate(activationAnimation);
{
final Path path = Path()
..addArc(Rect.fromCenter(center: center, width: 1 * radius, height: 1 * radius), 0, math.pi * 2);
Paint paint = Paint()..color = Colors.black;
paint.strokeWidth = 15;
paint.style = PaintingStyle.stroke;
canvas.drawCircle(
center,
radius,
paint,
);
{
Paint paint = Paint()..color = Colors.white;
paint.style = PaintingStyle.fill;
canvas.drawCircle(
center,
radius,
paint,
);
}
}
}
}
Then use it in your widget tree like this
SliderTheme(
data: SliderThemeData(
activeTrackColor: Colors.blue,
inactiveTrackColor: Color(0xffd0d2d3),
trackHeight: 2,
thumbShape: SliderThumbShape(),
),
child: Slider(
onChanged: (value) {},
value: 40.5,
max: 100,
min: 0,
),
),
The other option is to draw a thick line with a round StrokeCap. The cap radius at the two ends is in addition to the line width.
#override
void paint(PaintingContext context, Offset offset,
{required RenderBox parentBox,
required SliderThemeData sliderTheme,
required Animation<double> enableAnimation,
required Offset thumbCenter,
bool isEnabled = true,
bool isDiscrete = true,
required TextDirection textDirection}) {
final canvas = context.canvas;
final width = 200;
final height = 16;
canvas.drawLine(
Offset(0, height / 2),
Offset(width, height / 2),
Paint()
..strokeCap = StrokeCap.round
..strokeWidth = height);
}

Changing speed of InkWell

I am having trouble replicating a normal settings menu in Flutter. I am using an InkWell to try to create the splash effect that normally occurs when you tap on a settings option. The problem is that the splash effect appears way too fast compared to how it normally is. Basically, I just want to slow down the InkWell.
If you would like a slower ripple effect, then you have to change splashFactory property in your MaterialApp theme from InkSplash.splashFactory (default) to InkRipple.splashFactory. InkRipple's splash looks more like native.
It's possible to create what you wanted but it requires a custom splashFactory under InkWell class.
As you see in the variables below, these are meant to be private values and they are not open to modification within classes.
const Duration _kUnconfirmedSplashDuration = const Duration(seconds: 1);
const Duration _kSplashFadeDuration = const Duration(milliseconds: 200);
const double _kSplashInitialSize = 0.0; // logical pixels
const double _kSplashConfirmedVelocity = 1.0;
To answer your question, yes you can do it. I just copied and pasted everything from the source code and change the animation values. After the code below just use it in splashFactory.
///Part to use within application
new InkWell(
onTap: () {},
splashFactory: CustomSplashFactory(),
child: Container(
padding: EdgeInsets.all(12.0),
child: Text('Flat Button'),
),
//Part to copy from the source code.
const Duration _kUnconfirmedSplashDuration = const Duration(seconds: 10);
const Duration _kSplashFadeDuration = const Duration(seconds: 2);
const double _kSplashInitialSize = 0.0; // logical pixels
const double _kSplashConfirmedVelocity = 0.1;
class CustomSplashFactory extends InteractiveInkFeatureFactory {
const CustomSplashFactory();
#override
InteractiveInkFeature create({
#required MaterialInkController controller,
#required RenderBox referenceBox,
#required Offset position,
#required Color color,
bool containedInkWell = false,
RectCallback rectCallback,
BorderRadius borderRadius,
double radius,
VoidCallback onRemoved,
}) {
return new CustomSplash(
controller: controller,
referenceBox: referenceBox,
position: position,
color: color,
containedInkWell: containedInkWell,
rectCallback: rectCallback,
borderRadius: borderRadius,
radius: radius,
onRemoved: onRemoved,
);
}
}
class CustomSplash extends InteractiveInkFeature {
/// Used to specify this type of ink splash for an [InkWell], [InkResponse]
/// or material [Theme].
static const InteractiveInkFeatureFactory splashFactory = const CustomSplashFactory();
/// Begin a splash, centered at position relative to [referenceBox].
///
/// The [controller] argument is typically obtained via
/// `Material.of(context)`.
///
/// If `containedInkWell` is true, then the splash will be sized to fit
/// the well rectangle, then clipped to it when drawn. The well
/// rectangle is the box returned by `rectCallback`, if provided, or
/// otherwise is the bounds of the [referenceBox].
///
/// If `containedInkWell` is false, then `rectCallback` should be null.
/// The ink splash is clipped only to the edges of the [Material].
/// This is the default.
///
/// When the splash is removed, `onRemoved` will be called.
CustomSplash({
#required MaterialInkController controller,
#required RenderBox referenceBox,
Offset position,
Color color,
bool containedInkWell = false,
RectCallback rectCallback,
BorderRadius borderRadius,
double radius,
VoidCallback onRemoved,
}) : _position = position,
_borderRadius = borderRadius ?? BorderRadius.zero,
_targetRadius = radius ?? _getTargetRadius(referenceBox, containedInkWell, rectCallback, position),
_clipCallback = _getClipCallback(referenceBox, containedInkWell, rectCallback),
_repositionToReferenceBox = !containedInkWell,
super(controller: controller, referenceBox: referenceBox, color: color, onRemoved: onRemoved) {
assert(_borderRadius != null);
_radiusController = new AnimationController(duration: _kUnconfirmedSplashDuration, vsync: controller.vsync)
..addListener(controller.markNeedsPaint)
..forward();
_radius = new Tween<double>(
begin: _kSplashInitialSize,
end: _targetRadius
).animate(_radiusController);
_alphaController = new AnimationController(duration: _kSplashFadeDuration, vsync: controller.vsync)
..addListener(controller.markNeedsPaint)
..addStatusListener(_handleAlphaStatusChanged);
_alpha = new IntTween(
begin: color.alpha,
end: 0
).animate(_alphaController);
controller.addInkFeature(this);
}
final Offset _position;
final BorderRadius _borderRadius;
final double _targetRadius;
final RectCallback _clipCallback;
final bool _repositionToReferenceBox;
Animation<double> _radius;
AnimationController _radiusController;
Animation<int> _alpha;
AnimationController _alphaController;
#override
void confirm() {
final int duration = (_targetRadius / _kSplashConfirmedVelocity).floor();
_radiusController
..duration = new Duration(milliseconds: duration)
..forward();
_alphaController.forward();
}
#override
void cancel() {
_alphaController?.forward();
}
void _handleAlphaStatusChanged(AnimationStatus status) {
if (status == AnimationStatus.completed)
dispose();
}
#override
void dispose() {
_radiusController.dispose();
_alphaController.dispose();
_alphaController = null;
super.dispose();
}
RRect _clipRRectFromRect(Rect rect) {
return new RRect.fromRectAndCorners(
rect,
topLeft: _borderRadius.topLeft, topRight: _borderRadius.topRight,
bottomLeft: _borderRadius.bottomLeft, bottomRight: _borderRadius.bottomRight,
);
}
void _clipCanvasWithRect(Canvas canvas, Rect rect, {Offset offset}) {
Rect clipRect = rect;
if (offset != null) {
clipRect = clipRect.shift(offset);
}
if (_borderRadius != BorderRadius.zero) {
canvas.clipRRect(_clipRRectFromRect(clipRect));
} else {
canvas.clipRect(clipRect);
}
}
#override
void paintFeature(Canvas canvas, Matrix4 transform) {
final Paint paint = new Paint()..color = color.withAlpha(_alpha.value);
Offset center = _position;
if (_repositionToReferenceBox)
center = Offset.lerp(center, referenceBox.size.center(Offset.zero), _radiusController.value);
final Offset originOffset = MatrixUtils.getAsTranslation(transform);
if (originOffset == null) {
canvas.save();
canvas.transform(transform.storage);
if (_clipCallback != null) {
_clipCanvasWithRect(canvas, _clipCallback());
}
canvas.drawCircle(center, _radius.value, paint);
canvas.restore();
} else {
if (_clipCallback != null) {
canvas.save();
_clipCanvasWithRect(canvas, _clipCallback(), offset: originOffset);
}
canvas.drawCircle(center + originOffset, _radius.value, paint);
if (_clipCallback != null)
canvas.restore();
}
}
}
double _getTargetRadius(RenderBox referenceBox, bool containedInkWell, RectCallback rectCallback, Offset position) {
if (containedInkWell) {
final Size size = rectCallback != null ? rectCallback().size : referenceBox.size;
return _getSplashRadiusForPositionInSize(size, position);
}
return Material.defaultSplashRadius;
}
double _getSplashRadiusForPositionInSize(Size bounds, Offset position) {
final double d1 = (position - bounds.topLeft(Offset.zero)).distance;
final double d2 = (position - bounds.topRight(Offset.zero)).distance;
final double d3 = (position - bounds.bottomLeft(Offset.zero)).distance;
final double d4 = (position - bounds.bottomRight(Offset.zero)).distance;
return math.max(math.max(d1, d2), math.max(d3, d4)).ceilToDouble();
}
RectCallback _getClipCallback(RenderBox referenceBox, bool containedInkWell, RectCallback rectCallback) {
if (rectCallback != null) {
assert(containedInkWell);
return rectCallback;
}
if (containedInkWell)
return () => Offset.zero & referenceBox.size;
return null;
}