Find out which items in a ListView are visible - flutter

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

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

Flutter - Prevent custom draggable widget from being affected by a keyboard showing and hiding

I've created a custom draggable widget in Flutter, for my app, which can be used anywhere, simply by using a stack and adding the widget on top of that stack. This is the code:
import 'package:flutter/material.dart';
class DraggableWidget extends StatefulWidget {
final Widget child;
final Offset initialOffset;
final VoidCallback onPressed;
final GlobalKey parentKey;
const DraggableWidget({
Key? key,
required this.child,
required this.initialOffset,
required this.onPressed,
required this.parentKey,
}) : super(key: key);
#override
_DraggableWidgetState createState() => _DraggableWidgetState();
}
class _DraggableWidgetState extends State<DraggableWidget> {
final GlobalKey _key = GlobalKey();
bool _isDragging = false;
late Offset _offset;
late Offset _minOffset;
late Offset _maxOffset;
#override
void initState() {
super.initState();
_offset = widget.initialOffset;
WidgetsBinding.instance?.addPostFrameCallback(_setBoundary);
}
void _setBoundary(_) {
final RenderBox parentRenderBox =
widget.parentKey.currentContext?.findRenderObject() as RenderBox;
final RenderBox renderBox =
_key.currentContext?.findRenderObject() as RenderBox;
try {
final Size parentSize = parentRenderBox.size;
final Size size = renderBox.size;
setState(() {
_minOffset = const Offset(0, 0);
_maxOffset = Offset(
parentSize.width - size.width, parentSize.height - size.height);
});
} catch (e) {
print('catch: $e');
}
}
void _updatePosition(PointerMoveEvent pointerMoveEvent) {
double newOffsetX = _offset.dx - pointerMoveEvent.delta.dx;
double newOffsetY = _offset.dy - pointerMoveEvent.delta.dy;
if (newOffsetX < _minOffset.dx) {
newOffsetX = _minOffset.dx;
} else if (newOffsetX > _maxOffset.dx) {
newOffsetX = _maxOffset.dx;
}
if (newOffsetY < _minOffset.dy) {
newOffsetY = _minOffset.dy;
} else if (newOffsetY > _maxOffset.dy) {
newOffsetY = _maxOffset.dy;
}
setState(() {
_offset = Offset(newOffsetX, newOffsetY);
});
}
#override
Widget build(BuildContext context) {
return Positioned(
right: _offset.dx,
bottom: _offset.dy,
child: Listener(
onPointerMove: (PointerMoveEvent pointerMoveEvent) {
_updatePosition(pointerMoveEvent);
setState(() {
_isDragging = true;
});
},
onPointerUp: (PointerUpEvent pointerUpEvent) {
if (_isDragging) {
setState(() {
_isDragging = false;
});
} else {
widget.onPressed();
}
},
child: Container(
key: _key,
child: widget.child,
),
),
);
}
}
Now, this does work really well when the screen dimensions are fixed/do not change. However, I have noticed a bug, whereby if the keyboard slides up on a phone (for a texfield input) this shrinks the widget moveable area. Then, when the keyboard is removed, instead of the widget seeing the whole screen area again, it only sees an area equivalent to when the keyboard was out. That means that where before the widget could be dragged all over the screen, it can now only be dragged within the total area of the screen minus the area of the keyboard, even when the keyboard has been closed/removed, almost like there's an imaginary boundary.
Is there a way to prevent this from happening? Essentially, the draggable widget needs to move around the whole screen when the keyboard is closed and when the keyboard is open, it needs to move around the area of the screen minus the area of the keyboard. When the keyboard is closed again, it needs to move around the whole screen again.
Thanks in advance!
I noticed that it was actually happening because the draggable widget was being built before the preceding search delegate's keyboard had closed (this screen comprises a SingleChildScrollView widget with a number of modular stateful widgets as its children, one of which is a text button that calls a search delegate) so it took the height of the screen minus the keyboard height as the available height.
I don't know if this is the most elegant solution but I have managed to fix it by calling
final keyboardSize = MediaQuery.of(context).viewInsets.bottom;
in the build method, before returning the scaffold, which contained the stack (which itself contains the SingleChildScrollView and overlaid draggable widget).
Then, I made that keyboardSize parameter a required variable of the custom draggable widget and modified the _maxOffset parameter of the _setBoundary(_) function as follows:
_maxOffset = Offset(
parentSize.width - size.width,
parentSize.height - size.height + widget.keyboardSize,
);
This now seems to ensure that the draggable widget can be moved around the whole of the screen, not just the screen height minus the keyboard height.

Flutter: How to share an instance of statefull widget?

I have a "WidgetBackGround" statefullwidget that return an animated background for my app,
I use it like this :
Scaffold( resizeToAvoidBottomInset: false, body: WidgetBackGround( child: Container(),),)
The problem is when I use navigator to change screen and reuse WidgetBackGround an other instance is created and the animation is not a the same state that previous screen.
I want to have the same animated background on all my app, is it possible to instance it one time and then just reuse it ?
WidgetBackGround.dart look like this:
final Widget child;
WidgetBackGround({this.child = const SizedBox.expand()});
#override
_WidgetBackGroundState createState() => _WidgetBackGroundState();
}
class _WidgetBackGroundState extends State<WidgetBackGround> {
double iter = 0.0;
#override
void initState() {
Future.delayed(Duration(seconds: 1)).then((value) async {
for (int i = 0; i < 2000000; i++) {
setState(() {
iter = iter + 0.000001;
});
await Future.delayed(Duration(milliseconds: 50));
}
});
super.initState();
}
#override
Widget build(BuildContext context) {
return CustomPaint(painter: SpaceBackGround(iter), child: widget.child);
}
}
this is not a solution, but maybe a valid workaround:
try making the iter a static variable,
this of course won't preserve the state of WidgetBackGround but will let the animation continue from its last value in the previous screen
A valid solution (not sure if it's the best out there):
is to use some dependency injection tool (for example get_it) and provide your WidgetBackGround object as a singleton for every scaffold in your app

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

How to add image-manipulating route transitions to Flutter

I'm trying to create a custom route transition using Flutter. The existing route transitions (fade, scale, etc) are fine, but I want to create screen transitions that manipulate the screens' content by capturing their render and applying effects to it. Basically, I want to recreate DOOM screen melt effect as a route transition with Flutter.
It feels to me that its reliance on Skia and its own Canvas for rendering screen elements would make this possible, if not somewhat trivial. I haven't been able to do it, though. I can't seem to be able to capture the screen, or at least to render the target screen in chunks using clipping paths. A lot of this has to do with my lack of understanding of how Flutter composition works, so I'm still uncertain on which avenues to investigate.
My first approach was creating a custom transition by basically replicating what FadeTransition does.
Route createRouteWithTransitionCustom() {
return PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => ThirdScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return CustomTransition(
animation: animation,
child: child,
);
},
);
}
RaisedButton(
child: Text('Open Third screen (custom transition, custom code)'),
onPressed: () {
Navigator.push(context, createRouteWithTransitionCustom());
},
),
In this case, CustomTransition is a near exact duplicate of FadeTransition, with some light renaming (opacity becomes animation).
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'RenderAnimatedCustom.dart';
/// A custom transition to animate a widget.
/// This is a copy of FadeTransition: https://github.com/flutter/flutter/blob/27321ebbad/packages/flutter/lib/src/widgets/transitions.dart#L530
class CustomTransition extends SingleChildRenderObjectWidget {
const CustomTransition({
Key key,
#required this.animation,
this.alwaysIncludeSemantics = false,
Widget child,
}) : assert(animation != null),
super(key: key, child: child);
final Animation<double> animation;
final bool alwaysIncludeSemantics;
#override
RenderAnimatedCustom createRenderObject(BuildContext context) {
return RenderAnimatedCustom(
buildContext: context,
phase: animation,
alwaysIncludeSemantics: alwaysIncludeSemantics,
);
}
#override
void updateRenderObject(BuildContext context, RenderAnimatedCustom renderObject) {
renderObject
..phase = animation
..alwaysIncludeSemantics = alwaysIncludeSemantics;
}
#override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Animation<double>>('animation', animation));
properties.add(FlagProperty('alwaysIncludeSemantics', value: alwaysIncludeSemantics, ifTrue: 'alwaysIncludeSemantics'));
}
}
This new CustomTransition also creates a new RenderAnimatedCustom inside createRenderObject() (instead of FadeTransition's own RenderAnimatedOpacity). Sure enough, my custom RenderAnimatedCustom is a near duplicate of RenderAnimatedOpacity:
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
/// A custom renderer.
/// This is a copy of RenderAnimatedOpacity: https://github.com/flutter/flutter/blob/27321ebbad/packages/flutter/lib/src/rendering/proxy_box.dart#L825
class RenderAnimatedCustom extends RenderProxyBox {
RenderAnimatedCustom({
#required BuildContext buildContext,
#required Animation<double> phase,
bool alwaysIncludeSemantics = false,
RenderBox child,
}) : assert(phase != null),
assert(alwaysIncludeSemantics != null),
_alwaysIncludeSemantics = alwaysIncludeSemantics,
super(child) {
this.phase = phase;
this.buildContext = buildContext;
}
BuildContext buildContext;
double _lastUsedPhase;
#override
bool get alwaysNeedsCompositing => child != null && _currentlyNeedsCompositing;
bool _currentlyNeedsCompositing;
Animation<double> get phase => _phase;
Animation<double> _phase;
set phase(Animation<double> value) {
assert(value != null);
if (_phase == value) return;
if (attached && _phase != null) _phase.removeListener(_updatePhase);
_phase = value;
if (attached) _phase.addListener(_updatePhase);
_updatePhase();
}
/// Whether child semantics are included regardless of the opacity.
///
/// If false, semantics are excluded when [opacity] is 0.0.
///
/// Defaults to false.
bool get alwaysIncludeSemantics => _alwaysIncludeSemantics;
bool _alwaysIncludeSemantics;
set alwaysIncludeSemantics(bool value) {
if (value == _alwaysIncludeSemantics) return;
_alwaysIncludeSemantics = value;
markNeedsSemanticsUpdate();
}
#override
void attach(PipelineOwner owner) {
super.attach(owner);
_phase.addListener(_updatePhase);
_updatePhase(); // in case it changed while we weren't listening
}
#override
void detach() {
_phase.removeListener(_updatePhase);
super.detach();
}
void _updatePhase() {
final double newPhase = _phase.value;
if (_lastUsedPhase != newPhase) {
_lastUsedPhase = newPhase;
final bool didNeedCompositing = _currentlyNeedsCompositing;
_currentlyNeedsCompositing = _lastUsedPhase > 0 && _lastUsedPhase < 1;
if (child != null && didNeedCompositing != _currentlyNeedsCompositing) {
markNeedsCompositingBitsUpdate();
}
markNeedsPaint();
if (newPhase == 0 || _lastUsedPhase == 0) {
markNeedsSemanticsUpdate();
}
}
}
#override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
if (_lastUsedPhase == 0) {
// No need to keep the layer. We'll create a new one if necessary.
layer = null;
return;
}
if (_lastUsedPhase == 1) {
// No need to keep the layer. We'll create a new one if necessary.
layer = null;
context.paintChild(child, offset);
return;
}
assert(needsCompositing);
// Basic example, slides the screen in
context.paintChild(child, Offset((1 - _lastUsedPhase) * 255, 0));
}
}
#override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (child != null && (_lastUsedPhase != 0 || alwaysIncludeSemantics)) {
visitor(child);
}
}
#override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Animation<double>>('phase', phase));
properties.add(FlagProperty('alwaysIncludeSemantics', value: alwaysIncludeSemantics, ifTrue: 'alwaysIncludeSemantics'));
}
}
Finally, the problem is as such. In the above file, inside paint(), this placeholder code simply moves the screen sideways by rendering with different offsets using context.paintChild().
But I want to draw chunks of child instead. In this case, vertical strips so I can create the screen melt effect. But that's really just an example; I want to find a way to manipulate the render of the child so I could have other different image-based effects.
What I have tried
Looping and drawing parts with clip rectangles
Instead of simply doing context.paintChild(child, offset), I've tried looping and drawing it chunk by chunk. This is not super generic, but would work for the screen melt effect at least.
Inside paint() (ignore the clumsiness of the prototype code):
int segments = 10;
double width = context.estimatedBounds.width;
double height = context.estimatedBounds.height;
for (var i = 0; i < segments; i++) {
double l = ((width / segments) * i).round().toDouble();
double r = ((width / segments) * (i + 1)).round().toDouble();
double y = (1 - _lastUsedPhase) * 50 * (i + 1);
layer = context.pushClipRect(
true,
Offset(l, y),
Rect.fromLTWH(0, 0, r - l, height),
(c, o) => c.paintChild(child, Offset(0, o.dy)),
oldLayer: layer
);
}
Unfortunately, this doesn't work. Every call to paintChild() seems to clear out the previous call, so only the last "strip" is kept.
I've tried combinations of this with different "layer" properties, using clipRectAndPaint(), etc, but can't get anything different from the above example.
Capturing the image with toImage()
I haven't gone much further in this, but my first attempt was of course to simply capture a widget as an image, something I assume was straightforward.
Unfortunately this requires that my widget is wrapped around a RepaintBoundary() in the custom route. Something like this:
return CustomTransition(
animation: animation,
child: RepaintBoundary(child: child),
);
Then maybe we could just do child.toImage(), manipulate that inside a canvas, and present that.
My issue with that is that every time one defines the transition, the child would need to be wrapped in that way. I'd like the CustomTransition() to handle that instead, but I haven't found a way and I'm wondering if this is actually necessary.
There are other classes with a toImage() function - Picture, Scene, OffsetLayer - but none of them seemed to be readily available. Ideally there would be an easy way for me to capture stuff as an image from inside paint() on RenderAnimatedCustom`. I could then do any kind of manipulation to that image, and paint it instead.
Other orthogonal solutions
I know there's several answers on StackOverflow (and other places) about how to "capture an image from a Widget", but they seem to specific to using an existing Canvas, using RepaintBoundary, etc.
In sum, what I need is: a way to create custom canvas-manipulating screen transitions. An ability to capture arbitrary widgets (without an explicit RepaintBoundary) seem to be the key to this.
Any hints? Am I being foolish for avoiding RepaintBoundary? Is it the only way? Or is there any other way for me to use "layers" for accomplish this sort of segmented child drawing?
Minimal source code for this app example is available on GitHub.
PS. I'm aware the transition example as it is is trying to manipulate the oncoming screen, not the outgoing one, as it should work for this to work like Doom's screen melt. That's a further problem I'm not investigating right now.