如何获得小部件的高度?


81

我不明白如何LayoutBuilder用于获取小部件的高度。

我需要显示小部件列表并获取它们的高度,以便可以计算一些特殊的滚动效果。我正在开发一个程序包,其他开发人员提供了小部件(我不控制它们)。我读过LayoutBuilder可以用来获取高度。

在非常简单的情况下,我尝试将Widget包装在LayoutBuilder.builder中并将其放入堆栈中,但是我总是得到minHeight 0.0maxHeight INFINITY。我在滥用LayoutBuilder吗?

编辑:似乎LayoutBuilder是行不通的。我发现CustomSingleChildLayout几乎是一个解决方案。

我扩展了该委托,并且能够在getPositionForChild(Size size, Size childSize)方法中获得小部件的高度。但是,第一个调用的方法是Size getSize(BoxConstraints constraints),作为约束,我将0赋给INFINITY,因为我将这些CustomSingleChildLayouts放置在ListView中。

我的问题是SingleChildLayoutDelegate的getSize运行就像需要返回视图的高度一样。我当时不知道孩子的身高。我只能返回constraints.smallest(为0,高度为0),或者返回constraints.biggest,它为无穷大并导致应用崩溃。

在文档中甚至说:

...但是父母的大小不能取决于孩子的大小。

这是一个怪异的限制。


1
LayoutBuilder将为您提供父级的框约束。如果您想要孩子的大小,则需要不同的策略。我可以指出的一个示例是Wrap小部件,它根据相关RenderWrap类中子级的大小进行布局。不过,这是在布局过程中发生的,而不是build()。
乔纳·威廉姆斯

@JonahWilliams嗯。我不知道Wrap可以为我提供什么帮助,因为它是为在周围布置子对象而设计的小部件(可从网络上使用flexbox网格之类的工具)。我有一个需要找到其高度的子小部件。请查看问题中的修改。我几乎解决了CustomSingleChildLayout的问题,但遇到了限制。
古丁

你能更具体地解释你想要什么吗?有多种解决方案。但是每个都有不同的用例。
罗米·罗素(RémiRousselet)

当然。我正在开发一个程序包。用户/开发人员为我的班级提供了一个小部件。我们在这里谈论的是任何小部件,从new Text("hello")复杂到复杂。我将这些小部件放置到ListView中,并且需要它们的高度来计算一些滚动效果。我可以在布局时获取高度,就像SingleChildLayoutDelegate所做的一样。
古丁

“滚动效果”是什么意思?你有例子吗?
罗米·罗素

Answers:


146

要获取屏幕上小部件的大小/位置,您可以使用GlobalKey使其获取BuildContext,然后找到该RenderBox特定小部件的,其中将包含其全局位置和渲染的大小。

只需注意一件事:如果未呈现小部件,则该上下文可能不存在。ListView仅当小部件可能可见时才渲染它们,这可能会导致问题。

另一个问题是,由于尚未渲染小部件,因此您RenderBoxbuild通话期间无法获得小部件的。


但是我在构建时需要大小!我能做什么?

有一个很酷的小部件可以帮助您:Overlay及其及其OverlayEntry。它们用于在其他所有内容之上显示小部件(类似于堆栈)。

但是,最酷的是,它们的build流动方向不同。它们是常规小部件之后构建的。

具有一个超酷的含义:OverlayEntry其大小取决于实际小部件树的小部件。


好的。但是,OverlayEntry不需要手动重建吗?

是的,他们有。但是还有另一件事要注意:ScrollController传递给Scrollable,类似于,是可收听的AnimationController

这意味着您可以将AnimatedBuilder和组合使用ScrollController,这将具有在滚动中自动重建窗口小部件的可爱效果。非常适合这种情况,对吗?


将所有内容组合为示例:

在以下示例中,您将看到一个覆盖,该覆盖在其内部的小部件之后ListView并具有相同的高度。

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final controller = ScrollController();
  OverlayEntry sticky;
  GlobalKey stickyKey = GlobalKey();

  @override
  void initState() {
    if (sticky != null) {
      sticky.remove();
    }
    sticky = OverlayEntry(
      builder: (context) => stickyBuilder(context),
    );

    SchedulerBinding.instance.addPostFrameCallback((_) {
      Overlay.of(context).insert(sticky);
    });

    super.initState();
  }

  @override
  void dispose() {
    sticky.remove();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView.builder(
        controller: controller,
        itemBuilder: (context, index) {
          if (index == 6) {
            return Container(
              key: stickyKey,
              height: 100.0,
              color: Colors.green,
              child: const Text("I'm fat"),
            );
          }
          return ListTile(
            title: Text(
              'Hello $index',
              style: const TextStyle(color: Colors.white),
            ),
          );
        },
      ),
    );
  }

  Widget stickyBuilder(BuildContext context) {
    return AnimatedBuilder(
      animation: controller,
      builder: (_,Widget child) {
        final keyContext = stickyKey.currentContext;
        if (keyContext != null) {
          // widget is visible
          final box = keyContext.findRenderObject() as RenderBox;
          final pos = box.localToGlobal(Offset.zero);
          return Positioned(
            top: pos.dy + box.size.height,
            left: 50.0,
            right: 50.0,
            height: box.size.height,
            child: Material(
              child: Container(
                alignment: Alignment.center,
                color: Colors.purple,
                child: const Text("^ Nah I think you're okay"),
              ),
            ),
          );
        }
        return Container();
      },
    );
  }
}

注意

导航到其他屏幕时,后续通话将保持可见,否则将保持可见。

sticky.remove();

5
好的,终于有时间测试它了。因此,我实际上只需要第一部分。我不知道我可以使用访问context.height GlobalKey。好答案。
古丁

1
如何导入固定活页夹?
siddhesh

1
我已经尝试导入“ package:flutter / scheduler.dart”。但是我得到了错误的目标uri不存在@rémi-rousselet–
siddhesh

@rémi-rousselet当我在ListView后面有一个其高度要根据ListView的高度进行控制的小部件时,如何使此工作正常进行?
RoyalGriffin

2
Remi,感谢您的聪明解决方案和出色的解释。我有个问题。如果我们希望能够在按下它们时知道Recta的任何ListItem怎么办ListView.builderGlobalKey listItemKey = GlobalKey();为每个listItem设置都太过分了吗?假设我有+10000个物品。有没有解决性能/内存问题的聪明方法?
aytunch

38

(我认为)这是最简单的方法。

将以下内容复制粘贴到您的项目中。

更新:使用RenderProxyBox结果会实现更正确的实现,因为在每次对其子代及其子代进行重新构建时都会调用该方法,而对于顶层build()方法并不总是如此。

注意:正如Hixie此处指出的那样,这并不是一种有效的方法。但这是最简单的。

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

typedef void OnWidgetSizeChange(Size size);

class MeasureSizeRenderObject extends RenderProxyBox {
  Size oldSize;
  final OnWidgetSizeChange onChange;

  MeasureSizeRenderObject(this.onChange);

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

    Size newSize = child.size;
    if (oldSize == newSize) return;

    oldSize = newSize;
    WidgetsBinding.instance.addPostFrameCallback((_) {
      onChange(newSize);
    });
  }
}

class MeasureSize extends SingleChildRenderObjectWidget {
  final OnWidgetSizeChange onChange;

  const MeasureSize({
    Key key,
    @required this.onChange,
    @required Widget child,
  }) : super(key: key, child: child);

  @override
  RenderObject createRenderObject(BuildContext context) {
    return MeasureSizeRenderObject(onChange);
  }
}

然后,只需包装您想用其尺寸测量的小部件MeasureSize


var myChildSize = Size.zero;

Widget build(BuildContext context) {
  return ...( 
    child: MeasureSize(
      onChange: (size) {
        setState(() {
          myChildSize = size;
        });
      },
      child: ...
    ),
  );
}

所以,是的,父的大小不能 可以,如果你足够努力取决于儿童的体形。


个人轶事-这对于限制小部件的大小非常方便 Align,它喜欢占用大量的空间。


您到这里来的感觉很棒。没有疯狂的解释..只是代码有效。谢谢!
丹尼·达格拉斯

很棒的解决方法兄弟。将其作为发布包。
严厉的Bhikadia

对于许多高度和宽度问题,它都是非常好的解决方案,谢谢
alireza daryani

这可行,但是在某些地方很难使用。例如,在中PreferredSizeWidgetpreferredSize仅被调用一次,因此您无法通过简单的方式设置高度。
user2233706

好主意,但这在TextFormField上不起作用
Iworb

6

如果我理解正确,则需要测量一些任意小部件的尺寸,然后可以将这些小部件包装到另一个小部件中。在这种情况下,此答案中的方法应该适合您。

基本上,解决方案是在窗口小部件生命周期中绑定一个回调,在呈现第一帧之后将调用该回调,从那里您可以访问context.size。要注意的是,您必须将要测量的小部件包装在有状态的小部件中。而且,如果您绝对需要该尺寸,build()则只能在第二个渲染中访问它(仅在第一个渲染之后才可用)。


1
感谢您的指点,查看我完整的解决方案-stackoverflow.com/a/60868972/7061265
Dev Aggarwal

2

这是有关如何LayoutBuilder确定小部件尺寸的示例。

由于LayoutBuilder小部件能够确定其父小部件的约束,因此其用例之一就是能够使其子小部件适应其父级的尺寸。

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  var dimension = 40.0;

  increaseWidgetSize() {
    setState(() {
      dimension += 20;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(children: <Widget>[
          Text('Dimension: $dimension'),
          Container(
            color: Colors.teal,
            alignment: Alignment.center,
            height: dimension,
            width: dimension,
            // LayoutBuilder inherits its parent widget's dimension. In this case, the Container in teal
            child: LayoutBuilder(builder: (context, constraints) {
              debugPrint('Max height: ${constraints.maxHeight}, max width: ${constraints.maxWidth}');
              return Container(); // create function here to adapt to the parent widget's constraints
            }),
          ),
        ]),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: increaseWidgetSize,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

演示版

演示

日志

I/flutter (26712): Max height: 40.0, max width: 40.0
I/flutter (26712): Max height: 60.0, max width: 60.0
I/flutter (26712): Max height: 80.0, max width: 80.0
I/flutter (26712): Max height: 100.0, max width: 100.0

0

findRenderObject()返回RenderBox用于提供绘制的窗口小部件大小的,应该在构建窗口小部件树之后调用它,因此必须将其与某些回调机制或addPostFrameCallback()回调一起使用。

class SizeWidget extends StatefulWidget {
  @override
  _SizeWidgetState createState() => _SizeWidgetState();
}

class _SizeWidgetState extends State<SizeWidget> {
  final GlobalKey _textKey = GlobalKey();
  Size textSize;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) => getSizeAndPosition());
  }

  getSizeAndPosition() {
    RenderBox _cardBox = _textKey.currentContext.findRenderObject();
    textSize = _cardBox.size;
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Size Position"),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: <Widget>[
          Text(
            "Currern Size of Text",
            key: _textKey,
            textAlign: TextAlign.center,
            style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
          ),
          SizedBox(
            height: 20,
          ),
          Text(
            "Size - $textSize",
            textAlign: TextAlign.center,
          ),
        ],
      ),
    );
  }
}

输出:

在此处输入图片说明


0

使用包:z_tools。步骤:

1.更改主文件

void main() async {
  runZoned(
    () => runApp(
      CalculateWidgetAppContainer(
        child: Center(
          child: LocalizedApp(delegate, MyApp()),
        ),
      ),
    ),
    onError: (Object obj, StackTrace stack) {
      print('global exception: obj = $obj;\nstack = $stack');
    },
  );
}

2.在功能上使用

_Cell(
    title: 'cal: Column-min',
    callback: () async {
        Widget widget1 = Column(
        mainAxisSize: MainAxisSize.min,
        children: [
            Container(
            width: 100,
            height: 30,
            color: Colors.blue,
            ),
            Container(
            height: 20.0,
            width: 30,
            ),
            Text('111'),
        ],
        );
        // size = Size(100.0, 66.0)
        print('size = ${await getWidgetSize(widget1)}');
    },
),
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.