flutter 如何从小部件列表中选择动画来调整小部件的大小和显示小部件?

blpfk2vs  于 2022-11-17  发布在  Flutter
关注(0)|答案(1)|浏览(226)

我希望有一个小部件,采取任何数量的子小部件,并显示任何小部件被选中。一个小部件是由索引的小部件在儿童名单。
为了选择一个小部件,我创建了一个有状态的小部件,它的builder方法返回选定的子索引,ChildByIndexState允许其他小部件访问和更新选定的子索引。

class ChildByIndex extends StatefulWidget {
  const ChildByIndex({
    Key? key,
    required this.index,
    required this.children,
  }) : super(key: key);

  final int index;
  final List<Widget> children;

  @override
  State<ChildByIndex> createState() => ChildByIndexState();
}

class ChildByIndexState extends State<ChildByIndex> {
  late int _index = widget.index;

  int get index => _index;
  
  /// Update the state if the index is different from the current index.
  set index(int value) {
    if (value != _index) setState(() => _index = value);
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedSwitcher(
      duration: const Duration(milliseconds: 300),
      child: SizedBox(key: ValueKey(_index), child: widget.children[_index]),
    );
  }
}

这就解决了切换小部件的动画问题,接下来我需要根据所选小部件的固有大小动态调整大小。演示小部件使用AnimatedSize小部件来动画化大小转换,并使用GlobalKey<ChildByIndexState>来设置索引。

class Resizer extends StatelessWidget {
  const Resizer({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final children = <Widget>[
      Container(color: Colors.red, width: 100, height: 100),
      Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: const [
          SizedBox(height: 100, child: Card(color: Colors.purple)),
          SizedBox(height: 100, child: Card(color: Colors.pink)),
        ],
      ),
      Container(color: Colors.green, width: 250, height: 150),
    ];

    final childByIndexKey = GlobalKey<ChildByIndexState>();

    return Scaffold(
      body: SafeArea(
        child: Stack(
          children: [
            Positioned.fill(
              child: Center(
                child: AnimatedSize(
                  duration: const Duration(milliseconds: 300),
                  reverseDuration: const Duration(milliseconds: 300),
                  curve: Curves.easeOutCubic,
                  child: ChildByIndex(
                    key: childByIndexKey,
                    index: 0,
                    children: children,
                  ),
                ),
              ),
            ),

            /// Sets the index of [ChildByIndex] and wraps around to the first
            /// child index when the index is past at the final child.
            Positioned(
              width: 64.0,
              height: 64.0,
              top: 8.0,
              left: 8.0,
              child: FloatingActionButton(
                onPressed: () {
                  final childByIndexState = childByIndexKey.currentState;

                  if (childByIndexState == null) return;

                  childByIndexState.index =
                      (childByIndexState.index + 1) % children.length;
                },
                child: const Icon(Icons.ac_unit),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

问题

AnimatedSize小部件只有在从较小的尺寸转换到较大的尺寸时才能工作。当从较大的尺寸转换到较小的尺寸时,它会跳过(没有动画)。我该如何解决这个问题?
AnimatedContainer需要一个widthheight来实现动画,但我不想指定任何内容(目标是固有大小),并且在构建方法完成之前,这些大小不可用。

ttcibm8c

ttcibm8c1#

解决方案:

一个自定义MultiChildRenderObjectWidget,它使用selectedIndex参数从其children中查找匹配的子级,然后根据SizeTween动画设置其大小,该动画从以前选定的子级(如果有)的大小开始,到新选定的子级的大小结束。

输出:

注意事项:

下面是一个可以实现的特性的不完整列表。我把它们留给希望使用代码的任何人作为练习。

  • 这可以通过使用List<Widget>而不是ContainerRenderObjectMixin提供的类似于子访问的链表来优化。
  • 子切换过程可以使用构建器方法来完成,也可以使用先前构建的方法和用于某些子的构建器的混合来完成。
  • 自定义过渡可以通过绘画回调或处理小部件绘画的对象进行参数化。
    **源代码:**源代码文档相当完整,无需进一步解释即可阅读.

AnimatedSizeSwitcherParentData对于当前的实现不是必需的。它对于添加其他行为很有用,例如激活当前所选子对象的位置/偏移。

/// Every child of a [MultiChildRenderObject] has [ParentData].
/// Since this is a container-type of widget, we can used the predefined
/// [ContainerBoxParentData] which allows children to have access to their
/// previous and next sibling.
///
/// * See [ContainerBoxParentData].
/// * See [ContainerParentDataMixin].
class AnimatedSizeSwitcherParentData extends ContainerBoxParentData<RenderBox> {}

IntrinsicSizeSwitcher及其相关的RenderIntrinsicSizeSwitcher提供此功能。

/// A widget sizes itself according to the size of its selected child.
class IntrinsicSizeSwitcher extends MultiChildRenderObjectWidget {
  IntrinsicSizeSwitcher({
    Key? key,
    // Since this is a [MultiChildRenderObjectWidget], we pass the children
    // to the super constructor.
    required AnimationController animationController,
    required Curve curve,
    required int selectedIndex,
    required List<Widget> children,
  })  : assert(selectedIndex >= 0),
        _animationController = animationController,
        _animation = CurvedAnimation(parent: animationController, curve: curve),
        _selectedIndex = selectedIndex,
        super(key: key, children: children);

  final AnimationController _animationController;
  final CurvedAnimation _animation;
  final int _selectedIndex;

  @override
  RenderObject createRenderObject(BuildContext context) {
    // The custom [RenderObject] that we define to create our custom widget.
    return RenderIntrinsicSizeSwitcher(
      animationController: _animationController,
      animation: _animation,
      selectedIndex: _selectedIndex,
    );
  }

  @override
  void updateRenderObject(
    BuildContext context,
    RenderIntrinsicSizeSwitcher renderObject,
  ) {
    /// The [RenderObject] is updated when [selectedIndex] changes, so that
    /// the old child can be replaced by the new child.
    renderObject.selectedIndex = _selectedIndex;
  }
}

class RenderIntrinsicSizeSwitcher extends RenderBox
    with ContainerRenderObjectMixin<RenderBox, AnimatedSizeSwitcherParentData> {
  RenderIntrinsicSizeSwitcher({
    required AnimationController animationController,
    required CurvedAnimation animation,
    required int selectedIndex,
  })  : _animationController = animationController,
        _animation = animation,
        _selectedIndex = selectedIndex {
    _onLayout = _onFirstLayout;

    /// Listen to animation changes so that the layout of this [RenderBox] can
    /// be adjusted according to [animationController.value].
    animationController.addListener(
      () {
        if (_lastValue != animationController.value) markNeedsLayout();
      },
    );
  }

  final AnimationController _animationController;
  final CurvedAnimation _animation;

  /// A [SizeTween] whose [begin] is the size of the previous child, and
  /// whose [end] is the size of the next child.
  final SizeTween _sizeTween = SizeTween();

  /// Called by [performLayout]. This method is initialized to [_onFirstLayout]
  /// so that the first child can be found by index and laid out.
  ///
  /// After [_onFirstLayout] completes, additional layout passes have two
  /// possibilities: the selected child changed or the current child is being
  /// used to update the size of this [RenderBox].
  late void Function() _onLayout;

  int _selectedIndex;
  int get selectedIndex => _selectedIndex;
  set selectedIndex(int value) {
    assert(selectedIndex >= 0);

    /// Update [_selectedIndex] if it is different from [value], because this
    /// method restarts [_animationController], which calls [markNeedsLayout].
    if (_selectedIndex == value) return;

    _selectedIndex = value;

    /// No need to call [markNeedsLayout] because this [RenderBox] is a
    /// listener of [_animationController].
    ///
    /// The listener callback calls [markNeedsLayout] whenever [_lastValue]
    /// differs from [_animationController.value] in order to use the new
    /// animation value to update the layout.
    _animationController.forward(from: .0);
  }

  @override
  bool get sizedByParent => false;

  Pair<RenderBox?, Size>? _oldSelection;
  Pair<RenderBox?, Size>? _selection;
  double? _lastValue;

  @override
  void setupParentData(covariant RenderObject child) {
    if (child.parentData is AnimatedSizeSwitcherParentData) return;
    child.parentData = AnimatedSizeSwitcherParentData();
  }

  Pair<RenderBox?, Size> _findSelectedChildAndDryLayout() {
    var child = firstChild;
    var index = 0;

    /// Find the child matching [selectedIndex].
    while (child != null) {
      if (index == selectedIndex) {
        return Pair(first: child, second: child.computeDryLayout(constraints));
      }

      var childParentData = child.parentData as AnimatedSizeSwitcherParentData;
      child = childParentData.nextSibling;
      ++index;
    }

    /// No matching child was found.
    return const Pair(first: null, second: Size.zero);
  }

  /// Find the child corresponding to [selectedIndex] and perform a wet layout.
  Pair<RenderBox?, Size> _findSelectedChildAndWetLayout() {
    var child = firstChild;
    var index = 0;

    while (child != null) {
      if (index == selectedIndex) {
        return Pair(
          first: child,
          second: wetLayoutSizeComputer(child, constraints),
        );
      }

      var childParentData = child.parentData as AnimatedSizeSwitcherParentData;
      child = childParentData.nextSibling;
      ++index;
    }

    return const Pair(first: null, second: Size.zero);
  }

  @override
  void performLayout() {
    _lastValue = _animationController.value;
    _onLayout();
  }

  @override
  Size computeDryLayout(BoxConstraints constraints) {
    final child = _selection?.first;
    if (child == null) return Size.zero;
    return child.getDryLayout(constraints);
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
    final child = _selection?.first;
    if (child == null) return .0;
    return child.getMaxIntrinsicWidth(height);
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
    final child = _selection?.first;
    if (child == null) return .0;
    return child.getMaxIntrinsicHeight(width);
  }

  @override
  bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
    final child = _selection?.first;

    /// If there is no current child, then there is nothing to hit test.
    if (child == null) return false;

    var childParentData = child.parentData as BoxParentData;

    /// This [RenderBox] only displays one child at a time, so it only needs to
    /// hit test the child being displayed.
    final isHit = result.addWithPaintOffset(
      offset: childParentData.offset,
      position: position,
      hitTest: (BoxHitTestResult result, Offset transformed) {
        assert(transformed == position - childParentData.offset);
        return child.hitTest(result, position: transformed);
      },
    );

    return isHit;
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    final child = _selection?.first;

    /// If there is no current child, then there is nothing to paint.
    if (child == null) return;

    /// Clip the painted area to [size], which is set to the value of
    /// [animatedSize] during layout; this prevents having to resizing the old
    /// child which can cause visual overflows.
    context.pushClipRect(
      true,
      offset,
      Offset.zero & size,
      (PaintingContext context, Offset offset) {
        /// The animation dependent alpha value is used to fade out the old
        /// child and fade in the current child.
        final alpha = (_animationController.value * 255).toInt();

        final oldChild = _oldSelection?.first;

        /// If there is an old child, paint it first, so that it is painted
        /// below the current child.
        ///
        /// We only want to paint the old child if the animation is running.
        /// Once the animation is completed, the old child is fully transparent.
        /// Subsequently, it is no longer necessary to paint it.
        if (oldChild != null && _animationController.isAnimating) {
          context.pushOpacity(
            offset,
            255 - alpha,
            (PaintingContext context, Offset offset) {
              final childOffset = (oldChild.parentData as BoxParentData).offset;
              context.paintChild(oldChild, childOffset + offset);
            },
          );
        }

        context.pushOpacity(
          offset,
          alpha,
          (PaintingContext context, Offset offset) {
            final childOffset = (child.parentData as BoxParentData).offset;
            context.paintChild(child, childOffset + offset);
          },
        );
      },
    );
  }

  @override
  double? computeDistanceToActualBaseline(TextBaseline baseline) {
    assert(!debugNeedsLayout);

    final child = _selection?.first;

    if (child == null) return null;

    final result = child.getDistanceToActualBaseline(baseline);

    if (result == null) return null;

    return result + (child.parentData as BoxParentData).offset.dy;
  }

  /// Calculates the animated size of this [RenderBox].
  void _performLayout(RenderBox child) {
    final childParentData = child.parentData as AnimatedSizeSwitcherParentData;

    final animatedSize = _sizeTween.evaluate(_animation)!;
    final childSize = wetLayoutSizeComputer(child, constraints);

    final oldChild = _oldSelection?.first;

    if (oldChild != null) {
      final oldChildSize = wetLayoutSizeComputer(oldChild, constraints);
      final oldChildParentData = oldChild.parentData as BoxParentData;

      /// Center the old child.
      oldChildParentData.offset = Offset(
        (animatedSize.width - oldChildSize.width) / 2.0,
        (animatedSize.height - oldChildSize.height) / 2.0,
      );
    }

    /// Center the new child.
    childParentData.offset = Offset(
      (animatedSize.width - childSize.width) / 2.0,
      (animatedSize.height - childSize.height) / 2.0,
    );

    size = animatedSize;
  }

  void _onFirstLayout() {
    /// The first layout pass must find the selected child by index and perform
    /// a wet layout.
    final selectedChild = _findSelectedChildAndWetLayout();

    /// If [selection.first] is null, then the child list is empty, so there's
    /// nothing to lay out, and [Size.zero] is returned.
    if (selectedChild.first == null) {
      size = Size.zero;
      return;
    }

    /// Since this is the first pass, [_sizeTween.begin] and [_sizeTween.end]
    /// will just be set to the size of the currently selected child.
    _sizeTween.begin = _sizeTween.end = selectedChild.second;

    /// The selection is updated to the child matching [selectedChildIndex].
    _selection = selectedChild;

    /// Subsequent layout passes are ensured to have a selected child.
    _onLayout = _onUpdateLayout;

    /// The size is set to the size of the selected child.
    size = selectedChild.second;
  }

  void _onUpdateLayout() {
    /// A dry layout is needed just to get the size of the selected child.
    final newSelection = _findSelectedChildAndDryLayout();

    /// After [_onFirstLayout], it is safe to assume that [_selection] is not
    /// null and that the child that has gone through the wet layout process.
    final child = _selection!.first!;

    /// If the selection is the same, perform a layout pass.
    if (child == newSelection.first) {
      _performLayout(child);
    } else {
      /// The selected child index has changed.

      /// The size animation will begin from the current child's size.
      _sizeTween.begin = child.size;

      /// The size animation will end at the new child's size.
      _sizeTween.end = newSelection.second;

      assert(newSelection.first != null);

      _performLayout(newSelection.first!);

      /// Update the old and new children selection state.
      _oldSelection = _selection;
      _selection = newSelection;
    }
  }
}

一个方便的 Package 器,使使用IntrinsicSizeSwitcher小部件变得简单:

/// Convenience wrapper around an [IntrinsicSizeSwitcher].
class AnimatedSizeSwitcher extends StatefulWidget {
  const AnimatedSizeSwitcher({
    Key? key,
    this.duration = const Duration(milliseconds: 300),
    this.curve = Curves.linear,
    this.initialChildIndex = 0,
    required this.children,
  }) : super(key: key);

  final Duration duration;
  final Curve curve;
  final int initialChildIndex;
  final List<Widget> children;

  @override
  State<AnimatedSizeSwitcher> createState() => AnimatedSizeSwitcherState();
}

class AnimatedSizeSwitcherState extends State<AnimatedSizeSwitcher>
    with SingleTickerProviderStateMixin {
  late final AnimationController _animationController;
  late int _index;

  @override
  void initState() {
    super.initState();

    _animationController =
        AnimationController(vsync: this, duration: widget.duration);

    _index = widget.initialChildIndex;

    /// The [_animationController] is started at 1.0 so that the first child is
    /// visible, because [RenderIntrinsicSizeSwitcher.paint] uses
    /// [_animationController.value] as an opacity factor.
    SchedulerBinding.instance.addPostFrameCallback(
      (timeStamp) => _animationController.forward(from: 1.0),
    );
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return IntrinsicSizeSwitcher(
      animationController: _animationController,
      curve: widget.curve,
      selectedIndex: _index,
      children: widget.children,
    );
  }

  /// Rebuilds the widget by setting the selected index to whatever is next,
  /// wrapping back to 0 when the end is reached.
  void nextChild() {
    setState(() => _index = (_index + 1) % widget.children.length);
  }
}

实用程序类:
Pair<T, E>包含两个值。

class Pair<T, E> {
  final T first;
  final E second;

  const Pair({required this.first, required this.second});

  @override
  String toString() => "first = $first, second = $second";
}

SizeComputer<T>类用于抽象对RenderBox.getDryLayoutRenderBox.layout的调用。

abstract class SizeComputer<T> {
  const SizeComputer();

  Size call(T item, BoxConstraints constraints);
}

class DryLayoutSizeComputer extends SizeComputer<RenderBox> {
  const DryLayoutSizeComputer();

  @override
  Size call(RenderBox item, BoxConstraints constraints) {
    final size = item.getDryLayout(constraints);
    assert(size.isFinite);
    return size;
  }
}

class WetLayoutSizeComputer extends SizeComputer<RenderBox> {
  const WetLayoutSizeComputer();

  @override
  Size call(RenderBox item, BoxConstraints constraints) {
    item.layout(constraints, parentUsesSize: true);
    assert(item.hasSize);
    return item.size;
  }
}

const dryLayoutSizeComputer = DryLayoutSizeComputer();
const wetLayoutSizeComputer = WetLayoutSizeComputer();

测试应用程序:

void main() => runApp(const MaterialApp(home: AnimatedSizeSwitcherApp()));

class AnimatedSizeSwitcherApp extends StatelessWidget {
  const AnimatedSizeSwitcherApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final key = GlobalKey<_AnimatedSizeTestState>();

    return Scaffold(
      body: GestureDetector(
        onTap: () => key.currentState?.displayNextChild(),
        child: Center(child: AnimatedSizeTest(key: key)),
      ),
    );
  }
}

class AnimatedSizeTest extends StatefulWidget {
  const AnimatedSizeTest({Key? key}) : super(key: key);

  @override
  State<AnimatedSizeTest> createState() => _AnimatedSizeTestState();
}

class _AnimatedSizeTestState extends State<AnimatedSizeTest> {
  static const _decoration = BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.all(Radius.circular(6.0)),
  );

  late final children = <Widget>[
    Container(
      decoration: _decoration.copyWith(color: Colors.red),
      width: 110,
      height: 100,
    ),
    Container(
      decoration: _decoration.copyWith(color: Colors.green),
      width: 350,
      height: 200,
    ),
    Container(
      decoration: _decoration.copyWith(color: Colors.blue),
      width: 200,
      height: 250,
    ),
    Container(
      decoration: _decoration.copyWith(color: Colors.amber),
      width: 300,
      height: 350,
    ),
  ];

  final _switcherKey = GlobalKey<AnimatedSizeSwitcherState>();

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(4.0),
      decoration: _decoration.copyWith(
        boxShadow: [
          const BoxShadow(
            color: Colors.black38,
            spreadRadius: 1.0,
            blurRadius: 2.0,
          ),
        ],
      ),
      child: AnimatedSizeSwitcher(
        key: _switcherKey,
        curve: Curves.easeOutQuad,
        initialChildIndex: 0,
        children: children,
      ),
    );
  }

  void displayNextChild() {
    final switcherState = _switcherKey.currentState;
    if (switcherState == null) return;
    switcherState.nextChild();
  }
}

相关问题