I have several methods from API in my websockets, so i need to create the range slider which put value from sliders to Map. Now it works perfectly fine only with setting the max value not the min value. What should I change in my code to make min and max values works fine together? how can I write the values from the range slider to the value like it is in print alternately? The logic is this: the user moves the beginning of the slider and the value is written, then the user moves the end of the slider and the value from the end of the slider is also written. So can be done?
My API request:
Future<Map?> fetchGains(num values, num valueMax) async {
await hubConnection.start();
Map? gains;
num compensation = 1;
if (hubConnection.state == HubConnectionState.Connected) {
await hubConnection
.invoke('SetGainMax', args: <Object>[valueMax])
.then((value1) => compensation = valueMax)
.then((value2) => hubConnection
.invoke('GetGainMax')
.then((value3) => gains = value3 as Map))
.then((value) => hubConnection
.invoke('SetGain', args: [values])
.then((value) => compensation = values)
.then((value) => hubConnection
.invoke('GetGain')
.then((value) => gains = value as Map?)));
}
hubConnection.onclose(({error}) {
throw Exception(error);
});
print(gains!['min']);
print(gains);
return gains;
}
}
My range sliders where I want to manipulate with min and max value:
var _currentRangeValues = const RangeValues(1, 16);
#override
Widget build(BuildContext context) {
return FutureBuilder<Map?>(
future: GetGainsForCameras().fetchGains(
GetGainsForCameras().fetchGains(
_currentRangeValues.start.round().toInt(),
_currentRangeValues.end.round().toInt()),
builder: (context, snapshot) {
SizedBox(
child: RangeSlider(
values: _currentRangeValues,
activeColor: Colors.green,
min: 1,
max: 16,
inactiveColor: Colors.green,
onChanged: (RangeValues value) {
setState(() {
_currentRangeValues = value;
});
},
),
)
]);
},
}
Now I can set onle max value not the min value and this I get in print {min: 1, max: 16, value: 10.0, step: 1.0}
May thanks to everyone in answer section who noticed my mistake.
Is is going to work as I want or backend devs should make some changes?
your are calling .end for both min and max
_currentRangeValues.end.round().toInt(),
_currentRangeValues.end.round().toInt()
range has both start and end constructors. so
_currentRangeValues.start.round().toInt(),
_currentRangeValues.end.round().toInt()
You're not using _currentRangeValues.start which is the min value of the slider. Probably your fetchGains call should include that.
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 make range slider where values are loaded before the app is completed and also i am trying to get value from the API, not hardcoded it, so I create such function which should loaded values before app is completed and trying to get values from API, but still facing with issues about cast to types:
static Map? gainMin;
var _currentRangeValues = RangeValues(
num.parse(gainMin?['min']).round().toDouble(),
num.parse(gainMin?['max']).round().toDouble());
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) {
fetchGains(num.parse(gainMin?['min']).round().toDouble(),
num.parse(gainMin?['max']).round().toDouble());
});
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder<Map?>(
future: fetchGains(_currentRangeValues.start.round().toInt(),
_currentRangeValues.end.round().toInt()),
builder: (context, snapshot) {
print(gainMin);
RangeSlider(
values: RangeValues(num.parse(gainMin?['min']).roundToDouble(),
num.parse(gainMin?['max']).roundToDouble()),
min: num.parse(gainMin?['min']).roundToDouble(),
max: num.parse(gainMin?['max']).roundToDouble(),
activeColor: Colors.green,
inactiveColor: Colors.green,
onChanged: (RangeValues value) {
setState(() {
if (_debounce?.isActive ?? false) _debounce?.cancel();
_debounce = Timer(const Duration(milliseconds: 15), () {
snapshot.data?['value'] = value.start;
snapshot.data?['valueMax'] = value.end;
// _currentRangeValues = value;
});
});
},
)
}
}
But I got the error: type 'int' is not a subtype of type 'String' How can i solve this issue?
Print returns {min: 1, max: 22, value: 0.0, valueMax: 0.0, step: 1.0}
Try using double.tryParse
values: RangeValues(
double.tryParse("${gainMin?['max']}") ?? .0,
double.tryParse("${gainMin?['min']}") ?? 1.0,
),
I'm trying to build a note app, all data and other things is working perfectly, cos the data is displaying to the screen when the code file is saving, its weird , first time facing this problem
in short, the valuelistanble is not listening when the data adding from app, but when just hot reloading the data is displaying
how can i fix this,
here is the code
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
WidgetsBinding.instance!.addPostFrameCallback((_) async {
final value = await NoteDB.instance.getAllNotes();
});
____________________________________________
____________________________________________
//code line for aligment
Expanded(
child: ValueListenableBuilder(
valueListenable: NoteDB.instance.noteListNotifier,
builder: (context, List<NoteModel> newNotes, _) {
return GridView.count(
childAspectRatio: 3 / 4,
crossAxisCount: 2,
mainAxisSpacing: 34,
crossAxisSpacing: 30,
padding: const EdgeInsets.all(20),
//generating list for all note
children: List.generate(
newNotes.length,
(index) {
//setting the notelist to a variable called [note]
final note = newNotes[index];
if (note.id == null) {
//if the note's id is null set to sizedbox
//the note id never be null
const SizedBox();
}
return NoteItem(
id: note.id!,
//the ?? is the statement (if null)
content: note.content ?? 'No Content',
title: note.title ?? 'No Title',
);
},
),
);
},
)),
here is the NoteDB.instance.getAllNotes(); function
#override
Future<List<NoteModel>> getAllNotes() async {
final _result = await dio.get(url.baseUrl+url.getAllNotes);
if (_result.data != null) {
final noteResponse = GetAllNotes.fromJson(_result.data);
noteListNotifier.value.clear();
noteListNotifier.value.addAll(noteResponse.data.reversed);
noteListNotifier.notifyListeners();
return noteResponse.data;
} else {
noteListNotifier.value.clear();
return [];
}
}
and also there is a page to create note , and when create note button pressed there is only one function calling here is function
Future<void> saveNote() async {
final title = titleController.text;
final content = contentController.text;
final _newNote = NoteModel.create(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: title,
content: content,
);
final newNote = await NoteDB().createNote(_newNote);
if (newNote != null) {
print('Data Added to the DataBase Succesfully!');
Navigator.of(scaffoldKey.currentContext!).pushAndRemoveUntil(
MaterialPageRoute(
builder: (context) => HomePage()),
(Route<dynamic> route) => false);
} else {
print('Error caught while data adding to the DataBase');
}
}
everything work fine, but while add the data the UI isn't refreshing even tho notifier is active
and if you need full code please have a look at this github link : https://github.com/Mishalhaneef/Note-app
Since this ValueNotifier has a type of List<NoteModel>, the value will not change when you add new items to the list or delete from it or clear all. The value here is a reference to the list which does not change.
You have to assign a new value to it, like:
noteListNotifier.value = List<NoteModel>[<add your current items here>];
You can manipulate your current list with List.from, removeWhere, add etc., and then re-assign the complete list.
Besides you don't need to call notifyListeners in case of a ValueNotifier, the framework handles it, see here.
Another approach would be to use a custom ChangeNotifierProvider where you can call notifyListeners when the contents of your list are changed.
Some further suggestions:
In your homescreen.dart file, instead of NoteDB.instance.noteListNotifier.value[index] you can use newNotes[index].
In data.dart, within getAllNotes, you have to set a new value for noteListNotifier in order to get the changes propagated. Currently you are just modifying items in this list and that is not considered to be a change. Try this code:
#override
Future<List<NoteModel>> getAllNotes() async {
//patching all data from local server using the url from [Post Man]
final _result = await dio.get(url.baseUrl+url.getAllNotes);
if (_result.data != null) {
//if the result data is not null the rest operation will be operate
//recived data's data decoding to json map
final _resultAsJsonMap = jsonDecode(_result.data);
//and that map converting to dart class and storing to another variable
final getNoteResponse = GetAllNotes.fromJson(_resultAsJsonMap);
noteListNotifier.value = getNoteResponse.data.reversed;
//and returning the class
return getNoteResponse.data;
} else {
noteListNotifier.value = <NoteModel>[];
return [];
}
}
Minimal reproducible code:
class _MyPageState extends State<MyPage> {
double _dx1 = 0;
double _dx2 = 0;
final Duration _duration = Duration(seconds: 1);
final Curve _curve = Curves.linear;
void _play() {
final width = MediaQuery.of(context).size.width;
_dx1 = width;
var i = 0;
Timer.periodic(Duration(milliseconds: 1), (timer) {
if (i > 1000) {
timer.cancel();
} else {
setState(() {
_dx2 = _curve.transform(i / 1000) * width;
i++;
});
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: _play,
child: Icon(Icons.play_arrow),
),
body: SizedBox.expand(
child: Stack(
children: [
AnimatedPositioned(
left: _dx1,
duration: _duration,
curve: _curve,
child: _box,
),
Positioned(
left: _dx2,
top: 60,
child: _box,
),
],
),
),
);
}
Container get _box => Container(width: 50, height: 50, color: Colors.red);
}
Output:
As you can see my second custom animated box doesn't catch up with the first default AnimatedPositioned widget.
Note: This can easily be done using Tween and AnimationController but I just want to know why I'm unable to correctly use Curve's value to match the default behavior.
Assumption: The callback is periodically called for every millisecond.
Expected Result: After 1 second, i = 1000;
The assumption is wrong. Add the below code to verify:
void _play() {
...
var i = 0;
final start = DateTime.now().millisecondsSinceEpoch;
Timer.periodic(Duration(milliseconds: 1), (timer) {
final elapse = DateTime.now().millisecondsSinceEpoch - start;
print('elapse = $elapse');
print('i = $i');
print('ticks = ${timer.tick}');
print('######################');
...
});
}
On my pc the last value is:
elapse = 14670
i = 1001
ticks = 14670
######################
So this implies that it took 14 seconds on my PC for 1001 callbacks. That's not what we were expecting. What we can infer from this is that some callbacks are missed and i does not reflect the time elapsed.
However, the value we need is timer.tick. Quoting the docs
If a periodic timer with a non-zero duration is delayed too much, so more than one tick should have happened, all but the last tick in the past are considered "missed", and no callback is invoked for them. The tick count reflects the number of durations that have passed and not the number of callback invocations that have happened.
So, tick tells us the number of periods that have passed. The below code will catch up with the AnimatedPositioned
void _play() {
final width = MediaQuery.of(context).size.width;
_dx1 = width;
final period = Duration(milliseconds: 1);
Timer.periodic(period, (timer) {
final elapsed = timer.tick * period.inMilliseconds;
if (elapsed > _duration.inMilliseconds) {
timer.cancel();
} else {
setState(() {
_dx2 = _curve.transform(elapsed / _duration.inMilliseconds) * width;
});
}
});
}
Now, you might see some stutter where our box might be a bit ahead or behind the AnimatedPositioned, this is because we use Timer but the AnimatedPositioned uses Ticker. The difference is Timer.periodic is driven by the Duration we passed as period, but Ticker is driven by SchedulerBinding.scheduleFrameCallback. So, the instant the value _dx2 is updated and the instant the frame is rendered on the screen might not be the same. Add the fact that some callbacks are missed!
I want to use a (Cupertino)slider with which you can pick a number between 1-1000000. For every milestone, I will display another text. (1-10, 10-100, 100-1000, etc.) Since in the beginning, the steps are shorter, I want the slider not being that sensitive, and in the end, I wouldn't care it going with 100 each step, for example. Like it should go exponential. Does anybody have an idea of how to achieve that?
Thanks a lot!
I found a solution using https://pub.dev/packages/flutter_xlider
Thanks a lot!
Use the CupertinoSlider and some class that holds the min and max value, in this example I use RangeValues (it's a flutter class for another type of Slider, but I can use it to save a start and end value). When the onChangedEnd value is equal to the slider value then it displays a snackbar showing the milestone and update the range value from the old one 10 times. division parameter can be used to make a discrete slider, but if the max value is too big you can omit it or pass a null value to make it continuos
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
RangeValues range = RangeValues(1, 10);
double slide = 1;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Builder( //Needed to find Scaffold.of(context) and display the snackbar
builder: (BuildContext context) {
return CupertinoSlider(
value: slide,
onChanged: (newValue) => setState(() => slide = newValue),
min: range.start,
max: range.end,
divisions: (range.end - range.start).toInt(),
onChangeEnd: (value) {
if (value == range.end) {
Scaffold.of(context)
..hideCurrentSnackBar()
..showSnackBar(SnackBar(
content: Text('New Milestone reached: ${slide.toInt().toString()}')));
setState(() => range = RangeValues(range.start * 10, range.end * 10));
}
},
);
}
),
Text(slide.toInt().toString())
]
)
);
}
}