Flutter中Overlay中使用AnimatedPositioned时,坐标变换后没有动画效果

问题描述 投票:0回答:1

我尝试在 Flutter 中创建一个多选项切换器。

看起来像这样:

当我创建选择指示器(图中的橙色矩形)时,我认为需要为选择指示器添加变换动画(选择另一个选项后移动到另一个选项),所以我创建了一个 Stack 并添加了 ButtonsRow 和给孩子们选择指示器,将选择指示器放在底部,如图所示。

但我很快发现我需要根据Button的宽度设置选择指示器的宽度,因为不同语言的文本长度不同,使用固定宽度可能会导致溢出错误。

我在谷歌上搜索了有关获取小部件宽度的信息,并找到了这篇post。我使用了最上面评论中的方法,将Stack替换为Overlay,然后利用Overlay的特性,先渲染ButtonsRow,等待渲染后插入选择指示器。

更换Stack后,看起来很完美(如图),但是当我点击另一个按钮时,发现没有翻译动画,而是直接跳转到另一个按钮。

实际效果演示:

代码(简化版)

class AttributeSwitcher extends StatefulWidget {
  AttributeSwitcher(
      {super.key, required this.selected, required this.onSelected}) {
    if (selected < 0 || selected > 2) {
      throw Exception('selected must be in [0, 2]');
    }
  }

  final int selected;
  final void Function(int) onSelected;

  @override
  State<AttributeSwitcher> createState() => _AttributeSwitcherState();
}

class _AttributeSwitcherState extends State<AttributeSwitcher> {

  int _selected = 0;

  @override
  void initState() {
    super.initState();
    _selected = widget.selected;
  }

  void _updateSelected(int selected) {
    setState(() {
      _selected = selected;
    });
    widget.onSelected(selected);
  }

  Widget _createSticky({
    required double posX,
    required double posY,
    required double height,
    required double width,
    required ColorScheme colorScheme,
  }) {
    return AnimatedPositioned(
        duration: const Duration(milliseconds: 500),
        top: posY,
        left: posX,
        child: UnconstrainedBox(
          child: AnimatedContainer(
            height: height,
            width: width,
            decoration: BoxDecoration(
              color: colorScheme.primaryContainer,
              borderRadius: BorderRadius.circular(80),
            ),
            duration: const Duration(milliseconds: 500),
          ),
        ));
  }

  OverlayEntry _createRow({required List<GlobalKey> btnKeys}) {
    return OverlayEntry(
      builder: (context) => Row(
        mainAxisSize: MainAxisSize.min,
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          AttributeBtn(
              btnKey: btnKeys[0], text: '专注', onPressed: () => _updateSelected(0)),
          const AttributeSplitter(),
          AttributeBtn(
            btnKey: btnKeys[1],
            text: '小休息',
            onPressed: () => _updateSelected(1),
          ),
          const AttributeSplitter(),
          AttributeBtn(
            btnKey: btnKeys[2],
            text: '大休息',
            onPressed: () => _updateSelected(2),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    ThemeData theme = Theme.of(context);
    ColorScheme colorScheme = theme.colorScheme;
    TextTheme textTheme = theme.textTheme;

    List<GlobalKey> btnKeys = [GlobalKey(), GlobalKey(), GlobalKey()];
    List<Offset> btnPos = [];
    List<Size> btnSize = [];

    GlobalKey overlayKey = GlobalKey();

    OverlayEntry row = _createRow(btnKeys: btnKeys);

    SchedulerBinding.instance.addPostFrameCallback((_) {
      OverlayState state = overlayKey.currentState as OverlayState;

      if (overlayKey.currentContext == null) {
        throw Exception('overlayKey.currentContext is null');
      }

      var overlayPos =
          (overlayKey.currentContext?.findRenderObject() as RenderBox)
              .localToGlobal(Offset.zero);

      for (var element in btnKeys) {
        if (element.currentContext == null) {
          throw Exception('element.currentContext is null');
        }
        var readerBox = element.currentContext?.findRenderObject() as RenderBox;
        var readerSize = readerBox.size;
        var readerPos =
            readerBox.localToGlobal(Offset(-overlayPos.dx, -overlayPos.dy));

        btnPos.add(readerPos);
        btnSize.add(readerSize);
      }

      state.insert(OverlayEntry(builder: (context) {
        return _createSticky(
          posX: btnPos[_selected].dx,
          posY: btnPos[_selected].dy,
          height: btnSize[_selected].height,
          width: btnSize[_selected].width,
          colorScheme: colorScheme,
        );
      }), below: row);
    });

    return SizedBox(
      width: 300,
      height: 50,
      child: Overlay(key: overlayKey, initialEntries: [row]),
    );
  }
}

完整代码

在发这篇文章之前我尝试了很多方法,导致代码有点乱。原本AttributeSwitcher是一个StatelessWidget,选择切换逻辑是在父Widget中处理的。如果您回答我的问题,我将不胜感激!

我希望当我点击另一个选项时,选择指示器移动到另一个选项,而不是直接跳到另一个选项。

flutter flutter-animation
1个回答
0
投票

也尝试为动画容器提供曲线和持续时间

这是我使用的代码示例,效果很好

    AnimatedPositioned(
              curve: Curves.easeOutQuart,
              duration: const Duration(milliseconds: 800),
              left: _isGiveaway ? 10.w : (context.width / 2),
              top: 0,
              bottom: 0,
              right: _isGiveaway ? (context.width / 2) : 10.w,
              child: AnimatedContainer(
                curve: Curves.easeOutQuart,
                duration: const Duration(milliseconds: 800),
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(10),
                  color: context.primaryColor,
                ),
              ),
© www.soinside.com 2019 - 2024. All rights reserved.