【问题标题】:How to listen for resize events in a Flutter AnimatedSize widget如何在 Flutter AnimatedSize 小部件中侦听调整大小事件
【发布时间】:2026-01-22 14:25:02
【问题描述】:

Flutter 的 AnimatedSize 类根据其子项的大小对其大小进行动画处理。我需要知道如何监听大小的变化,最好是在调整大小完成后。

在我的用例中,这个小部件包含在 ListView 中,但我似乎只能使用 NotificationListener 来监听滚动事件(能够监听可滚动高度的变化将解决我的问题)。

另外,能够在诸如Column 之类的小部件更改其子级数量时进行侦听也可以。

【问题讨论】:

  • 我已经根据您问题的最后一行以及您实际上并不像孩子们那样关心身高的假设来回答。如果有错请告诉我。
  • 考虑创建第二个问题,解释你试图在视觉上实现什么。虽然这个解决方案在不改变 Flutter 的代码的情况下是不可能的,但很可能是其他的。

标签: flutter


【解决方案1】:

这是不可能的。小部件不知道其孩子的大小。他们唯一要做的就是对它们施加约束,但这与最终大小无关。

【讨论】:

  • 我不需要知道大小。我只需要知道尺寸何时发生变化。
  • 那是一回事。如果一个小部件不知道大小,它应该如何知道大小何时发生变化? :p
  • AnimatedSize 类的唯一目的是根据其子级大小的变化来为自己设置动画。所以从逻辑上讲,它必须在某个时候知道它的孩子的大小才能计算动画。但我再说一遍,我对大小不感兴趣,我只想在它执行动画时收到通知(即改变大小)。
  • 就像我说的,这是不可能的。至少没有小部件。您唯一的选择是每帧获取RenderAnimatedSize,然后查看它是state。但这过于复杂。您不妨创建自己的RenderAnimatedSize 小部件并收听AnimationController
  • --> 正如我在当前答案中提到的,有一种方法可以做到这一点--> SizeChangedLayoutNotifier 是这里解决方案的关键
【解决方案2】:

我相信您问题的最后一行暗示了您正在尝试做什么。听起来您正在显示一个事物列表,并且您希望在该事物列表发生变化时得到通知。如果我错了,请澄清=)。

有两种方法可以做到这一点;一种是您可以将回调函数传递给包含列表的小部件。当您向列表中添加某些内容时,您可以简单地调用回调。

但是,这有点脆弱,如果您在需要了解的位置和实际列表之间有多个层,它可能会变得混乱。

这部分是因为在 Flutter 中,在大多数情况下,数据向下(通过子级)比向上更容易。听起来您可能想要做的是拥有一个包含项目列表的父小部件,并将其传递给构建实际列表的任何内容。如果父子节点之间存在多层小部件,您可以使用InheritedWidget 从子节点获取信息,而无需直接传递。


编辑:经过 OP 的澄清,此答案仅提供了原始目标的次优替代方案。有关主要查询的答案,请参见下文:

我认为任何现有的颤振小部件都不可能做到这一点。但是,由于 Flutter 是开源的,因此完全有可能简单地基于 Flutter 创建自己的小部件,确实 可以满足您的需求。您只需要深入研究一下源代码即可。

请注意,我在下面粘贴的代码包含在rendering animated_size.dartwidgets animated_size.dart 中的flutter 实现的略微修改版本,因此它的使用必须遵守flutter LICENSE file at the time of copying。代码的使用受 BSD 风格许可证 yada yada 的约束。

我在下面的代码中创建了一个名为 NotifyingAnimatedSize 的 AnimatedSize 小部件的略微修改版本(以及相应的更有趣的 NotifyingRenderAnimatedSize),它只是在开始动画和完成动画时调用回调。我已经从源代码中删除了所有的 cmets,因为它们使它变得更长。

在整个代码中查找notificationCallback,因为这基本上就是我添加的所有内容。

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

【讨论】:

  • 我的问题的最后一行实际上只是一个安慰奖,实际上我希望了解 AnimatedSize 完成动画时的通知/侦听器(尤其是在列表视图的上下文中),但为您的建议 +1
【解决方案3】:

有一个专门为这种情况制作的小部件。它被称为: SizeChangedLayoutNotifier (https://api.flutter.dev/flutter/widgets/SizeChangedLayoutNotifier-class.html)

您只需用它包装您的小部件,然后使用 NotificationListener 小部件 (https://api.flutter.dev/flutter/widgets/NotificationListener-class.html) 监听更改。

一个例子如下:

             NotificationListener(
                onNotification: (SizeChangedLayoutNotification notification){

                  Future.delayed(Duration(milliseconds: 300),(){setState(() {
                    print('size changed');
      _height++;
 
                      });});
                      return true;
                    },
                    child: SizeChangedLayoutNotifier( child: AnimatedContainer(width: 100, height: _height)))

希望这将帮助所有将来找到此帖子的人。

【讨论】:

  • 您可以使用WidgetsBinding.instance?.addPostFrameCallback((_) { ... }) 延迟到下一帧,而不是300ms 延迟。延迟它的原因是在布局阶段不允许调用setState,因此回调应该在下一帧。