【问题标题】:How to get height of a Widget?如何获取 Widget 的高度?
【发布时间】:2018-08-24 17:59:21
【问题描述】:

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

我需要显示小部件列表并获取它们的高度,以便计算一些特殊的滚动效果。我正在开发一个包,其他开发人员提供小部件(我不控制它们)。我读到 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 中。

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

在文档中甚至说:

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

这是一个奇怪的限制。

【问题讨论】:

  • LayoutBuilder 将为您提供父级的框约束。如果你想要孩子的大小,你需要一个不同的策略。我可以指出的一个示例是 Wrap 小部件,它根据关联的 RenderWrap 类中的子项的大小进行布局。这发生在布局期间,而不是 build()。
  • @JonahWilliams 嗯。我看不出 Wrap 对我有什么帮助,因为它是设计用于在周围布局孩子的小部件(类似于网络上的 flexbox 网格)。我有一个需要查找高度的子小部件。请参阅问题中的编辑。我几乎解决了 CustomSingleChildLayout 的问题,但遇到了它的局限性。
  • 你能更具体地解释一下你想要什么吗?有多种解决方案。但每个都有不同的用例。
  • 当然。我正在开发一个包。用户/开发人员为我的班级提供了一个小部件。我们在这里讨论任何小部件,从new Text("hello") 到更复杂的小部件。我将这些小部件放入 ListView,我需要它们的高度来计算一些滚动效果。我可以在布局时获取高度,就像 SingleChildLayoutDelegate 正在做的那样。
  • “滚动效果”是什么意思?你有例子吗?

标签: flutter dart layout widget flutter-widget


【解决方案1】:

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

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

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


但是如果我需要在构建过程中获取大小怎么办!我能做什么?

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

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

这有一个非常酷的含义:OverlayEntry 可以具有取决于实际小部件树的小部件的大小。


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

是的,他们有。但还有一点需要注意:ScrollController,传递给Scrollable,是一个类似于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();

【讨论】:

  • 好了,终于有时间测试了。所以,我实际上只需要第一部分。我不知道我可以使用GlobalKey 访问 context.height。很好的答案。
  • 如何导入定时活页夹?
  • 我试过 import 'package:flutter/scheduler.dart.';但我得到错误目标 uri 不存在 @rémi-rousselet
  • @rémi-rousselet 当我想要根据 ListView 的高度控制高度的 ListView 后面有一个小部件时,我该如何进行这项工作?
  • Remi,感谢您的聪明解决方案和出色的解释。我有个问题。如果我们希望在按下 ListView.builder 的任何 ListItem 时能够知道它们的 Rect 怎么办。为每个 listItem 设置 GlobalKey listItemKey = GlobalKey(); 会不会有点过头了?假设我有 +10000 个项目。有没有一种聪明的方法可以在没有性能/内存问题的情况下进行管理?
【解决方案2】:

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

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

更新:使用RenderProxyBox 会产生更正确的实现,因为在每次重建子项及其后代时都会调用它,而顶级 build() 方法并非总是如此。

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

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 这样的小部件的大小很方便,它喜欢占用荒谬的空间。

【讨论】:

  • 厉害的解决方案兄弟。把它做成一个 pub 包。
  • 它非常适合解决许多高度和宽度问题,谢谢
  • 这可行,但在某些地方很难使用。例如,在PreferredSizeWidget 中,preferredSize 仅被调用一次,因此您无法轻松设置高度。
  • 嘿,我已经更新了实现以支持不调用 build() 进行重建的情况。希望这是更正确的。
  • 此解决方案不适用于 SliverAppBar。
【解决方案3】:

这里有一个示例,说明如何使用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

更新:你也可以使用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
  }
}

【讨论】:

  • 您能否详细说明导致问题的“动态大小”?你有我可以检查的最小复制品吗?
  • 要动态获取小部件的宽度/高度,您需要调用.findRenderObejct(),然后调用.sizeRenderBox box = widget.context.findRenderObject(); print(box.size);
  • 你也可以传递一个 GlobalKey 作为小部件的键,然后调用_myKey.currentContext.findRenderObject()
【解决方案4】:

让我给你一个小部件


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 包裹起来,使其尊重设备的方向

【讨论】:

  • 嗨!最初这似乎效果很好,但我发现了一个警告:尺寸没有随着设备方向的变化而更新。我怀疑这是因为有状态小部件的状态没有在设备旋转时重新初始化。
  • 嗨,flutter 像乐高积木一样模块化,只需用OrientationBuilder 包裹上面的 Widget,它开始尊重任何方向,我的意思是设备哈哈
  • 在 iOS 14.5 beta 上崩溃
【解决方案5】:

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

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

【讨论】:

【解决方案6】:

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

输出:

【讨论】:

  • _textKey.currentContext.size 就够了
【解决方案7】:

没有直接的方法来计算widget的大小,所以要找到我们必须借助widget的上下文。

调用 context.size 会返回 Size 对象,其中包含小部件的高度和宽度。 context.size 计算一个小部件的渲染框并返回大小。

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

【讨论】:

    【解决方案8】:

    这可能会有所帮助

    在 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

    Size mySize = Size.zero;
    

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

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

    【讨论】:

      【解决方案9】:

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

      【讨论】:

        【解决方案10】:

        最简单的方法是使用MeasuredSize,它是一个在运行时计算其子级大小的小部件。

        你可以这样使用它:

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

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

        【讨论】:

          【解决方案11】:

          这很简单,而且仍然可以在 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);
              });
            }
          }
          

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

          【讨论】:

            【解决方案12】:

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

            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;
              UjChildSizeNotifier({
                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');
              },
            )
            

            【讨论】:

              【解决方案13】:
              **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()
                })
              

              【讨论】:

              • 您的答案可以通过额外的支持信息得到改进。请edit 添加更多详细信息,例如引用或文档,以便其他人可以确认您的答案是正确的。你可以找到更多关于如何写好答案的信息in the help center
              猜你喜欢
              • 2018-10-11
              • 1970-01-01
              • 1970-01-01
              • 2014-07-08
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2023-02-26
              • 2011-07-12
              相关资源
              最近更新 更多