如何获得Widget的高度?

Gud*_*din 36 flutter

我不明白如何使用LayoutBuilder来获取Widget的高度.

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

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

编辑:看起来LayoutBuilder似乎不行.我发现CustomSingleChildLayout几乎是一个解决方案.

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

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

在文档中它甚至说:

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

这是一个奇怪的限制.

Rém*_*let 65

要在屏幕GlobalKey上获取窗口小部件的大小/位置,您可以使用它BuildContext来查找RenderBox特定窗口小部件,该窗口小部件将包含其全局位置和渲染大小.

需要注意的一件事是:如果未呈现窗口小部件,则可能不存在该上下文.这可能会导致ListView出现问题,因为窗口小部件只有在可能可见时才会呈现.

另一个问题是,由于窗口小部件尚未呈现,因此您无法RenderBoxbuild调用期间获取窗口小部件.


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

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

但最酷的是他们处于不同的build流程; 它们是常规小部件之后构建的.

这有一个超酷的含义:OverlayEntry可以有一个大小取决于实际小部件树的小部件.


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

是的,他们这样做.但还有另一件事需要注意:ScrollController传递给a Scrollable,是一种类似的可听AnimationController.

这意味着您可以将a AnimatedBuilder与a 组合在一起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(
      opaque: false,
      // lambda created to help working with hot-reload
      builder: (context) => stickyBuilder(context),
    );

    // not possible inside initState
    SchedulerBinding.instance.addPostFrameCallback((_) {
      Overlay.of(context).insert(sticky);
    });
    super.initState();
  }

  @override
  void dispose() {
    // remove possible overlays on dispose as they would be visible even after [Navigator.push]
    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();
      },
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

  • 好的,终于有时间测试了。所以,我实际上只需要第一部分。我不知道我可以使用 `GlobalKey` 访问 context.height。很好的答案。 (5认同)
  • 雷米,感谢您巧妙的解决方案和精彩的解释。我有个问题。如果我们希望在按下“ListView.builder”的任何 ListItem 时能够知道它们的“Rect”怎么办?为每个 listItem 设置 `GlobalKey listItemKey = GlobalKey();` 是否太过分了?假设我有+10000 件物品。有没有一种聪明的方法可以在没有性能/内存问题的情况下管理这个问题? (2认同)

Dev*_*wal 65

这是(我认为)最直接的方法。

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

更新: usingRenderProxyBox导致稍微更正确的实现,因为它会在每次重建子项及其后代时调用,而顶级 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);
  }
}
Run Code Online (Sandbox Code Playgroud)

然后,只需用 包裹您想要测量其大小的小部件MeasureSize


var myChildSize = Size.zero;

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

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


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

  • 兄弟的解决方案真棒。将其作为 pub 包。 (3认同)
  • 此解决方案不适用于 SliverAppBar。 (3认同)
  • 这可行,但在某些地方很难使用。例如,在“PreferredSizeWidget”中,“preferredSize”仅被调用一次,因此您无法以简单的方式设置高度。 (2认同)
  • 看看 Boxy,它封装了这种在完全渲染之前获取子级大小的策略。 (2认同)

Bri*_*tty 11

我将此小部件制作为一个简单的无状态解决方案:

class ChildSizeNotifier extends StatelessWidget {
  final ValueNotifier<Size> notifier = ValueNotifier(const Size(0, 0));
  final Widget Function(BuildContext context, Size size, Widget child) builder;
  final Widget child;
  ChildSizeNotifier({
    Key key,
    @required this.builder,
    this.child,
  }) : super(key: key) {}

  @override
  Widget build(BuildContext context) {
    WidgetsBinding.instance.addPostFrameCallback(
      (_) {
        notifier.value = (context.findRenderObject() as RenderBox).size;
      },
    );
    return ValueListenableBuilder(
      valueListenable: notifier,
      builder: builder,
      child: child,
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

像这样使用它

ChildSizeNotifier(
  builder: (context, size, child) {
    // size is the size of the text
    return Text(size.height > 50 ? 'big' : 'small');
  },
)
Run Code Online (Sandbox Code Playgroud)


Yad*_*adu 10

让我给你一个小部件


class SizeProviderWidget extends StatefulWidget {
  final Widget child;
  final Function(Size) onChildSize;

  const SizeProviderWidget(
      {Key? key, required this.onChildSize, required this.child})
      : super(key: key);
  @override
  _SizeProviderWidgetState createState() => _SizeProviderWidgetState();
}

class _SizeProviderWidgetState extends State<SizeProviderWidget> {
  @override
  void initState() {
    ///add size listener for first build
    _onResize();
    super.initState();
  }

  void _onResize() {
    WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
      if (context.size is Size) {
        widget.onChildSize(context.size!);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    ///add size listener for every build uncomment the fallowing
    ///_onResize();
    return widget.child;
  }
}

Run Code Online (Sandbox Code Playgroud)

编辑 只需将SizeProviderWidgetwith包裹起来OrientationBuilder以使其尊重设备的方向

  • 你好!一开始这看起来效果很好,但我发现了一个警告:尺寸不会随着设备方向的变化而更新。我怀疑这是因为有状态小部件的状态不会在设备旋转时重新初始化。 (2认同)
  • 嗨,flutter 和乐高一样模块化,只需用 `OrientationBuilder` 包装上面的 Widget,它就开始尊重任何方向,我的意思是设备哈哈 (2认同)

omn*_*sia 9

如果您不想等待框架获取大小,但想在将其包含到树中之前了解它:

\n

最简单的方法是遵循BuildOwner文档的示例。

\n

通过以下你可以做

\n
final size = MeasureUtil.measureWidget(MyWidgetTree());\n
Run Code Online (Sandbox Code Playgroud)\n
import \'package:flutter/material.dart\';\nimport \'package:flutter/rendering.dart\';\n\n/// Small utility to measure a widget before actually putting it on screen.\n///\n/// This can be helpful e.g. for positioning context menus based on the size they will take up.\n///\n/// NOTE: Use sparingly, since this takes a complete layout and sizing pass for the subtree you\n/// want to measure.\n///\n/// Compare https://api.flutter.dev/flutter/widgets/BuildOwner-class.html\nclass MeasureUtil {\n  static Size measureWidget(Widget widget, [BoxConstraints constraints = const BoxConstraints()]) {\n    final PipelineOwner pipelineOwner = PipelineOwner();\n    final _MeasurementView rootView = pipelineOwner.rootNode = _MeasurementView(constraints);\n    final BuildOwner buildOwner = BuildOwner(focusManager: FocusManager());\n    final RenderObjectToWidgetElement<RenderBox> element = RenderObjectToWidgetAdapter<RenderBox>(\n      container: rootView,\n      debugShortDescription: \'[root]\',\n      child: widget,\n    ).attachToRenderTree(buildOwner);\n    try {\n      rootView.scheduleInitialLayout();\n      pipelineOwner.flushLayout();\n      return rootView.size;\n    } finally {\n      // Clean up.\n      element.update(RenderObjectToWidgetAdapter<RenderBox>(container: rootView));\n      buildOwner.finalizeTree();\n    }\n  }\n}\n\nclass _MeasurementView extends RenderBox with RenderObjectWithChildMixin<RenderBox> {\n  final BoxConstraints boxConstraints;\n  _MeasurementView(this.boxConstraints);\n\n  @override\n  void performLayout() {\n    assert(child != null);\n    child!.layout(boxConstraints, parentUsesSize: true);\n    size = child!.size;\n  }\n\n  @override\n  void debugAssertDoesMeetConstraints() => true;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这将创建一个与主渲染树分开的全新渲染树,并且不会在屏幕上显示\xe2\x80\x99。

\n

例如

\n
  print(\n    MeasureUtil.measureWidget(\n      Directionality(\n        textDirection: TextDirection.ltr,\n        child: Row(\n          mainAxisSize: MainAxisSize.min,\n          children: const [\n            Icon(Icons.abc),\n            SizedBox(\n              width: 100,\n            ),\n            Text("Moin Meister")\n          ],\n        ),\n      ),\n    ),\n  );\n
Run Code Online (Sandbox Code Playgroud)\n

会给你Size(210.0, 24.0)

\n


Oma*_*att 8

这是关于如何使用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),
      ),
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

演示

演示

日志

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
Run Code Online (Sandbox Code Playgroud)

更新:您也可以使用MediaQuery来实现类似的功能。

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
Run Code Online (Sandbox Code Playgroud)


Gan*_*uan 6

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

基本上解决方案是在小部件生命周期中绑定一个回调,它将在第一帧渲染后调用,从那里你可以访问context.size. 问题是您必须将要测量的小部件包装在有状态小部件中。而且,如果您绝对需要其中的大小,build()那么您只能在第二次渲染中访问它(它仅在第一次渲染后可用)。


jit*_*555 5

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,
          ),
        ],
      ),
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

输出:

在此输入图像描述