溢出父部件小部件问题

3

我正在尝试创建一个小部件,它有一个按钮,每当按下该按钮时,该按钮下方会打开一个列表,填充按钮下方的所有空间。我使用简单的Column实现了它,类似于这样:

class _MyCoolWidgetState extends State<MyCoolWidget> {
  ...
  @override
  Widget build(BuildContext context) {
    return new Column(
      children: <Widget>[
        new MyButton(...),
        isPressed ? new Expanded(
          child: new SizedBox(
            width: MediaQuery.of(context).size.width,
            child: new MyList()
          )
        ) : new Container()
      ]
    )
  }
}

这在很多情况下都能正常工作,但并非所有情况都是如此。

我创建这个小部件时遇到的问题是,如果将MyCoolWidget与其他小部件(比如其他MyCoolWidget)一起放置在Row中,则列表会受到Row所限制的宽度的约束。
我尝试使用OverflowBox来解决这个问题,但不幸的是没有成功。

这个小部件与选项卡不同,因为它们可以放置在小部件树的任何位置,并且当按钮被按下时,列表将填充按钮下面的所有空间,即使这意味着忽略了约束条件。

以下图像代表了我想要实现的内容,在其中“BUTTON1”和“BUTTON2”或两者都是Row中的MyCoolWidgetenter image description here

编辑: 实际代码片段

class _MyCoolWidgetState extends State<MyCoolWidget> {

  bool isTapped = false;

  @override
  Widget build(BuildContext context) {
    return new Column(
      children: <Widget>[
        new SizedBox(
          height: 20.0,
          width: 55.0,
          child: new Material(
            color: Colors.red,
            child: new InkWell(
              onTap: () => setState(() => isTapped = !isTapped),
              child: new Text("Surprise"),
            ),
          ),
        ),
        bottomList()
      ],
    );
  }

  Widget comboList() {
    if (isTapped) {
      return new Expanded(
        child: new OverflowBox(
          child: new Container(
            color: Colors.orange,
            width: MediaQuery.of(context).size.width,
            child: new ListView( // Random list
              children: <Widget>[
                new Text("ok"),
                new Text("ok"),
                new Text("ok"),
                new Text("ok"),
                new Text("ok"),
                new Text("ok"),
                new Text("ok"),
                new Text("ok"),
                new Text("ok"),
                new Text("ok"),
                new Text("ok"),
                new Text("ok"),
                new Text("ok"),
              ],
            )
          )
        ),
      );
    } else {
      return new Container();
    }
  }
}

我将其使用方式如下:

我在这样使用它:

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Row(
      children: <Widget>[
        new Expanded(child: new MyCoolWidget()),
        new Expanded(child: new MyCoolWidget()),
      ]
    )
  }
}

这是代码实际执行的屏幕截图: 在这里输入图片描述

你能提供一个无法运行的代码链接吗?这样我们就可以自己复现错误了。 - Rémi Rousselet
@RémiRousselet 已添加! - Bram Vanbilsen
你能否发布包含按钮的“可运行”代码,而不是使用溢出的解决方案?溢出并不是解决问题的方法。 - Rémi Rousselet
@RémiRousselet 这是我正在使用的小部件代码,唯一的区别是我将我的按钮代码从单独的类中提取到了“Column”中,并创建了一个虚拟列表。我只在一行中有两个这样的小部件,如果添加行的代码是否有帮助? - Bram Vanbilsen
@rmtmckenzie 正是您刚才所描述的!我想我有责任确保底部有足够的空间。那里不应该有额外的检查。 - Bram Vanbilsen
显示剩余7条评论
1个回答

2
从评论中可以澄清,OP想要的是这个:
制作一个弹出窗口,覆盖整个屏幕,并从按钮在屏幕上的位置到屏幕底部,同时水平填充,无论按钮在屏幕的哪个位置。当按下按钮时,它也会切换打开/关闭状态。
有几种选项可以实现这个功能;最基本的方法应该是使用Dialog和showDialog,但是由于SafeArea的一些问题,这使得它很困难。此外,OP要求按钮切换而不是在对话框之外的任何地方按下(这就是对话框的作用 - 要么这样做,要么阻止对话框后面的触摸)。
这是一个可以实现类似功能的工作示例。完全声明 - 我并不表示这是一个好的做法,甚至是一个好的方法...但这是一种做法。
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

// We're extending PopupRoute as it (and ModalRoute) do a lot of things
// that we don't want to have to re-create. Unfortunately ModalRoute also
// adds a modal barrier which we don't want, so we have to do a slightly messy
// workaround for that. And this has a few properties we don't really care about.
class NoBarrierPopupRoute<T> extends PopupRoute<T> {
  NoBarrierPopupRoute({@required this.builder});

  final WidgetBuilder builder;

  @override
  Color barrierColor;

  @override
  bool barrierDismissible = true;

  @override
  String barrierLabel;

  @override
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
    return new Builder(builder: builder);
  }

  @override
  Duration get transitionDuration => const Duration(milliseconds: 100);

  @override
  Iterable<OverlayEntry> createOverlayEntries() sync* {
    // modalRoute creates two overlays - the modal barrier, then the
    // actual one we want that displays our page. We simply don't
    // return the modal barrier.
    // Note that if you want a tap anywhere that isn't the dialog (list)
    // to close it, then you could delete this override.
    yield super.createOverlayEntries().last;
  }

  @override
  Widget buildTransitions(
      BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
    // if you don't want a transition, remove this and set transitionDuration to 0.
    return new FadeTransition(opacity: new CurvedAnimation(parent: animation, curve: Curves.easeOut), child: child);
  }
}

class PopupButton extends StatefulWidget {
  final String text;
  final WidgetBuilder popupBuilder;

  PopupButton({@required this.text, @required this.popupBuilder});

  @override
  State<StatefulWidget> createState() => PopupButtonState();
}

class PopupButtonState extends State<PopupButton> {
  bool _active = false;

  @override
  Widget build(BuildContext context) {
    return new FlatButton(
      onPressed: () {
        if (_active) {
          Navigator.of(context).pop();
        } else {
          RenderBox renderbox = context.findRenderObject();
          Offset globalCoord = renderbox.localToGlobal(new Offset(0.0, context.size.height));
          setState(() => _active = true);
          Navigator
              .of(context, rootNavigator: true)
              .push(
                new NoBarrierPopupRoute(
                  builder: (context) => new Padding(
                        padding: new EdgeInsets.only(top: globalCoord.dy),
                        child: new Builder(builder: widget.popupBuilder),
                      ),
                ),
              )
              .then((val) => setState(() => _active = false));
        }
      },
      child: new Text(widget.text),
    );
  }
}

class MyApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => MyAppState();
}

class MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      home: new SafeArea(
        child: new Container(
          color: Colors.white,
          child: new Column(children: [
            new PopupButton(
              text: "one",
              popupBuilder: (context) => new Container(
                    color: Colors.blue,
                  ),
            ),
            new PopupButton(
              text: "two",
              popupBuilder: (context) => new Container(color: Colors.red),
            )
          ]),
        ),
      ),
    );
  }
}

对于更为奇特的建议,您可以参考此答案中描述的创建一个不受其父级位置限制的子级的方法来查找位置部分。
然而,无论您如何完成这个任务,最好不要将列表直接作为按钮的子级,因为Flutter中有很多东西依赖于子级的大小,使其能够扩展到全屏大小可能会很容易引起问题。

这个完全没问题!但是OverlayRoute类难道不比PopupRoute更适合吗? - Bram Vanbilsen
还有一个问题:yield super.createOverlayEntries().last; 这段代码具体是做什么的?它可能会获取最后一个 OverlayEntry,但那是否就是显示我的小部件的叠加层呢?如果是这样,那么这个叠加层最多只会有两个 OverlayEntry,一个用于模态屏障,另一个用于实际内容/小部件,这样理解对吗? - Bram Vanbilsen
1
PopupRoute是ModalRoute的子类,ModalRoute是TransitionRoute的子类,而TransitionRoute是OverlayRoute的子类。它们每个都添加了一些额外的功能-TransitionRoute用于转换,ModalRoute用于返回按钮和其他一些功能,虽然我可能可以跳过PopupRoute。这实际上是我对Flutter实现的一个不太满意之处,因为他们将路由的实现与长链相结合,每个实现一些东西,而不是说一个带有一堆混合的单个类。Tldr;如果您扩展OverlayRoute,则需要实现更多内容。 - rmtmckenzie
1
是的,那部分有点混乱,但ModalRoute应该始终返回2个覆盖条目——第一个是屏障,第二个是您实际想要显示的内容(因为最后一个总是在顶部)。使用yield只是我懒惰,我可以不使用sync*并返回一个仅包含超级可迭代对象中最后一个元素的列表/其他可迭代对象。 - rmtmckenzie

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接