How to detect drag velocity in flutter? - 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

Related

Why are Flutter CustomPainter Class Variables not Persisting

In Flutter, is a Custom Painter and all its class variables re-constructed from scratch every time the paint method is called? -- when setState is called in the parent?? I didn't expect that, but it seems to be the case:
I ask because I have a Custom Painter that contains an object inside the paint method (a sequence of points) that is the basis of all the subsequent painting effects. But... this object takes math in the first place just to be created... and it requires the canvas dimensions as part of that creation... which is why its inside the paint method.
So... I thought... instead of calculating the same exact skeleton shape every time paint is called, I'll make it a nullable class variable and initialize it once... then just check if its null every time in paint instead of recreating it every time.
I thought this was best practice to "move calculations out of the paint method when possible."
BUT... the unexpected result is that when I check, Flutter always says my object is null (it's recreated every time anyway).
Custom Painter:
import 'package:flutter/material.dart';
import 'dart:math';
class StackPainter02 extends CustomPainter {
final List<double> brightnessValues;
Paint backgroundPaint = Paint();
Paint segmentPaint = Paint();
List<Offset>? myShape;
StackPainter02({
required this.brightnessValues,
}) {
backgroundPaint.color = Colors.black;
backgroundPaint.style = PaintingStyle.fill;
segmentPaint.style = PaintingStyle.stroke;
}
#override
void paint(Canvas canvas, Size size) {
final W = size.width;
final H = size.height;
segmentPaint.strokeWidth = W / 100;
canvas.drawPaint(backgroundPaint);
// unfortunately, we must initialize this here because we need the view dimensions
if (myShape == null) {
myShape = _myShapePoints(brightnessValues.length, 0.8, W, H, Offset(W/2,H/2));
}
for (int i = 0; i<myShape!.length; i++) {
// bug fix... problem: using "i+1" results in index out-of-range on wrap-around
int modifiedIndexPlusOne = i;
if (modifiedIndexPlusOne == myShape!.length-1) {
modifiedIndexPlusOne = 0;
} else {
modifiedIndexPlusOne++;
}
// draw from point i to point i+1
Offset segmentStart = myShape![i];
Offset segmentEnd = myShape![modifiedIndexPlusOne];
double b = brightnessValues[i];
if (b < 0) {
b = 0; // !!!- temp debug... problem: brightness array algorithm is not perfect
}
int segmentAlpha = (255*b).toInt();
segmentPaint.color = Color.fromARGB(segmentAlpha, 255, 255, 200);
canvas.drawLine(segmentStart, segmentEnd, segmentPaint);
}
}
#override
bool shouldRepaint(covariant StackPainter02 oldDelegate) {
//return (oldDelegate.brightnessValues[32] != brightnessValues[32]); // nevermind
return true;
}
}
Build Method in Parent:
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: CustomPaint(
painter: //MyCustomPainter(val1: val1, val2: val2),
StackPainter02(
brightnessValues: brightnessValues,
),
size: Size.infinite,
),
),
);
}
PS - A ticker is used in parent and the "brightnessValues" are recalculated on every Ticker tick -> setState

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.

Can Flutter's inbuilt Canvas drawing methods be directly used to render variable-width strokes?

Can Flutter's inbuilt Canvas drawing methods be directly used to render variable-width strokes, for example to reflect pressure applied throughout each stroke in a handwriting app?
Ideally, in a manner compatible with saving in the XML-esque format of SVG (example at bottom).
What I think I've noticed / troubles I'm having / current attempts:
canvas.drawPath, canvas.drawPoints, canvas.drawPolygon, canvas.drawLines etc all take only a single Paint object, which can in turn have a single strokeWidth (as opposed to taking lists of Paint objects or strokeWidths, such that parameters of the path besides position could change point to point and be interpolated between).
Drawing lines, polygons or points of varying strokeWidths or radii by iterating over lists of position and pressure data and using the respective Canvas method results in no interpolation / paths not looking continuously stroked.
Screenshot from OneNote showing the behaviour I'd like:
Screenshot from the app the minimal working example below produces:
(Unoptimized) minimal working example:
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(Container(
color: Colors.white,
child: Writeable(),
));
}
class Writeable extends StatefulWidget {
#override
_WriteableState createState() => _WriteableState();
}
class _WriteableState extends State<Writeable> {
List<List<double>> pressures = List<List<double>>();
List<Offset> currentLine = List<Offset>();
List<List<Offset>> lines = List<List<Offset>>();
List<double> currentLinePressures = List<double>();
double pressure;
Offset position;
Color color = Colors.black;
Painter painter;
CustomPaint paintCanvas;
#override
Widget build(BuildContext context) {
painter = Painter(
lines: lines,
currentLine: currentLine,
pressures: pressures,
currentLinePressures: currentLinePressures,
color: color);
paintCanvas = CustomPaint(
painter: painter,
);
return Listener(
onPointerMove: (details) {
setState(() {
currentLinePressures.add(details.pressure);
currentLine.add(details.localPosition);
});
},
onPointerUp: (details) {
setState(() {
lines.add(currentLine.toList());
pressures.add(currentLinePressures.toList());
currentLine.clear();
currentLinePressures.clear();
});
},
child: paintCanvas,
);
}
}
class Painter extends CustomPainter {
Painter(
{#required this.lines,
#required this.currentLine,
#required this.color,
#required this.pressures,
#required this.currentLinePressures});
final List<List<Offset>> lines;
final List<Offset> currentLine;
final Color color;
final List<List<double>> pressures;
final List<double> currentLinePressures;
double scalePressures = 10;
Paint paintStyle = Paint();
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
// Paints here using drawPoints and PointMode.lines, but have also tried
// PointMode.points, PointMode.polygon and drawPath with a path variable and
// moveTo, lineTo methods.
#override
void paint(Canvas canvas, Size size) {
// Paint line currently being drawn (points added since pointer was
// last lifted)
for (int i = 0; i < currentLine.length - 1; i++) {
paintStyle.strokeWidth = currentLinePressures[i] * scalePressures;
canvas.drawPoints(
PointMode.lines, [currentLine[i], currentLine[i + 1]], paintStyle);
}
// Paint all completed lines drawn since app start
for (int i = 0; i < lines.length; i++) {
for (int j = 0; j < lines[i].length - 1; j++) {
paintStyle.strokeWidth = pressures[i][j] * scalePressures;
canvas.drawPoints(
PointMode.lines, [lines[i][j], lines[i][j + 1]], paintStyle);
}
}
}
}
I'm about to try writing my own implementation for rendering aesthetic SVG-friendly data from PointerEvents, but so many of the existing classes feel SVG/pretty-vectors-compatible (e.g. all the lineTos, moveTos, stroke types and endings and other parameters) that I thought it worth checking if there's something I've missed, and these methods can already do this?
Example of a few lines in an SVG file saved by Xournal++, with the stroke-width parameter changing for each line segment, and all other listed parameters presumably also having potential to change. Each line contains a moveTo command (M) and a lineTo command (L), where the latter draws a line from the current position (the last moveTo-ed or lineTo-ed), reminiscent of Flutter's segments/sub-paths and current point, to the specified offset:
<path style="fill:none;stroke-width:0.288794;stroke-linecap:round;stroke-linejoin:round;stroke:rgb(0%,100%,0%);stroke-opacity:1;comp-op:src;clip-to-self:true;stroke-miterlimit:10;" d="M 242.683594 45.519531 L 242.980469 45.476562 "/>
<path style="fill:none;stroke-width:0.295785;stroke-linecap:round;stroke-linejoin:round;stroke:rgb(0%,100%,0%);stroke-opacity:1;comp-op:src;clip-to-self:true;stroke-miterlimit:10;" d="M 242.980469 45.476562 L 243.28125 45.308594 "/>
<path style="fill:none;stroke-width:0.309105;stroke-linecap:round;stroke-linejoin:round;stroke:rgb(0%,100%,0%);stroke-opacity:1;comp-op:src;clip-to-self:true;stroke-miterlimit:10;" d="M 243.28125 45.308594 L 243.601562 45.15625 "/>
The approach seems to be 'draw a very short line, change stroke-width, draw the next very short line starting from the previous position', which I've tried to emulate with the paint method above.

Performance issue in drawing using Path Flutter

I am testing the performance in drawing using Flutter. I am using Path to draw line between each point detected by Listener because I have read that the performance would increase using it. I am using Listener because I tried also the Apple Pencil on iPad 2017 by changing the kind property to stylus.
The problem is that I was hoping to get a response in the stroke design similar to notability, it seems much slower, acceptable but not as much as I would like.
So I'm looking for tips to increase performance in terms of speed.
At the following link they recommended using NotifyListener(), but I didn't understand how to proceed. If it really improves performance I would like even an example to be able to implement it.
If Flutter has some limitations when it comes to drawing with your fingers or with a stylus then let me know.
performance issue in drawing using flutter
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
class DrawWidget extends StatefulWidget {
#override
_DrawWidgetState createState() => _DrawWidgetState();
}
class _DrawWidgetState extends State<DrawWidget> {
Color selectedColor = Colors.black;
double strokeWidth = 3.0;
List<MapEntry<Path, Object>> pathList = List();
StrokeCap strokeCap = (Platform.isAndroid) ? StrokeCap.butt : StrokeCap.round;
double opacity = 1.0;
Paint pa = Paint();
#override
Widget build(BuildContext context) {
return Listener(
child: CustomPaint(
size: Size.infinite,
painter: DrawingPainter(
pathList: this.pathList,
),
),
onPointerDown: (details) {
if (details.kind == PointerDeviceKind.touch) {
print('down');
setState(() {
Path p = Path();
p.moveTo(details.localPosition.dx, details.localPosition.dy);
pa.strokeCap = strokeCap;
pa.isAntiAlias = true;
pa.color = selectedColor.withOpacity(opacity);
pa.strokeWidth = strokeWidth;
pa.style = PaintingStyle.stroke;
var drawObj = MapEntry<Path,Paint>(p, pa);
pathList.add(drawObj);
});
}
},
onPointerMove: (details) {
if (details.kind == PointerDeviceKind.touch) {
print('move');
setState(() {
pathList.last.key.lineTo(details.localPosition.dx, details.localPosition.dy);
});
}
},
/*onPointerUp: (details) {
setState(() {
});
},*/
);
}
}
class DrawingPainter extends CustomPainter {
DrawingPainter({this.pathList});
List<MapEntry<Path, Object>> pathList;
#override
void paint(Canvas canvas, Size size) {
for(MapEntry<Path, Paint> m in pathList) {
canvas.drawPath(m.key, m.value);
}
}
#override
bool shouldRepaint(DrawingPainter oldDelegate) => true;
}
I think you should not use setState, rather use state management like Bloc or ChangeNotifier or smth.
Also, just drawing a path with this:
canvas.drawPath(m.key, m.value);
Works for only small stroke widths, it leaves a weird-looking line full of blank spaces when drawing.
I implemented this by using Bloc that updates the UI based on the gesture functions (onPanStart, onPanEnd, onPanUpdate). It holds a List of a data model that I called CanvasPath that represents one line (so from onPanStart to onPanEnd events), and it holds the resulting Path of that line, list of Offsets and Paint used to paint it.
In the end paint() method draws every single Path from this CanvasPath object and also a circle in every Offset.
` for every canvasPath do this:
canvas.drawPath(canvasPath.path, _paint);
for (int i = 0; i < canvasPath.drawPoints.length - 1; i++) {
//draw Circle on every Offset of user interaction
canvas.drawCircle(
canvasPath.drawPoints[i],
_raidus,
_paint);
}`
I made a blog about this, where it is explained in much more details:
https://ivanstajcer.medium.com/flutter-drawing-erasing-and-undo-with-custompainter-6d00fec2bbc2

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,
);
}
}