Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to listen for resize events in a Flutter AnimatedSize widget

Tags:

flutter

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.

like image 750
hunter Avatar asked Apr 24 '18 07:04

hunter


People also ask

How to use animatedsize in flutter?

In Flutter, AnimatedSize is a widget that animates its size to match the size of its child. In this tutorial, I am going to explain how to use the widget with some examples. To use AnimatedSize, below is the constructor that you need to call. There are two required arguments: vsync and duration.

What is the use of animatedsize widget?

AnimatedSize Widget is a widget that automatically transitions its size over a given duration whenever the given child’s size changes. The AnimatedSize Widget takes a duration parameter, and you use the Duration class to specify 1 second.

Is it possible to create a widget from a Flutter App?

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.

How do I animate the size change of a widget?

If what you need is animating the size change of a widget, one of the alternatives is using the AnimatedContainer widget. The AnimatedContainer is a Container that can animate its child when any property changes. Key? key: The widget's key, used to control how a widget is replaced with another widget.


2 Answers

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.

like image 152
Christian X Avatar answered Oct 01 '22 08:10

Christian X


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");
              },
            )
          ]),
        ),
      ),
    );
  }
}
like image 28
rmtmckenzie Avatar answered Oct 01 '22 08:10

rmtmckenzie