Flutter: How to handle Renderflex overflow in SliverPersistentHeader - flutter

I have a SliverPersistentHeader which contains a video. The desired behavior of this view is that as a user scrolls upward, the view should cover or minimize the size of the video. The video header is a widget containing a Chewie video player. The desired behavior works up to a certain point at which I get a pixel overflow as shown in this animation:
When the scroll reaches a certain point, the video can no longer resize and it results in a render overflow. The desired behavior would be for the video to continue to resize until it's gone, or to catch the error and hide or remove the video from the view. The code rendering this scroll view is:
Widget buildScollView(GenericScreenModel model) {
return CustomScrollView(
slivers: [
StandardHeader(),
SliverFillRemaining(
child: Container(
// color: Colors.transparent,
decoration: BoxDecoration(
border: Border.all(
color: Colors.white,
),
borderRadius: BorderRadius.only(topRight: radius, topLeft: radius)),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Text(model.model?.getContentText ?? 'Empty'),
)),
)
],
);
}
The StandardHeader class is a simple widget containing a Chewie video.
class _StandardHeaderState extends State<StandardHeader> {
#override
Widget build(BuildContext context) {
return SliverPersistentHeader(
floating: true,
delegate: Delegate(
Colors.blue,
'Header Title',
),
pinned: true,
);
}
}
Is there a way to catch this error and hide the video player? Can anyone help with this or point me to a resource? Thanks!

The issue seems to be with the Chewie and/or video player widget. If the header's height is less than the required height of the player, the overflow occurs.
You can achieve the desired effect by using a SingleChildRenderObjectWidget. I added an opacity factor that you can easily remove that gives it (in my opinion) an extra touch.
I named this widget: ClipBelowHeight
Output:
Source:
ClipBelowHeight is SingleChildRenderObjectWidget that adds the desired effect by using a clipHeight parameter to clamp the height of the child to one that does not overflow. It centers its child vertically (Chewie player in this case).
To understand more, read the comments inside the performLayout and paint method.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class ClipBelowHeight extends SingleChildRenderObjectWidget {
const ClipBelowHeight({
super.key,
super.child,
required this.clipHeight,
required this.opacityFactor,
});
/// The minimum height the [child] must have, as well as the height at which
/// clipping begins.
final double clipHeight;
/// The opacity factor to apply when the height decreases.
final double opacityFactor;
#override
RenderObject createRenderObject(BuildContext context) {
return RenderClipBelowHeight(clipHeight: clipHeight, factor: opacityFactor);
}
#override
void updateRenderObject(
BuildContext context,
RenderClipBelowHeight renderObject,
) {
renderObject
..clipHeight = clipHeight
..factor = opacityFactor;
}
}
class RenderClipBelowHeight extends RenderBox with RenderObjectWithChildMixin {
RenderClipBelowHeight({required double clipHeight, required double factor})
: _clipHeight = clipHeight,
_factor = factor;
double _clipHeight;
double get clipHeight => _clipHeight;
set clipHeight(double value) {
assert(value >= .0);
if (_clipHeight == value) return;
_clipHeight = value;
markNeedsLayout();
}
double _factor;
double get factor => _factor;
set factor(double value) {
assert(value >= .0);
if (_factor == value) return;
_factor = value;
markNeedsLayout();
}
#override
bool get sizedByParent => false;
#override
void performLayout() {
/// The child contraints depend on whether [constraints.maxHeight] is less
/// than [clipHeight]. This RenderObject's responsibility is to ensure that
/// the child's height is never below [clipHeight], because when the
/// child's height is below [clipHeight], then there will be visual
/// overflow.
final childConstraints = constraints.maxHeight < _clipHeight
? BoxConstraints.tight(Size(constraints.maxWidth, _clipHeight))
: constraints;
(child as RenderBox).layout(childConstraints, parentUsesSize: true);
size = Size(constraints.maxWidth, constraints.maxHeight);
}
#override
void paint(PaintingContext context, Offset offset) {
final theChild = child as RenderBox;
/// Clip the painted area to [size], which allows the [child] height to
/// be greater than [size] without overflowing.
context.pushClipRect(
true,
offset,
Offset.zero & size,
(PaintingContext context, Offset offset) {
/// (optional) Set the opacity by applying the specified factor.
context.pushOpacity(
offset,
/// The opacity begins to take effect at approximately half [size].
((255.0 + 128.0) * _factor).toInt(),
(context, offset) {
/// Ensure the child remains centered vertically based on [size].
final centeredOffset =
Offset(.0, (size.height - theChild.size.height) / 2.0);
context.paintChild(theChild, centeredOffset + offset);
},
);
},
);
}
#override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
final theChild = child as RenderBox;
var childParentData = theChild.parentData as BoxParentData;
final isHit = result.addWithPaintOffset(
offset: childParentData.offset,
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - childParentData.offset);
return theChild.hitTest(result, position: transformed);
},
);
return isHit;
}
#override
Size computeDryLayout(BoxConstraints constraints) => constraints.biggest;
#override
double computeMinIntrinsicWidth(double height) =>
(child as RenderBox).getMinIntrinsicWidth(height);
#override
double computeMaxIntrinsicWidth(double height) =>
(child as RenderBox).getMaxIntrinsicWidth(height);
#override
double computeMinIntrinsicHeight(double width) =>
(child as RenderBox).getMinIntrinsicHeight(width);
#override
double computeMaxIntrinsicHeight(double width) =>
(child as RenderBox).getMaxIntrinsicHeight(width);
}
The widget that uses the ClipBelowHeight widget is your header delegate. This widget should be self-explanatory and I think that you will be able to understand it.
class Delegate extends SliverPersistentHeaderDelegate {
Delegate(this.color, this.player);
final Color color;
final Chewie player;
#override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
return Container(
color: color,
child: ClipBelowHeight(
clipHeight: 80.0,
opacityFactor: 1.0 - shrinkOffset / maxExtent,
child: player,
),
);
}
#override
double get maxExtent => 150.0;
#override
double get minExtent => .0;
#override
bool shouldRebuild(Delegate oldDelegate) {
return color != oldDelegate.color || player != oldDelegate.player;
}
}

Related

Is there a Flutter widget specifying a min or max aspect ratio, rather than an exact ratio?

I want to force a child widget to have a max aspect ratio of e.g. 2.0: in other words, it is no wider than 2:1, but is as tall as possible. So in a tall skinny parent it would behave like SizedBox.expand(child: child), and in a short wide parent it would behave like AspectRatio(aspectRatio: 2.0, child: child).
Is there a widget that does this, like ConstrainedAspectRatio(maxAspectRatio: 2.0, child: child)? Or do I have to choose between (1) using a LayoutBuilder or (2) writing a custom SingleChildLayoutDelegate / SingleChildRenderObjectWidget?
Edit: In case there is no good answer out there, here's my SingleChildLayoutDelegate implementation. It is probably brittle.
import 'dart:math';
import 'package:flutter/material.dart';
class ConstrainedAspectRatio extends StatelessWidget {
const ConstrainedAspectRatio({required this.child, required this.maxAspectRatio, super.key});
final Widget child;
final double maxAspectRatio;
#override
Widget build(BuildContext context) => CustomSingleChildLayout(delegate: _CARDelegate(maxAspectRatio), child: child);
}
class _CARDelegate extends SingleChildLayoutDelegate {
_CARDelegate(this.maxAspectRatio);
final double maxAspectRatio;
Size _size = Size.zero;
#override
Size getSize(BoxConstraints constraints) {
// Full height, wide as allowed
final double w = constraints.maxWidth, h = constraints.maxHeight;
if (h.isInfinite) {
// Container infinitely tall; use max aspect ratio as exact aspect ratio
assert(w.isFinite, () => "Need at least one bounded constraint for $runtimeType.");
return _size = Size(w, w / maxAspectRatio);
} else {
// Finite height. Use all of it, and go as wide as maxAspectRatio allows.
return _size = Size(min(h * maxAspectRatio, w), h);
}
}
#override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
return BoxConstraints.tight(_size);
}
#override
bool shouldRelayout(covariant _CARDelegate oldDelegate) {
return oldDelegate.maxAspectRatio != maxAspectRatio;
}
}

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.

How to Dynamically Size a CustomPainter

I need to render my custom object inside of a ListTile with custom painter in order to draw some custom text.
ListTile(
title: CustomPaint(
painter: RowPainter.name(
_titleFontSelected,
_titleFont,
text,
index,
MediaQuery.of(context),
currentRow,
),
),
);
Inside my RowPainter I draw the text with the font selected.
When the row is too large, it automatically wraps and get drawn outside the given paint size.
void paint(Canvas canvas, Size size)
I like this behavior, but how can I resize the height of my paint area? Because this is a problem since this overlaps the next List row.
I know that the CustomPaint has a property Size settable, but I know the text dimension only inside my paint function using the TextPainter getBoxesForSelection but it's too late.
How can I "resize" my row painter height dynamically if the text wraps?
TL;DR
You cannot dynamically size a custom painter, however, your problem can be solved using a CustomPaint.
I will first elaborate on the dynamic sizing and then explain how to solve this problem using a constant size.
Dynamic size
This is essentially, where CustomPaint has its limits because it does not provide a way for you to size the painter based on the content.
The proper way of doing this is implementing your own RenderBox and overriding performLayout to size your render object based on the contents.
The RenderBox documentation is quite detailed on this, however, you might still find it difficult to get into it as it is quite different from building widgets.
Constant size
All of the above should not be needed in your case because you do not have a child for your custom paint.
You can simply supply the size parameter to your CustomPaint and calculate the required height in the parent widget.
You can use a LayoutBuilder to get the available width:
LayoutBuilder(
builder: (context, constraints) {
final maxWidth = constraints.maxWidth;
...
}
)
Now, you can simply use a TextPainter to retrieve the required size before even entering your custom paint:
builder: (context, constraints) {
...
final textPainter = TextPainter(
text: TextSpan(
text: 'Your text',
style: yourTextStyle,
),
textDirection: TextDirection.ltr,
);
textPainter.layout(maxWidth: maxWidth); // This will make the size available.
return CustomPaint(
size: textPainter.size,
...
);
}
Now, you can even pass your textPainter to your custom painter directly instead of passing the style arguments.
Your logic might be a bit more complicated, however, the point is that you can calculate the size before creating the CustomPaint, which allows you to set the size.
If you need something more complicated, you will likely have to implement your own RenderBox.
I haven't tested it out, but this might work:
First of all, you wrap the CustomPaint into a stateful widget (called e.g. DynamicCustomPaint), to manipulate your widget dynamically.
You give your CustomPainter a function onResize, which will give you the new size of the canvas when you know it.
You call this function once you know the exact size the Canvas has to be. By using, for example, this technique where you won't have to draw the text to know what size it will be.
When the onResize function will be called, you get the new size for the canvas and call setState in the DynamicCustomPaint state.
This might look like this:
class DynamicCustomPaint extends StatefulWidget {
#override
_DynamicCustomPaintState createState() => _DynamicCustomPaintState();
}
class _DynamicCustomPaintState extends State<DynamicCustomPaint> {
Size canvasSize;
#override
Widget build(BuildContext context) {
// Set inital size, maybe move this to initState function
if (canvasSize == null) {
// Decide what makes sense in your use-case as inital size
canvasSize = MediaQuery.of(context).size;
}
return CustomPaint(
size: canvasSize,
painter: RowPainter.name(_titleFontSelected, _titleFont, text, index, currentRow, onResize: (size) {
setState(() {
canvasSize = size;
});
}),
);
}
}
typedef OnResize = void Function(Size size);
class RowPainter extends CustomPainter {
RowPainter.name(
this._titleFontSelected,
this._titleFont,
this.text,
this.index,
this.currentRow,
{ this.onResize },
);
final FontStyle _titleFontSelected;
final FontStyle _titleFont;
final String text;
final int index;
final int currentRow;
final OnResize onResize;
#override
void paint(Canvas canvas, Size size) {
// TODO: implement paint
// call onResize somewhere in here
// onResize(newSize);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
Use SingleChildRenderObjectWidget and RenderBox instead. Full simple example with dynamic resizing.
DartPad
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
home: Scaffold(
body: Center(
child: Column(
children: [
SizedBox(height: 100,),
Text('I am above'),
MyWidget(),
Text('I am below')
],
),
),
),
));
}
class MyWidget extends SingleChildRenderObjectWidget {
#override
MyRenderBox createRenderObject(BuildContext context) {
return MyRenderBox();
}
}
class MyRenderBox extends RenderBox {
double myHeight = 200;
#override
void paint(PaintingContext context, Offset offset) {
Paint paint = Paint()
..color = Colors.black..style = PaintingStyle.fill;
context.canvas.drawRect(
Rect.fromLTRB(offset.dx, offset.dy,
offset.dx + size.width, offset.dy + size.height,), paint);
}
#override
void performLayout() {
size = Size(
constraints.constrainWidth(200),
constraints.constrainHeight(myHeight),
);
}
// Timer just an example to show dynamic behavior
MyRenderBox(){
Timer.periodic(Duration(seconds: 2), handleTimeout);
}
void handleTimeout(timer) {
myHeight += 40;
markNeedsLayoutForSizedByParentChange();
layout(constraints);
}
}
CustomPainter will only size to its children's size or initial value passed to the constructor. Documentation:
Custom painters normally size themselves to their child. If they do not have a child, they attempt to size themselves to the size, which defaults to Size.zero. size must not be null.
Basics of RenderBox
https://programmer.group/the-operation-instruction-of-flutter-s-renderbox-principle-analysis.html
https://api.flutter.dev/flutter/rendering/RenderBox-class.html

How to move element anywhere inside parent container with drag and drop in Flutter?

I'm using the Draggable widget in Flutter, and I want to move my element anywhere inside a parent widget.
I tried the dragtarget widget, but I couldn't set it to my parent widget. I also tried the onDraggableCanceled method of the Draggable widget for getting the offset, and applied it for the new offset of the element, but it gives me the offset from the device, not from the parent container.
So, what is the right way of doing this?
Just ran into the same problem working on a drag-and-drop canvas in Flutter Web. I ended up working around this by inserting a GlobalKey for each of the draggable widgets and adjusting the placement offsets by accounting for the size of the containing renderbox (after waiting for the rendering to complete, via the addPostFrameCallback()). In order to facilitate precise placement, I used a Stack()/Positioned() and placed the Draggable() within this:
import 'package:flutter/material.dart';
class PositionedDraggableIcon extends StatefulWidget {
final double top;
final double left;
PositionedDraggableIcon({Key key, this.top, this.left}) : super(key: key);
#override
_PositionedDraggableIconState createState() => _PositionedDraggableIconState();
}
class _PositionedDraggableIconState extends State<PositionedDraggableIcon> {
GlobalKey _key = GlobalKey();
double top, left;
double xOff, yOff;
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
top = widget.top;
left = widget.left;
super.initState();
}
void _getRenderOffsets() {
final RenderBox renderBoxWidget = _key.currentContext.findRenderObject();
final offset = renderBoxWidget.localToGlobal(Offset.zero);
yOff = offset.dy - this.top;
xOff = offset.dx - this.left;
}
void _afterLayout(_) {
_getRenderOffsets();
}
#override
Widget build(BuildContext context) {
return Positioned(
key: _key,
top: top,
left: left,
child: Draggable(
child: Icon(Icons.input),
feedback: Icon(Icons.input),
childWhenDragging: Container(),
onDragEnd: (drag) {
setState(() {
top = drag.offset.dy - yOff;
left = drag.offset.dx - xOff;
});
},
),
);
}
}
It's probably possible to do this with the widget context itself and avoid the GlobalKey, but this works well enough for me.

How to listen for resize events in a Flutter AnimatedSize widget

Flutter's AnimatedSize class animates its size according to the size of its child. I need to know how to listen for changes to the size, ideally when the resizing has finished.
With my use-case, this widget is contained within a ListView, but I only seem to be able to listen to scroll events on this with a NotificationListener (being able to listen to changes in scrollable height would solve my problem).
Alternatively, being able to listen for when a widget such as a Column changes it's number of children would work too.
There was a widget specifically made for this case. It's called:
SizeChangedLayoutNotifier (https://api.flutter.dev/flutter/widgets/SizeChangedLayoutNotifier-class.html)
You just have to wrap your widget with it and then listen with the NotificationListener widget (https://api.flutter.dev/flutter/widgets/NotificationListener-class.html) for changes.
An Example would be following:
NotificationListener(
onNotification: (SizeChangedLayoutNotification notification){
Future.delayed(Duration(milliseconds: 300),(){setState(() {
print('size changed');
_height++;
});});
return true;
},
child: SizeChangedLayoutNotifier( child: AnimatedContainer(width: 100, height: _height)))
Hope this will help all future people which will find this post.
I believe the last line of your question provides a hint as to what you're trying to do. It sounds like you're displaying a list of things, and you want something to be notified when that list of things changes. If I'm wrong about that, please clarify =).
There are two ways of doing this; one is that you could pass a callback function to the widget containing the list. When you added something to the list you could simply call the callback.
However, that is a little bit fragile and if you have multiple layers in between the place you need to know and the actual list it could get messy.
This is due in part to the fact that in flutter, for the most part, data goes downwards (through children) much easier than it goes up. It sounds like what you might want to do is have a parent widget that holds the list of items, and passes that down to whatever builds the actual list. If there are multiple layers of widgets between the parent and the child, you could use an InheritedWidget to get the information from the child without directly passing it.
EDIT: with clarification from the OP, this answer only provided an sub-optimal alternative to the original goal. See below for an answer to the main query:
I don't think that it is possible to do this with any existing flutter widgets. However, because flutter is open-source it's entirely possible to simply create your own widget based on the flutter one that does do what you need. You just need to dig into the source code a bit.
Please note that the code I'm pasting below contains a slightly modified version of the flutter implementation in rendering animated_size.dart and widgets animated_size.dart, and therefore usage of it must adhere to the flutter LICENSE file at the time of copying. Use of the code is governed by BSD style license, yada yada.
I've created a very slightly modified version of the AnimatedSize widget called NotifyingAnimatedSize (and the corresponding more-interesting NotifyingRenderAnimatedSize) in the code below, which simply calls a callback when it starts animated and when it's done animating. I've removed all of the comments from the source code as they made it even longer.
Look for notificationCallback throughout the code as that's basically all I added.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() => runApp(new MyApp());
enum NotifyingRenderAnimatedSizeState {
start,
stable,
changed,
unstable,
}
enum SizeChangingStatus {
changing,
done,
}
typedef void NotifyingAnimatedSizeCallback(SizeChangingStatus status);
class NotifyingRenderAnimatedSize extends RenderAligningShiftedBox {
NotifyingRenderAnimatedSize({
#required TickerProvider vsync,
#required Duration duration,
Curve curve: Curves.linear,
AlignmentGeometry alignment: Alignment.center,
TextDirection textDirection,
RenderBox child,
this.notificationCallback
}) : assert(vsync != null),
assert(duration != null),
assert(curve != null),
_vsync = vsync,
super(child: child, alignment: alignment, textDirection: textDirection) {
_controller = new AnimationController(
vsync: vsync,
duration: duration,
)..addListener(() {
if (_controller.value != _lastValue) markNeedsLayout();
});
_animation = new CurvedAnimation(parent: _controller, curve: curve);
}
AnimationController _controller;
CurvedAnimation _animation;
final SizeTween _sizeTween = new SizeTween();
bool _hasVisualOverflow;
double _lastValue;
final NotifyingAnimatedSizeCallback notificationCallback;
#visibleForTesting
NotifyingRenderAnimatedSizeState get state => _state;
NotifyingRenderAnimatedSizeState _state = NotifyingRenderAnimatedSizeState.start;
Duration get duration => _controller.duration;
set duration(Duration value) {
assert(value != null);
if (value == _controller.duration) return;
_controller.duration = value;
}
Curve get curve => _animation.curve;
set curve(Curve value) {
assert(value != null);
if (value == _animation.curve) return;
_animation.curve = value;
}
bool get isAnimating => _controller.isAnimating;
TickerProvider get vsync => _vsync;
TickerProvider _vsync;
set vsync(TickerProvider value) {
assert(value != null);
if (value == _vsync) return;
_vsync = value;
_controller.resync(vsync);
}
#override
void detach() {
_controller.stop();
super.detach();
}
Size get _animatedSize {
return _sizeTween.evaluate(_animation);
}
#override
void performLayout() {
_lastValue = _controller.value;
_hasVisualOverflow = false;
if (child == null || constraints.isTight) {
_controller.stop();
size = _sizeTween.begin = _sizeTween.end = constraints.smallest;
_state = NotifyingRenderAnimatedSizeState.start;
child?.layout(constraints);
return;
}
child.layout(constraints, parentUsesSize: true);
assert(_state != null);
switch (_state) {
case NotifyingRenderAnimatedSizeState.start:
_layoutStart();
break;
case NotifyingRenderAnimatedSizeState.stable:
_layoutStable();
break;
case NotifyingRenderAnimatedSizeState.changed:
_layoutChanged();
break;
case NotifyingRenderAnimatedSizeState.unstable:
_layoutUnstable();
break;
}
size = constraints.constrain(_animatedSize);
alignChild();
if (size.width < _sizeTween.end.width || size.height < _sizeTween.end.height) _hasVisualOverflow = true;
}
void _restartAnimation() {
_lastValue = 0.0;
_controller.forward(from: 0.0);
}
void _layoutStart() {
_sizeTween.begin = _sizeTween.end = debugAdoptSize(child.size);
_state = NotifyingRenderAnimatedSizeState.stable;
}
void _layoutStable() {
if (_sizeTween.end != child.size) {
_sizeTween.begin = size;
_sizeTween.end = debugAdoptSize(child.size);
_restartAnimation();
_state = NotifyingRenderAnimatedSizeState.changed;
} else if (_controller.value == _controller.upperBound) {
// Animation finished. Reset target sizes.
_sizeTween.begin = _sizeTween.end = debugAdoptSize(child.size);
notificationCallback(SizeChangingStatus.done);
} else if (!_controller.isAnimating) {
_controller.forward(); // resume the animation after being detached
}
}
void _layoutChanged() {
if (_sizeTween.end != child.size) {
// Child size changed again. Match the child's size and restart animation.
_sizeTween.begin = _sizeTween.end = debugAdoptSize(child.size);
_restartAnimation();
_state = NotifyingRenderAnimatedSizeState.unstable;
} else {
notificationCallback(SizeChangingStatus.changing);
// Child size stabilized.
_state = NotifyingRenderAnimatedSizeState.stable;
if (!_controller.isAnimating) _controller.forward(); // resume the animation after being detached
}
}
void _layoutUnstable() {
if (_sizeTween.end != child.size) {
// Still unstable. Continue tracking the child.
_sizeTween.begin = _sizeTween.end = debugAdoptSize(child.size);
_restartAnimation();
} else {
// Child size stabilized.
_controller.stop();
_state = NotifyingRenderAnimatedSizeState.stable;
}
}
#override
void paint(PaintingContext context, Offset offset) {
if (child != null && _hasVisualOverflow) {
final Rect rect = Offset.zero & size;
context.pushClipRect(needsCompositing, offset, rect, super.paint);
} else {
super.paint(context, offset);
}
}
}
class NotifyingAnimatedSize extends SingleChildRenderObjectWidget {
const NotifyingAnimatedSize({
Key key,
Widget child,
this.alignment: Alignment.center,
this.curve: Curves.linear,
#required this.duration,
#required this.vsync,
this.notificationCallback,
}) : super(key: key, child: child);
final AlignmentGeometry alignment;
final Curve curve;
final Duration duration;
final TickerProvider vsync;
final NotifyingAnimatedSizeCallback notificationCallback;
#override
NotifyingRenderAnimatedSize createRenderObject(BuildContext context) {
return new NotifyingRenderAnimatedSize(
alignment: alignment,
duration: duration,
curve: curve,
vsync: vsync,
textDirection: Directionality.of(context),
notificationCallback: notificationCallback
);
}
#override
void updateRenderObject(BuildContext context, NotifyingRenderAnimatedSize renderObject) {
renderObject
..alignment = alignment
..duration = duration
..curve = curve
..vsync = vsync
..textDirection = Directionality.of(context);
}
}
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() => MyAppState();
}
class MyAppState extends State<MyApp> with TickerProviderStateMixin<MyApp> {
double _containerSize = 100.0;
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new SafeArea(
child: new Container(
color: Colors.white,
child: new Column(children: [
new RaisedButton(
child: new Text("Press me to make the square change size!"),
onPressed: () => setState(
() {
if (_containerSize > 299.0)
_containerSize = 100.0;
else
_containerSize += 100.0;
},
),
),
new NotifyingAnimatedSize(
duration: new Duration(seconds: 2),
vsync: this,
child: new Container(
color: Colors.blue,
width: _containerSize,
height: _containerSize,
),
notificationCallback: (state) {
print("State is $state");
},
)
]),
),
),
);
}
}
This is not possible. Widgets have no clue about the size of their children. The only thing they do is apply constraints on them, but that's unrelated to the final size.
Here I repost rmtmckenzie's (credits to him) answer but with null safety. I decided not to edit his answer to offer with his and mine both answers with and without null safety. You can just use in your code the NotifyingAnimatedSize.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
enum SizeChangingStatus {
changing,
done,
}
enum NotifyingRenderAnimatedSizeState {
start,
stable,
changed,
unstable,
}
typedef NotifyingAnimatedSizeCallback = void Function(SizeChangingStatus status);
class NotifyingRenderAnimatedSize extends RenderAligningShiftedBox {
NotifyingRenderAnimatedSize({
required TickerProvider vsync,
required Duration duration,
Duration? reverseDuration,
Curve curve = Curves.linear,
AlignmentGeometry alignment = Alignment.center,
required TextDirection textDirection,
RenderBox? child,
Clip clipBehavior = Clip.hardEdge,
required this.notificationCallback,
})
: _vsync = vsync,
_clipBehavior = clipBehavior,
super(textDirection: textDirection, alignment: alignment, child: child) {
_controller = AnimationController(
vsync: vsync,
duration: duration,
reverseDuration: reverseDuration,
)
..addListener(() {
if (_controller.value != _lastValue) {
markNeedsLayout();
}
});
_animation = CurvedAnimation(
parent: _controller,
curve: curve,
);
}
late final AnimationController _controller;
late final CurvedAnimation _animation;
final SizeTween _sizeTween = SizeTween();
late bool _hasVisualOverflow;
double? _lastValue;
final NotifyingAnimatedSizeCallback notificationCallback;
/// The state this size animation is in.
///
/// See [RenderAnimatedSizeState] for possible states.
#visibleForTesting
NotifyingRenderAnimatedSizeState get state => _state;
NotifyingRenderAnimatedSizeState _state = NotifyingRenderAnimatedSizeState.start;
/// The duration of the animation.
Duration get duration => _controller.duration!;
set duration(Duration value) {
if (value == _controller.duration) {
return;
}
_controller.duration = value;
}
/// The duration of the animation when running in reverse.
Duration? get reverseDuration => _controller.reverseDuration;
set reverseDuration(Duration? value) {
if (value == _controller.reverseDuration) {
return;
}
_controller.reverseDuration = value;
}
/// The curve of the animation.
Curve get curve => _animation.curve;
set curve(Curve value) {
if (value == _animation.curve) {
return;
}
_animation.curve = value;
}
/// {#macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge], and must not be null.
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior = Clip.hardEdge;
set clipBehavior(Clip value) {
if (value != _clipBehavior) {
_clipBehavior = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
}
/// Whether the size is being currently animated towards the child's size.
///
/// See [RenderAnimatedSizeState] for situations when we may not be animating
/// the size.
bool get isAnimating => _controller.isAnimating;
/// The [TickerProvider] for the [AnimationController] that runs the animation.
TickerProvider get vsync => _vsync;
TickerProvider _vsync;
set vsync(TickerProvider value) {
if (value == _vsync) {
return;
}
_vsync = value;
_controller.resync(vsync);
}
#override
void attach(PipelineOwner owner) {
super.attach(owner);
switch (state) {
case NotifyingRenderAnimatedSizeState.start:
case NotifyingRenderAnimatedSizeState.stable:
break;
case NotifyingRenderAnimatedSizeState.changed:
case NotifyingRenderAnimatedSizeState.unstable:
// Call markNeedsLayout in case the RenderObject isn't marked dirty
// already, to resume interrupted resizing animation.
markNeedsLayout();
break;
}
}
#override
void detach() {
_controller.stop();
super.detach();
}
Size? get _animatedSize => _sizeTween.evaluate(_animation);
#override
void performLayout() {
_lastValue = _controller.value;
_hasVisualOverflow = false;
final BoxConstraints constraints = this.constraints;
if (child == null || constraints.isTight) {
_controller.stop();
size = _sizeTween.begin = _sizeTween.end = constraints.smallest;
_state = NotifyingRenderAnimatedSizeState.start;
child?.layout(constraints);
return;
}
child!.layout(constraints, parentUsesSize: true);
switch (_state) {
case NotifyingRenderAnimatedSizeState.start:
_layoutStart();
break;
case NotifyingRenderAnimatedSizeState.stable:
_layoutStable();
break;
case NotifyingRenderAnimatedSizeState.changed:
_layoutChanged();
break;
case NotifyingRenderAnimatedSizeState.unstable:
_layoutUnstable();
break;
}
size = constraints.constrain(_animatedSize!);
alignChild();
if (size.width < _sizeTween.end!.width || size.height < _sizeTween.end!.height) {
_hasVisualOverflow = true;
}
}
#override
Size computeDryLayout(BoxConstraints constraints) {
if (child == null || constraints.isTight) {
return constraints.smallest;
}
// This simplified version of performLayout only calculates the current
// size without modifying global state. See performLayout for comments
// explaining the rational behind the implementation.
final Size childSize = child!.getDryLayout(constraints);
switch (_state) {
case NotifyingRenderAnimatedSizeState.start:
return constraints.constrain(childSize);
case NotifyingRenderAnimatedSizeState.stable:
if (_sizeTween.end != childSize) {
return constraints.constrain(size);
} else if (_controller.value == _controller.upperBound) {
return constraints.constrain(childSize);
}
break;
case NotifyingRenderAnimatedSizeState.unstable:
case NotifyingRenderAnimatedSizeState.changed:
if (_sizeTween.end != childSize) {
return constraints.constrain(childSize);
}
break;
}
return constraints.constrain(_animatedSize!);
}
void _restartAnimation() {
_lastValue = 0.0;
_controller.forward(from: 0.0);
}
/// Laying out the child for the first time.
///
/// We have the initial size to animate from, but we do not have the target
/// size to animate to, so we set both ends to child's size.
void _layoutStart() {
_sizeTween.begin = _sizeTween.end = debugAdoptSize(child!.size);
_state = NotifyingRenderAnimatedSizeState.stable;
}
/// At this state we're assuming the child size is stable and letting the
/// animation run its course.
///
/// If during animation the size of the child changes we restart the
/// animation.
void _layoutStable() {
if (_sizeTween.end != child!.size) {
_sizeTween.begin = size;
_sizeTween.end = debugAdoptSize(child!.size);
_restartAnimation();
_state = NotifyingRenderAnimatedSizeState.changed;
} else if (_controller.value == _controller.upperBound) {
// Animation finished. Reset target sizes.
_sizeTween.begin = _sizeTween.end = debugAdoptSize(child!.size);
notificationCallback(SizeChangingStatus.done);
} else if (!_controller.isAnimating) {
_controller.forward(); // resume the animation after being detached
}
}
/// This state indicates that the size of the child changed once after being
/// considered stable.
///
/// If the child stabilizes immediately, we go back to stable state. If it
/// changes again, we match the child's size, restart animation and go to
/// unstable state.
void _layoutChanged() {
if (_sizeTween.end != child!.size) {
// Child size changed again. Match the child's size and restart animation.
_sizeTween.begin = _sizeTween.end = debugAdoptSize(child!.size);
_restartAnimation();
_state = NotifyingRenderAnimatedSizeState.unstable;
} else {
notificationCallback(SizeChangingStatus.changing);
// Child size stabilized.
_state = NotifyingRenderAnimatedSizeState.stable;
if (!_controller.isAnimating) {
// Resume the animation after being detached.
_controller.forward();
}
}
}
/// The child's size is not stable.
///
/// Continue tracking the child's size until is stabilizes.
void _layoutUnstable() {
if (_sizeTween.end != child!.size) {
// Still unstable. Continue tracking the child.
_sizeTween.begin = _sizeTween.end = debugAdoptSize(child!.size);
_restartAnimation();
} else {
// Child size stabilized.
_controller.stop();
_state = NotifyingRenderAnimatedSizeState.stable;
}
}
#override
void paint(PaintingContext context, Offset offset) {
if (child != null && _hasVisualOverflow && clipBehavior != Clip.none) {
final Rect rect = Offset.zero & size;
_clipRectLayer.layer = context.pushClipRect(
needsCompositing,
offset,
rect,
super.paint,
clipBehavior: clipBehavior,
oldLayer: _clipRectLayer.layer,
);
} else {
_clipRectLayer.layer = null;
super.paint(context, offset);
}
}
final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
#override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
}
class NotifyingAnimatedSize extends StatefulWidget {
/// Creates a widget that animates its size to match that of its child.
///
/// The [curve] and [duration] arguments must not be null.
const NotifyingAnimatedSize({
required this.child,
this.alignment = Alignment.center,
this.curve = Curves.linear,
required this.duration,
this.reverseDuration,
required this.notificationCallback,
this.clipBehavior = Clip.hardEdge,
});
/// The widget below this widget in the tree.
///
/// {#macro flutter.widgets.ProxyWidget.child}
final Widget child;
/// The alignment of the child within the parent when the parent is not yet
/// the same size as the child.
///
/// The x and y values of the alignment control the horizontal and vertical
/// alignment, respectively. An x value of -1.0 means that the left edge of
/// the child is aligned with the left edge of the parent whereas an x value
/// of 1.0 means that the right edge of the child is aligned with the right
/// edge of the parent. Other values interpolate (and extrapolate) linearly.
/// For example, a value of 0.0 means that the center of the child is aligned
/// with the center of the parent.
///
/// Defaults to [Alignment.center].
///
/// See also:
///
/// * [Alignment], a class with convenient constants typically used to
/// specify an [AlignmentGeometry].
/// * [AlignmentDirectional], like [Alignment] for specifying alignments
/// relative to text direction.
final AlignmentGeometry alignment;
/// The animation curve when transitioning this widget's size to match the
/// child's size.
final Curve curve;
/// The duration when transitioning this widget's size to match the child's
/// size.
final Duration duration;
/// The duration when transitioning this widget's size to match the child's
/// size when going in reverse.
///
/// If not specified, defaults to [duration].
final Duration? reverseDuration;
/// {#macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge], and must not be null.
final Clip clipBehavior;
/// Callback to trigger when animation ends
final NotifyingAnimatedSizeCallback notificationCallback;
#override
State<NotifyingAnimatedSize> createState() => _NotifyingAnimatedSizeState();
}
class _NotifyingAnimatedSizeState extends State<NotifyingAnimatedSize> with SingleTickerProviderStateMixin {
#override
Widget build(BuildContext context) =>
_NotifyingAnimatedSize(
alignment: widget.alignment,
curve: widget.curve,
duration: widget.duration,
vsync: this,
notificationCallback: widget.notificationCallback,
child: widget.child,
);
}
class _NotifyingAnimatedSize extends SingleChildRenderObjectWidget {
const _NotifyingAnimatedSize({
Key? key,
required Widget child,
this.alignment = Alignment.center,
this.curve = Curves.linear,
required this.duration,
required this.vsync,
required this.notificationCallback,
}) : super(key: key, child: child);
final AlignmentGeometry alignment;
final Curve curve;
final Duration duration;
final TickerProvider vsync;
final NotifyingAnimatedSizeCallback notificationCallback;
#override
NotifyingRenderAnimatedSize createRenderObject(BuildContext context) =>
NotifyingRenderAnimatedSize(
alignment: alignment,
duration: duration,
curve: curve,
vsync: vsync,
textDirection: Directionality.of(context),
notificationCallback: notificationCallback);
#override
void updateRenderObject(BuildContext context, NotifyingRenderAnimatedSize renderObject) {
renderObject
..alignment = alignment
..duration = duration
..curve = curve
..vsync = vsync
..textDirection = Directionality.of(context);
}
}
User the widget like so:
NotifyingAnimatedSize(
duration: const Duration(milliseconds: 200),
notificationCallback: (status) {
if (status == SizeChangingStatus.done) {
//do something
}
},
child: Container(height: 50, width: 50, color: Colors.red),
);