How to get device's maximum refresh rate in flutter? - flutter

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
}

Related

What's the best way to make a spinning wheel?

I'm trying to make a wheel that can spin when a user drags up and down on a screen. It's essentially an infinite vertical scroll. So far I can make it turn while actually scrolling, but I'd like to incorporate physics to make it keep spinning when you let go. At the moment I'm using a GestureDetector to put an angle into Provider, which is used to transform some child widgets that make up the wheel, like so:
GestureDetector(
onVerticalDragUpdate: (offset) {
provider.wheelAngle += atan(offset.delta.dy / wheelRadius);
},
);
I'm sure I can do the physics part manually by handling the onVerticalDragEnd, but given that this is essentially just scrolling, I was wondering if it would make more sense to somehow leverage Flutter's built in scrolling stuff - maybe ScrollPhysics or one of the classes that derive from it. I don't want to reinvent the wheel (no pun intended), but I also don't want extra complexity by trying to force something else into doing what I need if it isn't a good fit. I can't quite wrap my head around ScrollPhysics, so I feel like it might be going down the route of over-complicated. Any gut feelings on what the best technique would be?
As pskink mentioned in the comments, animateWith is the key. In case it helps anyone in the future, here's an untested, slimmed-down version of what I ended up with. It switches between using FrictionSimulation when spinning freely and SpringSimulation when snapping to a particular angle.
class Wheel extends StatefulWidget {
const Wheel({Key? key}) : super(key: key);
#override
State<Wheel> createState() => _WheelState();
}
class _WheelState extends State<Wheel> with SingleTickerProviderStateMixin {
late AnimationController _wheelAnimationController;
bool _isSnapping = false;
double _radius = 0.0; // Probably set this in the constructor.
static const double velocitySnapThreshold = 1.0;
static const double distanceSnapThreshold = 0.25;
#override
Widget build(BuildContext context) {
var provider = context.read<WheelAngleProvider>();
_wheelAnimationController = AnimationController.unbounded(vsync: this, value: provider.wheelAngle);
_wheelAnimationController.addListener(() {
if (!_isSnapping) {
// Snap to an item if not spinning quickly.
var wheelAngle = _wheelAnimationController.value;
var velocity = _wheelAnimationController.velocity.abs();
var closestSnapAngle = getClosestSnapAngle(wheelAngle);
var distance = (closestSnapAngle - wheelAngle).abs();
if (velocity == 0 || (velocity < velocitySnapThreshold && distance < distanceSnapThreshold)) {
snapTo(closestSnapAngle);
}
}
provider.wheelAngle = _wheelAnimationController.value;
});
return Stack(
children: [
// ... <-- Visible things go here
// Vertical dragging anywhere on the screen rotates the wheel, hence the SafeArea.
SafeArea(
child: GestureDetector(
onVerticalDragDown: (details) {
_wheelAnimationController.stop();
_isSnapping = false;
},
onVerticalDragUpdate: (offset) =>
provider.wheelAngle = provider.wheelAngle + atan(offset.delta.dy / _radius),
onVerticalDragEnd: (details) => onRotationEnd(provider, details.primaryVelocity),
),
),
],
);
}
double getClosestSnapAngle(double currentAngle) {
// Do what you gotta do here.
return 0.0;
}
void snapTo(double snapAngle) {
var wheelAngle = _wheelAnimationController.value;
_wheelAnimationController.stop();
_isSnapping = true;
var springSimulation = SpringSimulation(
SpringDescription(mass: 20.0, stiffness: 10.0, damping: 1.0),
wheelAngle,
snapAngle,
_wheelAnimationController.velocity,
);
_wheelAnimationController.animateWith(springSimulation);
}
void onRotationEnd(WheelAngleProvider provider, double? velocity) {
// When velocity is not null, this is the result of a fling and it needs to spin freely.
if (velocity != null) {
_wheelAnimationController.stop();
var frictionSimulation = FrictionSimulation(0.5, provider.wheelAngle, velocity / 200);
_wheelAnimationController.animateWith(frictionSimulation);
}
}
}

How to reset an animation in specific offset, when animation status is forwarding?

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.

How to detect drag velocity in flutter?

How to detect drag velocity in flutter ?
I want to draw on screen in flutter using custom paint, when the velocity is less the stroke width should be less, but when the drag velocity is high the stroke width should be greater.
GestureDetector(
onPanUpdate: (DragUpdateDetails details) {
setState(
() {
RenderBox object = context.findRenderObject();
Offset _localPosition =
object.globalToLocal(details.globalPosition);
_points = List.from(_points)..add(_localPosition);
},
);
},
onPanEnd: (DragEndDetails details) => {
_deletedPoints.clear(),
_points.add(null),
// _listPoints.add(_points),
// _listPoints = List.from(_listPoints)..add(_points),
},
child: CustomPaint(
painter: Draw(points: _points),
size: Size.infinite,
),
),
The custom draw widget that extends customPainter
class Draw extends CustomPainter {
List<Offset> points;
// List<List<Offset>> points;
Draw({this.points});
#override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = brushColor
..strokeCap = StrokeCap.round
..strokeWidth = brushWidth;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != null && points[i + 1] != null) {
canvas.drawLine(points[i], points[i + 1], paint);
}
}
}
#override
bool shouldRepaint(Draw oldDelegate) => oldDelegate.points != points;
}
velocity is how much the position changed in a given time.
for this purpose you have the details.delta.distance in the onPanUpdate callback, which returns a double indicating how much the pointer has moved since last update, the bigger it is, the larger the velocity.
in your case, you can change the stroke width based on the distance traveled.
I haven't used any of these myself but you could look into
DragUpdateDetails. details in onPanUpdate has an Offset object called delta. Which are coordinates which update every time onPanUpdate is called.
https://api.flutter.dev/flutter/gestures/DragUpdateDetails-class.html
There's also a class called VelocityTracker
https://api.flutter.dev/flutter/gestures/VelocityTracker-class.html
Hope this helps you a bit
[Update] a practical example
late final Ticker _velocityTicker;
final List<int> _velocityRecs = List.filled(6, 0, growable: true);
double _rec = 0;
get _deltaSum {
var sum = 0;
for (var i = 1; i < _velocityRecs.length; i++) {
sum += (_velocityRecs[i] - _velocityRecs[i - 1]).abs();
}
return sum;
}
/// in initState()
_velocityTicker = Ticker((duration) {
_velocityRecs.removeAt(0);
// add whatever values you want to track
_velocityRecs.add(_currentIndex);
// You get a 'velocity' here, do something you want
if (_deltaSum > 2) {
// _offsetYController.forward();
} else if (_deltaSum < 1) {
// _offsetYController.reverse();
}
});
/// then .start()/.stop() the ticker in your event callback
/// Since each ticker has a similar time interval,
/// for Simplicity, I did not use duration here,
/// if you need a more accurate calculation value,
/// you may need to use it
In short, You can't.
Although details.delta and VelocityTracker are mentioned above, neither of them technically has access to the user's PointerEvent velocity,
For example, if the user drags the slider and stops, but the finger does not leave the screen, the velocity should be about 0, but this state can't be captured in flutter.
velocityTracker doesn't help
If you want to implement it yourself, one idea is to create a Timer/Ticker, record the last value and put it into a list, then set it to zero, and each time you want get, the sum of the whole list will be averaged to get the velocity, you need to be careful to determine how much the length of your List is appropriate

Persistent Ticker in Flutter

How to get a persistent tick at every frame refresh time. For example in Flame game engine update method gets called at around every 1/60 seconds and a value dt with elapsed time is passed.
I want to implement one simple animation where a fan will rotate. I want to change its rotation speed depending on user input. My idea is that at every tick I will rotate the fan image/ container at a fixed value. As the user increases the speed I will increase the multiplier. There are few options like using the Flame engine or Flare, but they seem overkill. Also, I can use SingleTickerProviderMixin but there are few overheads like reverse the animation when finished and forwarded it and so...
I think there will be a simple solution, which will notify me at each frame refresh time that occurs at around every 1/60 seconds, and pass me the elapsed time dt (around 167 mS or so).
A nice way to do it (without Animation widgets), is to implement a Timer with a Stream; see the example below:
import 'package:flutter/material.dart';
import "dart:async";
const frequency = Duration(milliseconds: 50);
void main() => runApp(
MaterialApp(
home: Material(
child: Center(
child: Container(
color: Colors.white,
child: MyWidget(),
),
),
),
),
);
class MyWidget extends StatefulWidget {
MyWidgetState createState() => MyWidgetState();
}
class MyWidgetState extends State<MyWidget> {
final StreamController<double> _streamer =
StreamController<double>.broadcast();
Timer timer;
double _rotation = 0.0;
#override
void initState() {
super.initState();
timer = Timer.periodic(frequency, (t) {
_rotation++;
_streamer.add(1);
});
}
#override
Widget build(BuildContext context) {
return StreamBuilder<double>(
initialData: 0,
stream: _streamer.stream,
builder: (context, snapshot) {
return Transform(
transform: Matrix4.rotationZ(_rotation),
child: Text('Hello, World!'),
);
});
}
}
I would also make sure to implement the dispose() callback if you copy this code. You need to make sure to cancel() any running timers to prevent odd behaviors or they will become a source of memory leaks.
The timer = null; is not always needed, but there are situations where the state object will hold a reference to the timer var itself and also cause a memory leak. For example, if you capture the timer var inside the timer callback body.
Example:
#override
void dispose() {
timer?.cancel();
timer = null;
super.dispose();
}

Find out which items in a ListView are visible

How can I find out which items are currently visible or invisible in a ListView?
For example, I have 100 items in ListView and when i scroll to top of screen or list, I want to detect which items appear or disappear from the viewport.
Illustration:
There is no easy way to do this. Here is the same question, however, it does not have an answer.
There is an active GitHub issue about this.
There are multiple solutions for the problem in that issue. This Gist features one that requires the rect_getter package.
Alternatively, you could take a look at this proposal.
TL;DR
This is not yet implemented if you are searching for an easy way to find it out. However, there are solutions, like the ones I mentioned above and from other packages, say VisibilityDetector from flutter_widgets.
You can also use inview_notifier_list. It's basically a normal ListView which defines a visible region and it's children get notified when they are in that region.
There is a package for this purpose.
A VisibilityDetector widget wraps an existing Flutter widget and fires a callback when the widget's visibility changes.
Usage:
VisibilityDetector(
key: Key('my-widget-key'),
onVisibilityChanged: (visibilityInfo) {
var visiblePercentage = visibilityInfo.visibleFraction * 100;
debugPrint(
'Widget ${visibilityInfo.key} is ${visiblePercentage}% visible');
},
child: someOtherWidget,
)
I'm Sharing for visibility on how to approach detecting position of widget in general.
I was curious as to how you access positional data of widgets, and also wanted to be able to control the animated state of a ListView child element.
Looks like the main point of access to a widgets, size and position is via the BuildContext's context.findRenderObject()
However, this is only usable after the component has been built and the widget is mounted.
This is addressed by using context.findRenderObject() in a function called using WidgetsBinding.instance.addPostFrameCallback((_) => calculatePosition(context));
Here's a wrapper component you can use in your ListView.itemBuilder() code
import 'package:flutter/cupertino.dart';
import 'dart:developer' as developer;
enum POCInViewDirection { up, down, static }
class POCInView extends StatefulWidget {
final Widget child;
final double scrollHeight;
const POCInView({super.key, required this.child, required this.scrollHeight});
#override
POCInState createState() => POCInState();
}
class POCInState extends State<POCInView> {
bool inView = false; // are you in view or not.
double lastPositionY = 0; // used to determine which direction your widget is moving.
POCInViewDirection direction = POCInViewDirection.static; // Set based on direction your moving.
RenderBox? renderBoxRef;
bool skip = true;
#override
void initState() {
super.initState();
developer.log('InitState', name: 'POCInView');
lastPositionY = 0;
renderBoxRef = null;
direction = POCInViewDirection.static;
skip = true;
}
/// Calculate if this widget is in view.
/// uses BuildContext.findRenderObject() to get the RenderBox.
/// RenderBox has localToGlobal which will give you the objects offset(position)
/// Do some math to workout if you object is in view.
/// i.e. take into account widget height and position.
///
/// I only do Y coordinates.
///
void calculatePosition(BuildContext context) {
// findRenderObject() will fail if the widget has been unmounted. so leave if not mounted.
if (!mounted) {
renderBoxRef = null;
return;
}
// It says this can be quite expensive as it will hunt through the view tree to find a RenderBox.
// probably worth timing or seeing if its too much for you view.
// I've put a rough cache in, deleting the ref when its unmounted. mmmmm.
renderBoxRef ??= context.findRenderObject() as RenderBox;
//
inView = false;
if (renderBoxRef is RenderBox) {
Offset childOffset = renderBoxRef!.localToGlobal(Offset.zero);
final double y = childOffset.dy;
final double componentHeight = context.size!.height;
final double screenHeight = widget.scrollHeight;
if (y < screenHeight) {
if (y + componentHeight < -20) {
inView = false;
} else {
inView = true;
}
} else {
inView = false;
}
// work out which direction we're moving. Not quite working right yet.
direction = y > lastPositionY ? POCInViewDirection.down : POCInViewDirection.up;
lastPositionY = y;
//developer.log('In View: $inView, childOffset: ${childOffset.dy.toString()}', name: 'POCInView');
}
skip = false;
}
#override
Widget build(BuildContext context) {
// calculate position after build is complete. this is required to use context.findRenderObject().
WidgetsBinding.instance.addPostFrameCallback((_) => calculatePosition(context));
// fade in when in view.
final oChild = AnimatedOpacity(opacity: inView ? 1 : 0, duration: const Duration(seconds: 1), child: widget.child);
// slide in when in view, and adjust slide direction based on scroll direction.
return AnimatedSlide(
duration: Duration(seconds: inView ? 1 : 0),
offset: Offset(0, inView ? 0.0 : 0.25 * (skip == true ? 0 : (direction == POCInViewDirection.up ? 1 : -1))),
child: oChild,
);
}
}