简体   繁体   中英

How to listen for resize events in a Flutter AnimatedSize widget

Flutter'sAnimatedSize 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),
    );

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM