Как получить высоту виджета?
Я не понимаю, как LayoutBuilder используется для получения высоты виджета.
Мне нужно отобразить список виджетов и получить их высоту, чтобы я мог рассчитать некоторые специальные эффекты прокрутки. Я разрабатываю пакет, а другие разработчики предоставляют виджет (я не контролирую их). Я прочитал, что LayoutBuilder может быть использован для получения высоты.
В очень простом случае я попытался обернуть Widget в LayoutBuilder.builder и поместить его в стек, но я всегда получаю minHeight
0.0
, а также maxHeight
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, который равен бесконечности и вылетает из приложения.
В документах даже сказано:
... но размер родителя не может зависеть от размера ребенка.
И это странное ограничение.
16 ответов
Чтобы получить размер / положение виджета на экране, вы можете использовать GlobalKey
чтобы получить его BuildContext
чтобы затем найти RenderBox
этого конкретного виджета, который будет содержать его глобальную позицию и размер рендеринга.
Остается только одна вещь: этот контекст может не существовать, если виджет не отображается. Что может вызвать проблемы с ListView, поскольку виджеты отображаются, только если они потенциально видимы.
Другая проблема в том, что вы не можете получить виджет RenderBox
в течение build
call, поскольку виджет еще не обработан.
Но мне нужно размер во время сборки! Что я могу сделать?
Есть один классный виджет, который может помочь: Overlay
И его OverlayEntry
, Они используются для отображения виджетов поверх всего остального (аналогично стеку).
Но самое классное то, что они находятся на другом build
течь; они построены после обычных виджетов.
Это имеет очень крутое значение: OverlayEntry
может иметь размер, который зависит от виджетов реального дерева виджетов.
Хорошо. Но не требует ли OverlayEntry восстановления вручную?
Да, они делают. Но есть еще одна вещь, о которой нужно знать: ScrollController
передал Scrollable
, является слушаемым похож на AnimationController
,
Что означает, что вы могли бы объединить AnimatedBuilder
с ScrollController
, Это будет иметь прекрасный эффект, чтобы автоматически перестроить ваш виджет при прокрутке. Идеально подходит для этой ситуации, верно?
Объединяем все в пример:
В следующем примере вы увидите наложение, которое следует за виджетом внутри ListView
и разделяет ту же высоту.
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final controller = new ScrollController();
OverlayEntry sticky;
GlobalKey stickyKey = new GlobalKey();
@override
void initState() {
if (sticky != null) {
sticky.remove();
}
sticky = new 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 new Scaffold(
body: new ListView.builder(
controller: controller,
itemBuilder: (context, index) {
if (index == 6) {
return new Container(
key: stickyKey,
height: 100.0,
color: Colors.green,
child: new Text("I'm fat"),
);
}
return new ListTile(
title: new Text('Hello $index'),
);
},
),
);
}
Widget stickyBuilder(BuildContext context) {
return new AnimatedBuilder(
animation: controller,
builder: (_, child) {
final keyContext = stickyKey.currentContext;
if (keyContext != null) {
// widget is visible
final RenderBox box = keyContext.findRenderObject();
final pos = box.localToGlobal(Offset.zero);
return new Positioned(
top: pos.dy + box.size.height,
left: 50.0,
right: 50.0,
height: box.size.height,
child: new Material(
child: new Container(
alignment: Alignment.center,
color: Colors.purple,
child: new Text("^ Nah I think you're okay"),
),
),
);
}
return new Offstage();
},
);
}
}
Это (я думаю) самый простой способ сделать это.
Скопируйте и вставьте в свой проект следующее.
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
typedef void OnWidgetSizeChange(Size size);
class MeasureSize extends StatefulWidget {
final Widget child;
final OnWidgetSizeChange onChange;
const MeasureSize({
Key key,
@required this.onChange,
@required this.child,
}) : super(key: key);
@override
_MeasureSizeState createState() => _MeasureSizeState();
}
class _MeasureSizeState extends State<MeasureSize> {
@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);
}
}
Затем просто оберните виджет, размер которого вы хотите измерить, MeasureSize
.
var myChildSize = Size.zero;
Widget build(BuildContext context) {
return ...(
child: MeasureSize(
onChange: (size) {
setState(() {
myChildSize = size;
});
},
child: ...
),
);
}
Так что да, размер родителя не может зависеть от размера ребенка, если вы достаточно стараетесь.
Личный анекдот - это удобно для ограничения размера таких виджетов, как Algin
, который любит занимать абсурдное количество места.
Вот пример того, как вы можете использовать LayoutBuilder
чтобы определить размер виджета.
поскольку
LayoutBuilder
widget может определять ограничения своего родительского виджета, один из вариантов его использования - иметь возможность адаптировать дочерние виджеты к размерам своих родительских.
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
Я делаю это так
class SizeProviderWidget extends StatefulWidget {
final Widget child;
final Widget Function(Size) onChildSize;
const SizeProviderWidget({Key key, this.onChildSize, this.child})
: super(key: key);
@override
_SizeProviderWidgetState createState() => _SizeProviderWidgetState();
}
class _SizeProviderWidgetState extends State<SizeProviderWidget> {
Widget _upperChild = SizedBox();
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_upperChild = widget.onChildSize(context.size);
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [widget.child, _upperChild],
);
}
}
использовать
SizeProviderWidget
и верните желаемый виджет из
onChildSize
который предоставляет вам визуализированный размер фактического дочернего элемента
Я сделал этот виджет простым решением без сохранения состояния:
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');
},
)
В тех случаях, когда вы не хотите ждать, пока фрейм получит размер, но хотите знать его, прежде чем включать его в свое дерево:
Самый простой способ — последовать примеру документации BuildOwner .
Со следующим вы можете просто сделать
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)
Если я правильно понимаю, вы хотите измерить размерность некоторых произвольных виджетов, и вы можете обернуть эти виджеты другим виджетом. В этом случае метод в этом ответе должен работать для вас.
В основном решение состоит в том, чтобы связать обратный вызов в жизненном цикле виджета, который будет вызываться после рендеринга первого кадра, оттуда вы можете получить доступ context.size
, Подвох в том, что вы должны обернуть виджет, который вы хотите измерить, в виджет с сохранением состояния. И, если вам абсолютно необходим размер в пределах build()
тогда вы можете получить к нему доступ только во втором рендере (он доступен только после первого рендера).
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,
),
],
),
);
}
}
Выход:
ht tps:https://stackru.com/images/27442d8a8e81aeb3552abe3dce0a63cc6b35dca6.png
Не существует прямого способа вычислить размер виджета, поэтому, чтобы обнаружить, что мы должны воспользоваться помощью контекста виджета.
Вызов context.size возвращает нам объект Size, который содержит высоту и ширину виджета. context.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: ()
Это ответ Реми с нулевой безопасностью, поскольку очередь редактирования заполнена, я должен опубликовать его здесь.
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();
},
);
}
Самый простой способ - использовать MeasuredSize, это виджет, который вычисляет размер своего дочернего элемента во время выполнения.
Вы можете использовать это так:
MeasuredSize(
onChange: (Size size) {
setState(() {
print(size);
});
},
child: Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
);
Вы можете найти его здесь: https://pub.dev/packages/measured_size
Используйте пакет: 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)}');
},
),
Это легко и все еще можно сделать в 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);
});
}
}
таким же образом вы можете рассчитать высоту любого дочернего виджета и прокрутить его до этой позиции.
**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()
})
<p>@override
<br/>
Widget build(BuildContext context) {<br/>
return Container(<br/>
width: MediaQuery.of(context).size.width,<br/>
height: MediaQuery.of(context).size.height,<br/>
color: Colors.blueAccent,<br/>
);<br/>
}</p>