My slider jumps from one value to another. How can I make it movable in between the values or make it smoother in Flutter?
Column(
children: <Widget>[
RangeSlider(
min: 1,
max: 40,
divisions: 4,
onChanged: onChange,
values: RangeValues((startValue ?? 1).toDouble(), (endValue ?? 40).toDouble()),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Text('Beginner', style: Theme.of(context).textTheme.body2),
Text('Ameateur', style: Theme.of(context).textTheme.body2),
Text('Intermediate', style: Theme.of(context).textTheme.body2),
Text('Advanced', style: Theme.of(context).textTheme.body2),
Text('Professional', style: Theme.of(context).textTheme.body2),
],
),
],
);
The divisions property makes it jump in 4 steps. Remove that and the slider will be fluent.
RangeSlider(
min: 1,
max: 40,
// divisions: 4, // <-- remove this
onChanged: onChange,
values: RangeValues((startValue ?? 1).toDouble(), (endValue ?? 40).toDouble()),
),
Update after question clarification
Question is actually about having the thumbs being fluent while still snapping to discrete points on the range slider. The Flutter widget does not provide an easy way to achieve that, so there are two options.
Option 1: custom widget
The first is to create a new widget to get the exact behavior you need. This might be the best option if you need a lot of customisation.
Option 2: hack the RangeSlider widget
Use the onChangeEnd callback in addition to the onChanged callback to update the position of the thumb correctly. The onChanged callback is used to make it fluent, while the onChangeEnd callback is used to snap the thumb to the closest discrete value based on a division count.
A possible implementation (the discretize method can possibly be shorter/improved):
class _RangeSliderExampleState extends State<RangeSliderExample> {
double _startValue;
double _endValue;
static const double _minValue = 1;
static const double _maxValue = 40;
static const double _divisions = 4;
#override
Widget build(BuildContext context) {
return RangeSlider(
min: _minValue,
max: _maxValue,
onChanged: (values) {
setState(() {
_startValue = values.start;
_endValue = values.end;
});
},
onChangeEnd: (values) {
setState(() {
_startValue = _discretize(values.start, _divisions);
_endValue = _discretize(values.end, _divisions);
});
},
values: RangeValues((_startValue ?? 1).toDouble(),
(_endValue ?? 40).toDouble()),
);
}
double _discretize(double value, double divisions) {
double x = value - _minValue;
double range = _maxValue - _minValue;
double result = x / range;
return (result * divisions).round() / divisions * range + _minValue;
}
}
Related
I have built a custom slider and have been using GestureDetector with onHorizontalDragUpdate to report drag changes, update the UI and value.
However, when a user lifts their finger, there can sometimes be a small, unintentional hop/drag, enough to adjust the value on the slider and reduce accuracy. How can I stop this occuring?
I have considered adding a small delay to prevent updates if the drag hasn't moved for a tiny period and assessing the primaryDelta, but unsure if this would be fit for purpose or of there is a more routine common practive to prevent this.
--
Example of existing drag logic I am using. The initial drag data is from onHorizontalDragUpdate in _buildThumb. When the slider is rebuilt, the track size and thumb position is calculated in the LayoutBuilder and then the value is calculated based on the thumb position.
double valueForPosition({required double min, required double max}) {
double posIncrements = ((max) / (_divisions));
double posIncrement = (_thumbPosX / (posIncrements));
double incrementVal =
(increment) * (posIncrement + widget.minimumValue).round() +
(widget.minimumValue - widget.minimumValue.truncate());
return incrementVal.clamp(widget.minimumValue, widget.maximumValue);
}
double thumbPositionForValue({required double min, required double max}) {
return (max / (widget.maximumValue - widget.minimumValue - 1)) *
(value - widget.minimumValue - 1);
}
double trackWidthForValue({
required double min,
required double max,
required double thumbPosition,
}) {
return (thumbPosition + (_thumbTouchZoneWidth / 2))
.clamp(min, max)
.toDouble();
}
bool isDragging = false;
bool isSnapping = false;
Widget _buildSlider() {
return SizedBox(
height: _contentHeight,
child: LayoutBuilder(
builder: (context, constraints) {
double minThumbPosX = -(_thumbTouchZoneWidth - _thumbWidth) / 2;
double maxThumbPosX =
constraints.maxWidth - (_thumbTouchZoneWidth / 2);
if (isDragging) {
_thumbPosX = _thumbPosX.clamp(minThumbPosX, maxThumbPosX);
value = valueForPosition(min: minThumbPosX, max: maxThumbPosX);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
widget.onChanged(value);
});
} else {
_thumbPosX = thumbPositionForValue(
min: minThumbPosX,
max: maxThumbPosX,
);
}
double minTrackWidth = 0;
double maxTrackWidth = constraints.maxWidth;
double trackWidth = 0;
if (isDragging) {
trackWidth = (_thumbPosX + (_thumbTouchZoneWidth / 2))
.clamp(_thumbWidth, constraints.maxWidth);
} else {
trackWidth = trackWidthForValue(
min: minTrackWidth,
max: maxTrackWidth,
thumbPosition: _thumbPosX,
);
}
return Stack(
alignment: Alignment.centerLeft,
clipBehavior: Clip.none,
children: [
_buildLabels(),
_buildInactiveTrack(),
Positioned(
width: trackWidth,
child: _buildActiveTrack(),
),
Positioned(
left: _thumbPosX,
child: _buildThumb(),
),
],
);
},
),
);
}
Widget _buildThumb() {
return GestureDetector(
behavior: HitTestBehavior.opaque,
dragStartBehavior: DragStartBehavior.down,
onHorizontalDragUpdate: (details) {
setState(() {
_thumbPosX += details.delta.dx;
isDragging = true;
});
},
child: // Thumb UI
);
}
Updated: I make a little adjustment by adding a delay state and lastChangedTime.
If the user stops dragging for a short period (3 sec), the slider will be locked until the next new value is updated + a short delay (1.5 sec)
I follow your train of thought and make a simple example from Slider widget.
Is the result act like your expected? (You can adjust the Duration to any number)
DartPad: https://dartpad.dev/?id=95f2bd6d004604b3c37f27dd2852cb31
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({super.key});
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
double _currentSliderValue = 20;
DateTime lastChangedTime = DateTime.now();
bool isDalying = false;
#override
Widget build(BuildContext context) {
return Column(
children: [
Text(_currentSliderValue.toString()),
const SizedBox(height: 30),
Slider(
value: _currentSliderValue,
max: 100,
label: _currentSliderValue.round().toString(),
onChanged: (double value) async {
if (isDalying) {
await Future.delayed(
Duration(milliseconds: 1500),
() => isDalying = false,
);
} else {
if (DateTime.now().difference(lastChangedTime) >
Duration(seconds: 3)) {
isDalying = true;
} else {
setState(() {
_currentSliderValue = value;
});
}
}
lastChangedTime = DateTime.now();
},
),
],
);
}
}
I am trying to move slider for zoom in flutter camera. I tried many things but I could not get solution. I am attaching my code with screenshot. Please give right suggestion.
double _minAvailableZoom = 2.0;
double _maxAvailableZoom = 2.0;
double _currentZoomLevel = 2.0;
Expanded(
child: Slider(
value: _currentZoomLevel,
max: _maxAvailableZoom,
min: _minAvailableZoom,
activeColor: Colors.white,
inactiveColor: Colors.white30,
onChanged: (value) async {
setState(() {
_currentZoomLevel = value;
});
await cameraController!.setZoomLevel(value);
},
),
),
just do this,
double _minAvailableZoom = 0.0;
double _currentZoomLevel = 0.0;
You have set that maximum available zoom is equal to minimun available zoom so you are unable to zoom. don't add maximum zoom property if it is required the set to some higherpoints as you require to zoom.
This is how a Slider's animation normally looks like:
This is how a Slider looks when I try to add the value label:
This is the sample code:
Slider(
value: sliderValue,
activeColor: color,
min: 0.0,
max: 100.0,
divisions: 2000, //TO COMMENT
label: sliderValue.toStringAsFixed(2), //TO COMMENT
onChanged: (value) {
setState(() {
sliderValue = value;
});
}),
In this code, if I comment out the marked //TO COMMENT lines which are the divisions and label properties, the `label goes away as expected, and the animation is smooth again.
I assume this is due to divisions, and any amount of it, even just 100 does not change the lag in any way.
Additionally, it seems that the label property does not work on its own.
It needs the divisions property to also be set so that the value
label can be displayed.
What is the workaround so that I can achieve a Slider with the smoothness shown in the first image, but have the default value label or what looks the same?
If you take a look on source code, you can find _positionAnimationDuration which is responsible to animate the slider
Change it to
static const Duration _positionAnimationDuration = Duration.zero;
Changing on source-code will affect on others project, instead create a local dart file, paste the full slider code and make changes.
Let say we have created customSlider.dart file
. Make sure to replace some(./xyz.dart) top imports with import 'package:flutter/cupertino.dart'; or material on our customSlider.dart.
Then replace _positionAnimationDuration.
To use this, import the file
import 'customSlider.dart' as my_slider;
...
//use case
my_slider.Slider(....)
// create class
// .yaml > another_xlider: ^1.0.0
import 'package:another_xlider/another_xlider.dart';
import '../res/res_controller.dart';
import '../utils/utils_controller.dart';
import 'package:flutter/material.dart';
class RangeBar extends StatelessWidget {
final List<double>? values;
final double? min;
final double? max;
final Function(int, dynamic, dynamic)? onDragging;
const RangeBar(
{Key? key,
#required this.values,
#required this.onDragging,
#required this.min,
#required this.max})
: super(key: key);
#override
Widget build(BuildContext context) {
return FlutterSlider(
values: values!,
// pre set values
rangeSlider: true,
handlerAnimation: FlutterSliderHandlerAnimation(
curve: Curves.elasticOut,
reverseCurve: Curves.bounceIn,
duration: Duration(milliseconds: 500),
scale: 1.5),
jump: true,
min: min ?? 0,
max: max ?? 0,
touchSize: Sizes.s13,
trackBar: FlutterSliderTrackBar(
activeTrackBar: BoxDecoration(color: AppColors.orange),
),
tooltip: FlutterSliderTooltip(
boxStyle: FlutterSliderTooltipBox(
decoration: BoxDecoration(
color: Colors.greenAccent,
borderRadius: BorderRadius.all(Radius.circular(Sizes.s5)),
border: Border.all(
color: AppColors.steelGray,
),
),
),
positionOffset: FlutterSliderTooltipPositionOffset(top: -Sizes.s15),
alwaysShowTooltip: true,
textStyle:
TextStyles.defaultRegular.copyWith(fontSize: FontSizes.s11),
),
onDragging: onDragging);
}
}
// try to call
Container(
child: _size(),
)
Widget _size() {
{
double sizeMin;
double sizeMax;
sizeMin = 0;
sizeMax = 0;
sizeMax = sizeMin.round() == sizeMax.round() ? sizeMax + 1 : sizeMax;
return RangeBar(
values: [
// add values
],
min: sizeMin,
max: sizeMax.ceilToDouble(),
onDragging: (p0, lowerValue, upperValue) {
log(lowerValue);
log(upperValue);
},
);
return C0();
}
}
So I want to build a slider where users can choose a value between 100-1000 and the value should be like this 100,150,200,250... 950,1000 but I noticed that I get values like 499.9999994 or 500.000001. Any idea how can I fix this?
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('100'),
CupertinoSlider(
value: context.watch<LoanValue>().value,
min: 100,
max: 1000,
divisions: 18,
onChanged: (double value) {
context.read<LoanValue>().updateValue(value);
},
),
Text('1000'),
],
),
this is where I update the value:
class LoanValue with ChangeNotifier, DiagnosticableTreeMixin {
double loanValue = 100;
double get value => loanValue;
void updateValue(double division) {
loanValue = division;
// debugPrint('loanValue: $loanValue');
notifyListeners();
}
}
I fixed this by adding this .roundToDouble()
void updateValue(double division) {
loanValue = division.roundToDouble();
// debugPrint('loanValue: $loanValue');
notifyListeners();
}
I am using Flutter and the Bloc Pattern with rxdart and would like to have a debug mode in my app similar like you enable the developer mode in Android the user should touch 5 times a logo in the UI.
The UI part is pretty simple with:
Column _renderLogo(BuildContext context) => new Column(
children: <Widget>[
GestureDetector(
onTap: () => BlocProvider.of(context).debugEnabledSink.add(true),
child: Container( ...more logo rendering...
With that in mind, I am looking for an easy elegant way to enable the detection of 5 events in 10 seconds. The whole detection should reset when not enough events are detected in any 10 second time window.
You can use a pseudo-timer to achieve this:
const maxDebugTimerSeconds = 10;
const maxTapCount = 5;
DateTime firstTap;
int tapCount;
void doGestureOnTap() {
final now = DateTime.now();
if (firstTap != null && now.difference(firstTap).inSeconds < maxDebugTimerSeconds) {
tapCount++;
if (tapCount >= maxTapCount) {
BlocProvider.of(context).debugEnabledSink.add(true);
}
} else {
firstTap = now;
tapCount = 0;
}
}
...
GestureDetector(
onTap: doGestureOnTap,
...
),
This answer is based on the comment of #pskink above. I just post it here because it seems like a very elegant way of solving my problem. Thank you #pskink
DebugSwitcher might not be the best class Name it's more TimeWindowEventDetector but you get the idea.
class DebugSwitcher {
final Duration window;
final int cnt;
bool value = false;
var timeStamps = [];
DebugSwitcher({this.window = const Duration(seconds: 10), this.cnt = 5});
call(data, sink) {
var now = DateTime.now();
timeStamps.removeWhere((ts) => now.difference(ts) >= window);
timeStamps.add(now);
if (timeStamps.length >= cnt) {
timeStamps = [];
sink.add(value = !value);
}
}
}
Than u listen to the Sink Events this way:
_debugEnabledController.stream
.transform(StreamTransformer.fromHandlers(handleData: DebugSwitcher()))
.listen(
(_) {
_isDebugModeOn.value = true;
infoInternal.value = 'Enabled Debug Mode';
},
);
The Sink is defined like this:
final StreamController<bool> _debugEnabledController =
StreamController<bool>();
Sink<bool> get debugEnabledSink => _debugEnabledController.sink;
In the UI the code looks like this:
Column _renderLogo(BuildContext context) => new Column(
children: <Widget>[
GestureDetector(
onTap: () => BlocProvider.of(context).debugEnabledSink.add(true),
child: Container(
margin: const EdgeInsets.only(top: 10.0, right: 10.0),
height: 80.0,
child: Theme.of(context).primaryColorBrightness ==
Brightness.light
? new Image.asset('assets/app_icon_light.png', fit: BoxFit.contain)
: new Image.asset('assets/app_icon.png', fit: BoxFit.contain),
),
),
],
);