Ok, I am using a circularProgressIndicator to show when a timer reaches complete, however my brain has ceased to function and I can't get the math to work for it.
The timer value is 90 seconds
The progress indicator is 100% at 1, or 50% at 0.5 etc..
Every second I reduce the 90 seconds timer by 1 second so show the countdown
I would also like to incrementally add to the progress indicator value to reach 100% when the timer of 90 seconds has run out.
90 is an example, this will change regularly
What I have tried
1. _intervalProgress += (( 100 / intervalValueSeconds) / 60;
2. _intervalProgress += (( 100 / intervalValueSeconds) * ( 100 / 60 )) / 100;
Question: How can I find a decimal of a value, then divide by 60 in order to iterate ever second <--- Wait, I just realised that the value isn't 60 seconds, it is the value of the timer, therefore I have been calculating it wrong all along.
You can calculate and display the progress like below. As per your question, seems like you are looking for the calculation of the variable progressFraction.
class _MyWidgetState extends State<MyWidget> {
int totalSeconds = 90;
int secondsRemaining = 90;
double progressFraction = 0.0;
int percentage = 0;
Timer timer;
#override
void initState() {
timer = Timer.periodic(Duration(seconds: 1), (_) {
if(secondsRemaining == 0){
return;
}
setState(() {
secondsRemaining -= 1;
progressFraction = (totalSeconds - secondsRemaining) / totalSeconds;
percentage = (progressFraction*100).floor();
});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Column(
children: [
SizedBox(height: 20),
Text('$secondsRemaining seconds remaining'),
SizedBox(height: 20),
CircularProgressIndicator(
value: progressFraction,
),
SizedBox(height: 20),
Text('$percentage% complete'),
],
);
}
#override
void dispose(){
timer.cancel();
super.dispose();
}
}
Demo on dartpad - https://dartpad.dev/4fae5a168d5e9421562d0e813b907a01
Use below logic
int value = ((yourValue/ 90) * 100).toInt(); // here 90 is higest value which you have
print(value);
Example:
int value = ((45/ 90) * 100).toInt();
print(value); // Output will be 50
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();
},
),
],
);
}
}
void valuerandomer() {
Future.delayed(Duration(milliseconds: 500), () {
int count = 0;
int max = 1000;
int min = 1;
Random rnd = new Random();
while (count != -1) {
count++;
value += rnd.nextInt(6) + (-5);
}
if (value > (max - 1)) {
value = 999;
} else if (value < 0) {
value = 0;
}
print(value);
});
}
I want the function to print every 500 miliseconds in the text widget so the value parameter starts with the value of 75 and changes every 500 milliseconds with this function.
How do I do that?
How do I declare this function in the text widget like Text('$valuerandomer')? cuz its just dont work. I tried just to type there $value but still doesnt work
For every x time, try using Timer.periodic
Timer? _timer;
String text = "initText";
#override
void dispose() {
_timer?.cancel();
super.dispose();
}
void valuerandomer() {
_timer = Timer.periodic(
Duration(milliseconds: 500),
(t) {
//perform your work
text = "newText ";
setState(() {});
},
);
}
Use funtion
Timer.periodic(Duration(/..),(timer){
//Put your logic
setState((){});
})
My widget uses TickerProviderStateMixn and AnimationController to animate CustomPaint. and whenever AnimationController updates CustomPaint, it also updates data handler class by calling update() method in this data handler.
This widget contains player thing, and whenever update() is called, I made player bar move specific amount of x position. Player also has to show time value, basically, player bar's position indicates how time passes. So time value is displayed along player bar's position. I thought flutter supports only 60 fps, meaning update() is called 60 times per second, I made player bar move X/60 whenever update() is called. X is just any value, if time becomes 1 second when player bar reaches position of 100, then X will be 100.
But recently, I changed my phone to new one, and this device can support 120fps, and it seems AnimationController tries to animate widget with this max capable refresh rate. So update() is now called 120 times per second, and everything is done twice faster. I want to make data handler able to get maximum refresh rate, so it can synchronize player bar moving with device's screen. If device's max refresh rate is 60fps, then handler will make bar move X/60 per call, and if device can support 120fps, then it will make bar move X/120 per call, and so on.
To achieve this, I need to pass device's display's max capable refresh rate to data handler. I tried to google it, but I couldn't find any helpful posts or codes which tells the way to get this info.
Is there any way to get maximum refresh rate in flutter?
Or are there any plugins which can provide such info?
Edit:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:test_app/util/core/TestPainter.dart';
import 'package:test_app/util/core/cubit/TestData.dart';
import 'package:test_app/widget/EditPanel.dart';
import 'package:test_app/widget/Editor.dart';
class EditorPage extends StatefulWidget {
EditorPage({required this.sh, required this.sw});
final double sh;
final double sw;
#override
EditorStatus createState() => EditorStatus();
}
class EditorStatus extends State<EditorPage> with TickerProviderStateMixin {
late AnimationController _controller;
late TestPainter painter;
late TestData data;
#override
void initState() {
_controller = AnimationController(vsync: this, duration: Duration(seconds: 1));
_controller.repeat();
data = TestData(widget.sh * 0.85, widget.sw, _controller);
painter = TestPainter(data: data, animated: _controller);
super.initState();
}
#override
Widget build(BuildContext context) {
return BlocProvider<TestData>.value(
value: data,
child: Scaffold(
body: FractionallySizedBox(
widthFactor: 1,
heightFactor: 1,
child: SafeArea(
child: Stack(
children: [
Positioned(
top: widget.sh * 0.15,
bottom: 0,
left: 0,
right: 0,
child: Editor(
sh: widget.sh,
sw: widget.sw,
painter: painter,
),
),
Positioned(
top: widget.sh * 0.15 / 3,
left: 0,
right: 0,
height: widget.sh * 0.1,
child: EditPanel(widget.sw, widget.sh * 0.1),
),
],
),
),
)),
);
}
}
So basically widget called Editor and EditPanel require data handler called TestData, and painter called TestPainter. TestData requires _controller to manipulate TestPainter's animating status by calling repeat() and stop(). By status of widget (whether user is touching/scaling/long touching/etc widget or not, or whether buttons in EditPanel is clicked or not), _controller animates widget or stops animating it. TestPainter requires TestData as variable (I know now TestPainter can access _controller, but as long as I make TestPainter not touch _controller, it should be fine?), and it updates data whenever paint method is called. Even though data updating code is called, data's result differs from widget's status
For example,
return IconButton(
splashRadius: sh * 0.75 / 3,
icon: Icon(Icons.play_arrow, color: ThemeManager.getRGBfromHex(0x77d478),),
onPressed: () {
if (!data.playing) {
data.setStatus(Status.playing);
}
},
);
In EditPanel, this play button calles data, which is TestData, and in TestData,
void setStatus(Status st) {
if(st == Status.idle) {
pause();
stopVelocity();
current = st;
if(!blockAnimating) {
controller.stop();
}
} else if(st == Status.playing) {
play();
controller.repeat();
current = st;
} else if(st == Status.sliding) {
current = st;
controller.repeat();
} else if(st == Status.reversing) {
reverse();
controller.repeat();
current = st;
} else if(st == Status.selecting) {
current = st;
controller.repeat();
}
if(blockAnimating) {
controller.repeat();
}
}
As you can see, if st is playing, it calls controller.repeat(), which is AnimationController stored in TestData.
So if player status is playing, _controller will repeat itself, resulting in calls of paint method in TestPainter. Then TestPainer calls _handlePlaying() by calling updateData(Size s) method in paint, so animation with data updating can be done with AnimationController. Whenever paint is called, data gets updated, so each paint will result in different appearance (or may be not depending on status of player. If player is on idle, then it will draw same. Actually, it won't draw at all because controller.stop() will be called).
void updateData(Size s) {
sh = s.height;
sw = s.width;
if(current == Status.playing) {
_handlePlaying();
} else if(current == Status.selecting) {
updateSelectMode();
} else if(current == Status.sliding) {
performVelocity();
} else if(current == Status.reversing) {
_handleReverse();
}
_handleBlockAnimation();
}
void _handlePlaying() {
if(playPos == ((_lastBlock?.row ?? 0) + 48) * blockSize) {
setStatus(Status.idle);
return;
}
double upf = tick * blockSize / frames; //Here
playPos += upf;
if(playPos - posX >= sw - sh * 0.075) {
posX += upf;
}
updatePlayer();
if(posX > endPosX) {
posX = endPosX;
}
if(playPos > (( _lastBlock?.row ?? 0) + 48) * blockSize) {
playPos = ((_lastBlock?.row ?? 0) + 48) * blockSize;
}
}
playPos is player bar's position, and upf (unit per frame) is amount of x-pos which bar has to move per frame. So in this code X will be tick * blockSize, and frames is 60 initially. By tweaking this frames variable, I can make players move slower or faster per frame. If I set frames to 120 on device whose display can support 120 fps, then player is synced with real-time, but on 60fps display, player gets twice slower. On opposite, if I set frames to 60, then player is synced with real-time, and on 120 fps device, player gets twice faster.
I know this synchronization will be broken if fps goes lower than 60. I'm aware of this. I checked that my widget can be run with 60 fps with debug mode with slow device, so it should run faster on release mode in future. Let's skip this.
#override
void paint(Canvas cv, Size s) {
//Somewhere of codes in TestPainter
data.updateData(s);
//Keep going
}
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 design a simple game in which the ball hits the boxes and the user has to try to bring the ball up with the cursor.
When the ball returns, end of ball movement, is the offset at the bottom of the screen, and I want to reset the animation if the ball offset equals the cursor and then give it a new direction, but that never happens.
Please see the values I have printed.
532.0 is cursor.position.dy and others are positionBall.dy + renderBall.size.height.
Why only when the ball moves up (the moment I tap on the screen) the ball offset and the cursor offset are equal, but not in return?
---update---
When I increase the duration (for example, 10 seconds), or activate the Slow Animations button from the flutter inspector, the numbers get closer to each other, and by adjusting them to the int, the condition is made.
I/flutter (21563): 532.0
I/flutter (21563): 532.45585
I'm really confused and I do not know what is going on in the background.
void initState() {
super.initState();
Offset init = initialBallPosition();
final g = Provider.of<GameStatus>(context, listen: false);
var key = ball.key;
_animationController = AnimationController(duration: Duration(seconds: 1), vsync: this);
_tweenOffset = Tween<Offset>(begin: init, end: init);
_animationOffset = _tweenOffset.animate(
CurvedAnimation(parent: _animationController, curve: Curves.linear),
)..addListener(() {
if (_animationController.isAnimating) {
//if (_animationController.status == AnimationStatus.forward) {
RenderBox renderBall = key.currentContext.findRenderObject();
final positionBall = renderBall.localToGlobal(Offset.zero);
print(cursor.position.dy);
print(positionBall.dy + renderBall.size.height);
if (positionBall.dy + renderBall.size.height == cursor.position.dy && g.ballDirection == 270) {
print('bang');
colideWithCursor();
}
}
if (_animationController.status == AnimationStatus.completed) {
if (bottomOfBall().dy == Screen.screenHeight / ball.width) {
gameOver();
} else
collision();
}
});
_animationController.isDismissed;
}
#override
Widget build(BuildContext context) {
final game = Provider.of<GameStatus>(context, listen: false);
return Selector<GameStatus, bool>(
selector: (ctx, game) => game.firstShoot,
builder: (context, startGame, child) {
if (startGame) {
game.ballDirection = 90;
routing(game.ballDirection);
}
return UnconstrainedBox(child: (SlideTransition(position: _animationOffset, child: ball.createBall())));
});
}
The two numbers are never exactly matching because the animation value is checked every frame and the overlap is occurring between frames.
You probably either want to add a tolerance (eg consider the values to have matched if they're within a certain amount) or create some interpolation logic where you check if the ball is about to collide with the cursor in-between the current frame and the next. eg replace:
positionBall.dy + renderBall.size.height == cursor.position.dy && g.ballDirection == 270
With:
positionBall.dy + renderBall.size.height + <current_speed_per_frame_of_ball> <= cursor.position.dy && g.ballDirection == 270
The important thing here is that the animations aren't actually fluid. An animation doesn't pass from 0.0 continuously through every conceivable value to 1.0. The value of the animation is only calculated when a frame is rendered so the values you'll actually get might be something along the lines of: 0.0, 0.14, 0.30, 0.44, 0.58....0.86, 0.99, 1.0. The exact values will depend on the duration of the animation and the exact times the Flutter framework renders each frame.
Since you asked (in the comments) for an example using onTick, here's an example app I wrote up for a ball that bounces randomly around the screen. You can tap to randomize it's direction and speed. Right now it kinda hurts your eyes because it's redrawing the ball in a new position on every frame.
You'd probably want to smoothly animate the ball between each change in direction (eg replace Positioned with AnimatedPositioned) to get rid of the eye-strain. This refactor is beyond what I have time to do.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:vector_math/vector_math.dart' hide Colors;
Random _rng = Random();
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
get randomizedDirection =>
_randomDirectionWithVelocity((150 + _rng.nextInt(600)).toDouble());
Ticker _ticker;
Vector2 _initialDirection;
Duration prevT = Duration.zero;
BallModel _ballModel;
#override
void dispose() {
super.dispose();
_ticker.dispose();
}
void _init(Size size) {
_ballModel = BallModel(
Vector2(size.width / 2, size.height / 2),
randomizedDirection,
16.0,
);
_ticker = createTicker((t) {
// This sets state and forces a rebuild on every frame. A good optimization would be
// to only build when the ball changes direction and use AnimatedPositioned to fluidly
// draw the ball between changes in direction.
setState(() {
_ballModel.updateBall(t - prevT, size);
});
prevT = t;
});
_ticker.start();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: GestureDetector(
child: Scaffold(
body: LayoutBuilder(
builder: (context, constraints) {
// Initialize everything here because we need to access the constraints.
if (_ticker == null) _init(constraints.biggest);
return Stack(children: [
Ball(_ballModel),
]);
},
),
),
onTap: () => setState(() => _ballModel.v = randomizedDirection),
),
);
}
}
class BallModel {
// The current x,y position of the ball.
Vector2 p;
// The direction, including speed in pixels per second, of the ball
Vector2 v;
// The radius of the ball.
double r;
BallModel(this.p, this.v, this.r);
void updateBall(Duration elapsed, Size size) {
// Move the ball by v, scaled by what fraction of a second has passed
// since the last frame.
p = p + v * (elapsed.inMilliseconds / 1000);
// If the ball overflows on a given dimension, correct the overflow and update v.
var newX = _correctOverflow(p.x, r, 0, size.width);
var newY = _correctOverflow(p.y, r, 0, size.height);
if (newX != p.x) v.x = -v.x;
if (newY != p.y) v.y = -v.y;
p = Vector2(newX, newY);
}
}
class Ball extends StatelessWidget {
final BallModel b;
Ball(this.b);
#override
Widget build(BuildContext context) {
return Positioned(
left: b.p.x - b.r,
bottom: b.p.y - b.r,
child: DecoratedBox(
decoration:
BoxDecoration(shape: BoxShape.circle, color: Colors.black)),
width: 2 * b.r,
height: 2 * b.r);
}
}
double _correctOverflow(s, r, lowerBound, upperBound) {
var underflow = s - r - lowerBound;
// Reflect s across lowerBound.
if (underflow < 0) return s - 2 * underflow;
var overflow = s + r - upperBound;
// Reflect s across upper bound.
if (overflow > 0) return s - 2 * overflow;
// No over or underflow, return s.
return s;
}
Vector2 _randomDirectionWithVelocity(double velocity) {
return Vector2(_rng.nextDouble() - .5, _rng.nextDouble() - 0.5).normalized() *
velocity;
}
Writing game and physics logic from scratch gets really complicated really fast. I encourage you to use a game engine like Unity so that you don't have to build everything yourself. There's also a Flutter based game engine called flame that you could try out:
https://github.com/flame-engine/flame.