繁体   English   中英

如何在 Flutter AnimatedSize 小部件中监听调整大小事件

[英]How to listen for resize events in a Flutter AnimatedSize widget

Flutter 的AnimatedSize class 根据其子项的大小为其大小设置动画。 我需要知道如何监听大小的变化,最好是在调整大小完成后。

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

或者,能够监听诸如Column之类的小部件何时更改其子项数量也可以。

有一个专门为此案例制作的小部件。 它被称为: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)))

希望这会帮助所有将来会找到这篇文章的人。

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

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

但是,这有点脆弱,如果您需要知道的地方和实际列表之间有多个层,它可能会变得混乱。

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


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

我认为使用任何现有的颤振小部件都无法做到这一点。 然而,由于颤振是开源的,这完全有可能基于扑一个,做你需要简单地创建自己的控件。 你只需要深入研究一下源代码。

请注意,我在下面粘贴的代码包含在渲染 animation_size.dart小部件animation_size.dart 时颤振实现的稍微修改版本,因此它的使用必须在复制时遵守颤振许可证文件 代码的使用受 BSD 风格许可证 yada yada 的约束。

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

在整个代码中寻找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");
              },
            )
          ]),
        ),
      ),
    );
  }
}

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

在这里,我重新发布了 rmtmckenzie(归功于他)的答案,但安全性为 null。 我决定不编辑他的答案,以便在有和没有 null 安全的情况下提供他和我的两个答案。 您可以只在代码中使用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);
  }
}

像这样使用小部件:

NotifyingAnimatedSize(
      duration: const Duration(milliseconds: 200),
      notificationCallback: (status) {
        if (status == SizeChangingStatus.done) {
          //do something
        }
      },
      child: Container(height: 50, width: 50, color: Colors.red),
    );

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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