如何获得一个部件的高度?

我不明白如何使用 LayoutBuilder来获得 Widget 的高度。

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

在一个非常简单的例子中,我试图将 Widget 包装在 LayoutBuilder.builder 中并将其放入 Stack 中,但是我总是得到 minHeight 0.0maxHeight INFINITY。我在滥用 LayoutBuilder 吗?

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

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

我的问题是 SingleChildLayoutGenerategetSize的操作就像它需要返回视图的高度一样。那个时候我不知道一个孩子的身高。我只能返回 straints.small (0,高度为0) ,或者 straints.best (无穷大) ,这会导致应用程序崩溃。

文件中甚至写道:

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

这是个奇怪的限制。

204990 次浏览

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

只有一件事需要注意: 如果不呈现小部件,那么上下文可能不存在。这可能会导致 ListView出现问题,因为只有在可能可见的情况下才呈现小部件。

另一个问题是,在 build调用期间无法获得小部件的 RenderBox,因为小部件尚未呈现。


但是,如果我需要在构建过程中得到的大小! 我能做什么?

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

但是最酷的是,它们处于不同的 build流上; 它们是构建在 之后常规小部件上的。

这有一个超级酷的含义: OverlayEntry可以有一个依赖于实际小部件树的小部件的大小。


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

是的,没错。但是还有另外一件事需要注意: 传递给 ScrollableScrollController是一个类似于 AnimationController的可监听程序。

这意味着你可以把 AnimatedBuilderScrollController结合起来,这样就可以在滚动条上自动重建你的小部件了。很适合这种情况,对吧?


将所有事情结合成一个例子:

在下面的示例中,您将看到一个覆盖层,它紧跟在 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();

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

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

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

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

更新: 使用 RenderProxyBox可以得到稍微正确一些的实现,因为它是在子代及其后代的每次重新构建中调用的,而顶级 build ()方法并不总是这样。

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

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




typedef void OnWidgetSizeChange(Size size);


class MeasureSizeRenderObject extends RenderProxyBox {
Size? oldSize;
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);
}


@override
void updateRenderObject(
BuildContext context, covariant MeasureSizeRenderObject renderObject) {
renderObject.onChange = onChange;
}
}

然后,简单地包装要用 MeasureSize测量其大小的小部件。

var myChildSize = Size.zero;


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

所以是的,如果你足够努力的话,父 不能 可以的大小取决于子 可以的大小。


个人轶事-这对于限制像 Align这样的小部件的大小很方便,因为它喜欢占用荒谬的空间。

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,
),
],
),
);
}
}

产出:

enter image description here

使用包: 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)}');
},
),

下面是一个关于如何使用 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),
),
);
}
}

演示

demo

日志

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

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

@override
Widget build(BuildContext context) {
var screenSize = MediaQuery.of(context).size ;
if (screenSize.width > layoutSize){
return Widget();
} else {
return Widget(); /// Widget if doesn't match the size
}
}

最简单的方法是使用 尺寸,它是一个小部件,可以在运行时计算子部件的大小。

你可以这样使用它:

MeasuredSize(
onChange: (Size size) {
setState(() {
print(size);
});
},
child: Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
);

你可以在这里找到它: https://pub.dev/packages/measured_size

让我给你一个小工具


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;
}
}


剪辑 只需将 SizeProviderWidgetOrientationBuilder包装起来,使其尊重设备的方向即可

这很简单,仍然可以在 StatelessWidget 中完成。

class ColumnHeightWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final scrollController = ScrollController();
final columnKey = GlobalKey();


_scrollToCurrentProgress(columnKey, scrollController);


return Scaffold(
body: SingleChildScrollView(
controller: scrollController,
child: Column(
children: [],
),
),
);
}


void _scrollToCurrentProgress(GlobalKey<State<StatefulWidget>> columnKey,
ScrollController scrollController) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final RenderBox renderBoxRed =
columnKey.currentContext.findRenderObject();
final height = renderBoxRed.size.height;
scrollController.animateTo(percentOfHeightYouWantToScroll * height,
duration: Duration(seconds: 1), curve: Curves.decelerate);
});
}
}

以同样的方式,您可以计算任何小部件的子高度并滚动到该位置。

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

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,
);
}
}

像这样使用它

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

没有直接的方法可以计算小部件的大小,所以我们必须借助小部件的上下文。

调用 context.Size 将返回 Size 对象,该对象包含小部件的高度和宽度。Size 计算小部件的呈现框并返回大小。

检查 https://medium.com/flutterworld/flutter-how-to-get-the-height-of-the-widget-be4892abb1a2

也许这个能帮上忙

在 Flutter 上测试: 2.2.3

在项目中复制下面的代码。

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

class WidgetSize extends StatefulWidget {
final Widget child;
final Function onChange;
    

const WidgetSize({
Key? key,
required this.onChange,
required this.child,
}) : super(key: key);
    

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

class _WidgetSizeState extends State<WidgetSize> {
@override
Widget build(BuildContext context) {
SchedulerBinding.instance!.addPostFrameCallback(postFrameCallback);
return Container(
key: widgetKey,
child: widget.child,
);
}
    

var widgetKey = GlobalKey();
var oldSize;
    

void postFrameCallback(_) {
var context = widgetKey.currentContext;
if (context == null) return;
    

var newSize = context.size;
if (oldSize == newSize) return;
    

oldSize = newSize;
widget.onChange(newSize);
}
}

声明一个变量来存储 大小

Size mySize = Size.zero;

添加以下代码以获取大小:

 child: WidgetSize(
onChange: (Size mapSize) {
setState(() {
mySize = mapSize;
print("mySize:" + mySize.toString());
});
},
child: ()
**Credit to @Manuputty**


class OrigChildWH extends StatelessWidget {
final Widget Function(BuildContext context, Size size, Widget? child) builder;
final Widget? child;
const XRChildWH({
Key? key,
required this.builder,
this.child,
}) : super(key: key);


@override
Widget build(BuildContext context) {
return OrientationBuilder(builder: (context, orientation) {
return ChildSizeNotifier(builder: builder);
});
}
}


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,
);
}
}


**Simple to use:**


OrigChildWH(
builder: (context, size, child) {
//Your child here: mine:: Container()
return Container()
})

这是雷米的回答与空安全,因为编辑队列已满,我必须在这里发布它。

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


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


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


@override
void initState() {
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) => 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) => AnimatedBuilder(
animation: controller,
builder: (_, Widget? child) {
final keyContext = stickyKey.currentContext;
if (keyContext != null) {
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();
},
);
}

在这种情况下,你不想等待一个帧得到大小,但想知道它包括它在你的树:

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

你可以用下面的方法

final size = MeasureUtil.measureWidget(MyWidgetTree());
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';


/// Small utility to measure a widget before actually putting it on screen.
///
/// This can be helpful e.g. for positioning context menus based on the size they will take up.
///
/// NOTE: Use sparingly, since this takes a complete layout and sizing pass for the subtree you
/// want to measure.
///
/// Compare https://api.flutter.dev/flutter/widgets/BuildOwner-class.html
class MeasureUtil {
static Size measureWidget(Widget widget, [BoxConstraints constraints = const BoxConstraints()]) {
final PipelineOwner pipelineOwner = PipelineOwner();
final _MeasurementView rootView = pipelineOwner.rootNode = _MeasurementView(constraints);
final BuildOwner buildOwner = BuildOwner(focusManager: FocusManager());
final RenderObjectToWidgetElement<RenderBox> element = RenderObjectToWidgetAdapter<RenderBox>(
container: rootView,
debugShortDescription: '[root]',
child: widget,
).attachToRenderTree(buildOwner);
try {
rootView.scheduleInitialLayout();
pipelineOwner.flushLayout();
return rootView.size;
} finally {
// Clean up.
element.update(RenderObjectToWidgetAdapter<RenderBox>(container: rootView));
buildOwner.finalizeTree();
}
}
}


class _MeasurementView extends RenderBox with RenderObjectWithChildMixin<RenderBox> {
final BoxConstraints boxConstraints;
_MeasurementView(this.boxConstraints);


@override
void performLayout() {
assert(child != null);
child!.layout(boxConstraints, parentUsesSize: true);
size = child!.size;
}


@override
void debugAssertDoesMeetConstraints() => true;
}


这将创建一个独立于主渲染树的全新渲染树,并且不会显示在屏幕上。

举个例子

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

会给你 Size(210.0, 24.0)