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();
}
}
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();
},
),
],
);
}
}
Good day! I am new to Flutter but I was trapped in the problem to make an AR app
I have almost been frustrated all day because of these problems...
Dart Analysis keeps saying this below...
Non-nullable instance field ['Ar_controller'] must be initialized in flutter. My whole day wasted due to this and tomorrow is my presentation. Please help me out as soon as possible
really appreciate it in advance.
import 'dart:typed_data';
import 'package:flutter_auth_ui/armodelselect.dart';
import 'package:arcore_flutter_plugin/arcore_flutter_plugin.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:network_image_to_byte/network_image_to_byte.dart';
import 'package:vector_math/vector_math_64.dart' as vector;
import 'armodelselect.dart';
class arviewpage extends StatefulWidget {
objmodel modeldes;
arviewpage(this.modeldes);
#override
_arviewpageState createState() => _arviewpageState(this.modeldes);
}
class _arviewpageState extends State<arviewpage> {
_arviewpageState(this.modeldes);
void initstate(){
super.initState();
}
List<double> cubesize;
objmodel modeldes;
ArCoreController arCoreController;
Uint8List byteImage,byteImage1;
Future<bool> _onWillPop() async {
Navigator.of(context).pop(true);
return true;
}
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: _onWillPop,
child: Scaffold(
appBar: AppBar(
title: const Text('Xperience'),
),
body: ArCoreView(
onArCoreViewCreated: _onArCoreViewCreated,
),
),
);
}
//functions::::::::::::::::::
void _onArCoreViewCreated(ArCoreController controller) {
arCoreController = controller;
if(modeldes.type=='sphere'){
_addsphere();
}
else if(modeldes.type=="sphere2"){
_addmoonearth();
}
else if(modeldes.type=='cube'){
_addCube();
}
}
//convert image to byteimage
Future<Uint8List> _networkImageToByte(String imageaddress,String imageaddress1) async {
byteImage = await networkImageToByte(imageaddress);
if(imageaddress1!="0"){
byteImage1=await networkImageToByte(imageaddress1);
}
return byteImage;
}
//convert cube size to double
List<String> convertStringtodoublearr(String size){
var cubesize=size.split(",");
for(int i=0;i<cubesize.length;i++){
this.cubesize[i]=double.parse(cubesize[i]);
}
return cubesize;
}
//sphere ar model implementation
Future _addsphere() async {
await _networkImageToByte(modeldes.img_url,modeldes.explain);
//description ar model:::::::::::::::::::::::::::::::
final material = ArCoreMaterial(
metallic: 1,
color: Colors.blue,
//metallic: 1.0,
textureBytes: byteImage1,
);
final cube = ArCoreCube(
materials: [material],
size: vector.Vector3(0.5,0.2,0.1)
);
final node = ArCoreNode(
shape: cube,
position: vector.Vector3(0,-0.4,0.5),
);
//sphere texture model ::::::::::::::::::::::::::::::::::::
final eamaterial = ArCoreMaterial(
metallic: 1,
color: Colors.blue,
textureBytes: byteImage,
);
final sphere = ArCoreSphere(
materials: [eamaterial],
radius: double.parse(modeldes.size[0]),
);
final eanode = ArCoreNode(
shape: sphere,
position: vector.Vector3(0,0,-1),
children: modeldes.explain=="0"?[]:[node],
);
arCoreController.addArCoreNode(eanode);
}
//cube ar model implementation
Future _addCube() async{
await _networkImageToByte(modeldes.img_url,modeldes.explain);
//description ar model::::::::::::::::::::::::::::::::::::
final material = ArCoreMaterial(
metallic: 1,
color: Colors.blue,
//metallic: 1.0,
textureBytes: byteImage1,
);
final cube = ArCoreCube(
materials: [material],
size: vector.Vector3(0.5,0.2,0.1)
);
final node = ArCoreNode(
shape: cube,
position: vector.Vector3(0,-0.4,0.5),
);
//cube texture model ::::::::::::::::::::::::::::::::::;
final cube_material = ArCoreMaterial(
metallic: 1,
color: Colors.blue,
//metallic: 1.0,
textureBytes: byteImage,
);
final cube_cube = ArCoreCube(
materials: [cube_material],
size: vector.Vector3(double.parse(modeldes.size[0]),double.parse(modeldes.size[1]),double.parse(modeldes.size[2])),
);
final cube_node = ArCoreNode(
shape: cube_cube,
children: modeldes.explain=="0"?[]:[node],
position: vector.Vector3(0,0,-1),
);
arCoreController.addArCoreNode(cube_node);
}
Future _addmoonearth() async {
await _networkImageToByte(modeldes.img_url,modeldes.explain);
//description ar model:::::::::::::::::::::::::::::::
final material = ArCoreMaterial(
metallic: 1,
color: Colors.blue,
//metallic: 1.0,
textureBytes: byteImage1,
);
final moon = ArCoreSphere(
materials: [material],
radius: 0.05,
);
final node = ArCoreNode(
shape: moon,
position: vector.Vector3(0.2,0.3,0.5),
);
//sphere texture model ::::::::::::::::::::::::::::::::::::
final eamaterial = ArCoreMaterial(
metallic: 1,
color: Colors.blue,
textureBytes: byteImage,
);
final sphere = ArCoreSphere(
materials: [eamaterial],
radius: double.parse(modeldes.size[0]),
);
final eanode = ArCoreNode(
shape: sphere,
position: vector.Vector3(0,0,-1),
children: modeldes.explain=="0"?[]:[node],
);
arCoreController.addArCoreNode(eanode);
}
#override
void dispose() {
arCoreController.dispose();
super.dispose();
}
}
If some variable is not initialized at definition,
you need to 'late' keyword.
https://dart.dev/null-safety/understanding-null-safety#lazy-initialization
...
late ArCoreController arCoreController;
...
Copy your package name which requires null safety
Go to pub.dev, search for the package name.
Look for similar package with null safety tag.
Copy package name from pub.dev
Paste in your pubspec.yaml in place of old package name.
Click on download button if working in VS Code or type in the command
flutter pub get
I'm creating a mini-game in my app, which should consist of a grid of 3x3 buttons, which will flash randomly in sequence one-by-one and the user has to recreate it.
I managed to create the buttons with GridView() and set up a timer, but right now I'm struggling with changing the color property of the button inside the GridView(). Which got me thinking If I'm using GridView() correctly.
I want to change the color of a button multiple times via Timer in a random sequence and then the user should recreate it. Can I do it with a timer and GridView()or is there an easier way of doing this?
(My mini-game should be similar to Among us Reactor task)
import 'dart:async';
import 'package:flutter/material.dart';
class Challenge extends StatefulWidget {
#override
_ChallengeState createState() => _ChallengeState();
}
class _ChallengeState extends State<Challenge> {
Timer timer;
Map userData = {};
int indexSaved;
#override
void initState() {
super.initState();
timer = new Timer.periodic(new Duration(seconds: 2), (Timer timer) {
print('hey');
});
}
#override
void dispose() {
super.dispose();
timer.cancel();
}
#override
Widget build(BuildContext context) {
userData = ModalRoute.of(context).settings.arguments;
print(userData);
return Scaffold(
body: SafeArea(
child: Center(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: GridView.count(
crossAxisCount: 3,
// mainAxisSpacing: 20.0,
shrinkWrap: true,
children: List.generate(9, (index) {
return Center(
child: MaterialButton(
color: Colors.blueGrey[300],
padding: EdgeInsets.all(50.0),
splashColor: Colors.white,
onPressed: () {
indexSaved = index;
print(indexSaved);
},
),
);
}),
),
),
),
),
);
}
}
EDIT 21/10/2020:
I created a little function that should generate sequences and I'm launching them inside the timer. I worked on the answers which I got and tried to redo them so I could use them in my use case.
The color I'm changing with:
color: indexWithColor == index
? Colors.indigo
: Colors.blueGrey[300],
which works great.
(The snippet was edited)
List<int> correctValues = [];
int indexWithColor;
int indexDisplayed = 0;
void generateNewLevel() {
final random = new Random(seed);
correctValues.add(random.nextInt(9));
timer = new Timer.periodic(new Duration(milliseconds: 500), (Timer timer) {
setState(() {
indexWithColor = correctValues[indexDisplayed];
// print('Index color ' + indexWithColor.toString());
indexDisplayed++;
if (indexDisplayed == correctValues.length) {
new Timer(new Duration(milliseconds: 500), () {
setState(() {
indexWithColor = null;
indexDisplayed = 0;
});
timer.cancel();
});
timer.cancel();
}
});
});
}
Right now I'm using a button to generate a new level (later I will change it when I solve this problem). It works, but I have issues with it.
Users cannot distinguish if the button is pressed twice (right now its color is pressed for a bit longer).
2. It seems like the buttons have a bit delay when they are launching. It's not exact 500ms and the sleep is really messy.
3. The first level (with a one-long sequence is not visible as it gets changed as soon as the timer is canceled.
Is there a better option?
EDIT: 21/10/2020 12:00pm
I solved the second and third problems with edited timer, so it kinda works right now, but isn't there a better way?
Check the edited snippet. -^
If you want to just randomly change the color of your button this is one way to do it:
(Edited: to make sure that same index isn't selected twice in a row)
final rng = Random();
int indexWithColor;
#override
void initState() {
super.initState();
timer = new Timer.periodic(new Duration(seconds: 2), (Timer timer) {
setState(() {
int tempIndex;
do {
tempIndex = rng.nextInt(9);
} while (tempIndex == indexWithColor);
indexWithColor = tempIndex;
});
});
}
and in your MaterialButton:
color: indexWithColor == index
? Colors.red
: Colors.blueGrey[300],
Yes, you can do it with a Timer. But you need to expand your list with a boolean field which is isIndexSaved.
class NewModel {
int index;
bool isIndexSaved;
}
Then you need to generate a new list with newModel and set isIndexSaved to true when onPressed(). And the color of the MaterialButton should look like:
color: newModel.isIndexSaved
? Colors.red[300], // desired color
: Colors.blueGrey[300],
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;
}
}
I am looking at charts_flutter package. I need to implement a gauge chart, with a single segment and its label value at the gauge's center. See the mockup file below, where three charts from the required type are placed in a row:
Using Google chart gauge sample, I was able to implement the required gauge, however, I am struggling to set the label as per the requirement.
Below is the class, I use for the gauge, any help / hints how to add the label would be greatly appreciated:
/// Gauge chart example, where the data does not cover a full revolution in the
/// chart.
import 'package:charts_flutter/flutter.dart' as charts;
import 'package:flutter/material.dart';
import 'dart:math';
class GaugeChart extends StatelessWidget {
final List<charts.Series> seriesList;
final bool animate;
GaugeChart(this.seriesList, {this.animate});
factory GaugeChart.fromValue(
{#required double value, #required Color color, bool animate}) {
return GaugeChart(
_createDataFromValue(value, color),
// Disable animations for image tests.
animate: animate,
);
}
#override
Widget build(BuildContext context) {
return charts.PieChart(
seriesList,
animate: animate,
// Configure the width of the pie slices to 30px. The remaining space in
// the chart will be left as a hole in the center. Adjust the start
// angle and the arc length of the pie so it resembles a gauge.
defaultRenderer: charts.ArcRendererConfig(
arcWidth: 20,
startAngle: 3 / 5 * pi,
arcLength: 9 / 5 * pi,
//arcRendererDecorators: [charts.ArcLabelDecorator(labelPosition: charts.ArcLabelPosition.outside)],
),
);
}
static List<charts.Series<GaugeSegment, String>> _createDataFromValue(
double value, Color color) {
double toShow = (1 + value) / 2;
final data = [
GaugeSegment('Main', toShow, color),
GaugeSegment('Rest', 1 - toShow, Colors.transparent),
];
return [
charts.Series<GaugeSegment, String>(
id: 'Segments',
domainFn: (GaugeSegment segment, _) => segment.segment,
measureFn: (GaugeSegment segment, _) => segment.value,
colorFn: (GaugeSegment segment, _) => segment.color,
// Set a label accessor to control the text of the arc label.
labelAccessorFn: (GaugeSegment segment, _) =>
segment.segment == 'Main' ? '${segment.value}' : null,
data: data,
)
];
}
}
/// data type.
class GaugeSegment {
final String segment;
final double value;
final charts.Color color;
GaugeSegment(this.segment, this.value, Color color)
: this.color = charts.Color(
r: color.red, g: color.green, b: color.blue, a: color.alpha);
}
This is how the class can be used:
// value can take values between -1 and 1
GaugeChart.fromValue(value: 0.34, color: Colors.red)
We achieved this using a Stack:
return Container(
width: 120,
height: 120,
child: Stack(children: [
GaugeChart(_getChartData()),
Center(
child: Text(
'$percent',
))
]));