如何获取Widget的高度?

How to get height of a Widget?

我不明白 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 中。

我的问题是 SingleChildLayoutDelegate getSize 的运行就像它需要 return 视图的高度一样。我不知道那一刻child的高度。我只能 return constraints.smallest (即 0,高度为 0),或者 constraints.biggest 无穷大并使应用程序崩溃。

在文档中它甚至说:

...but the size of the parent cannot depend on the size of the child.

这是一个奇怪的限制。

要获取屏幕上某个小部件的 size/position,您可以使用 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();

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

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

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

Copy-paste将以下内容放入您的项目中。

更新:使用 RenderProxyBox 会导致更正确的实现,因为每次重建 child 及其后代时都会调用它,而 [=36= 并非总是如此] 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: ...
    ),
  );
}

所以是的,parent cannot can 的大小取决于 child如果你够努力的话。


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

findRenderObject() returns RenderBox 用于给定绘制的widget的大小,应该在widget树构建完成后调用,所以必须配合使用一些回调机制或 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,
          ),
        ],
      ),
    );
  }
}

输出:

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

演示

日志

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它是一个在运行时计算其子项大小的小部件。

你可以这样使用它:

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

编辑 只需用 OrientationBuilder 包裹 SizeProviderWidget 使其符合设备的方向

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

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

调用 context.size returns 我们 Size 对象,它包含小部件的高度和宽度。 context.size 计算小部件的渲染框和 returns 大小。

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

Might be this could help

Tested on 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: ()
**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()
  })