Як отримати висоту віджета?


90

Я не розумію, як LayoutBuilderвикористовується для отримання висоти віджета.

Мені потрібно відобразити список віджетів і отримати їх висоту, щоб я міг обчислити деякі спеціальні ефекти прокрутки. Я розробляю пакет, а інші розробники надають віджет (я не контролюю їх). Я читав, що LayoutBuilder можна використовувати для отримання висоти.

У дуже простому випадку я намагався обернути віджет у 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, який є нескінченністю і виводить програму з ладу.

У документах навіть сказано:

... але розмір батьків не може залежати від розміру дитини.

І це дивне обмеження.


1
LayoutBuilder надасть вам обмеження для поля батьків. Якщо ви хочете розміри дитини, вам потрібна інша стратегія. Одним із прикладів, на який я можу вказати, є віджет Wrap, він робить макет, виходячи з розміру його дочірніх елементів у відповідному класі RenderWrap. Це трапляється під час макетування, а не build ().
Йона Вільямс,

@JonahWilliams Хм. Я не бачу, як Wrap може мені допомогти, оскільки це віджет, призначений для розміщення дітей навколо (працює щось на зразок сітки flexbox з Інтернету). У мене є один дочірній віджет, для якого мені потрібно знайти висоту. Будь ласка, дивіться редагування у питанні. Я майже вирішив проблему з CustomSingleChildLayout, але застряг на її обмеженні.
Гудін

Ви можете пояснити, що ви хочете конкретніше? Є декілька рішень. Але у кожного є різні варіанти використання.
Ремі Русселет,

Звичайно. Я розробляю пакет. Користувач / розробник надає віджети для мого класу. Тут ми говоримо про будь-який віджет, від new Text("hello")більш складного. Я розміщую ці віджети в ListView, і мені потрібна їх висота для обчислення деяких ефектів прокрутки. Я в порядку з отриманням висоти під час макетування, як і в тому, що робить SingleChildLayoutDelegate.
Гудін

Що ви маєте на увазі під "ефектами прокрутки"? У вас є приклад?
Ремі Русселет,

Відповіді:


159

Щоб отримати розмір / позицію віджета на екрані, ви можете скористатися GlobalKeyдля його отримання, BuildContextщоб потім знайти RenderBoxтой конкретний віджет, який буде містити його глобальну позицію та розмір відтворення.

Будьте обережні лише з одним: цього контексту може не існувати, якщо віджет не відображається. Що може спричинити проблему, ListViewоскільки віджети відображаються лише в тому випадку, якщо вони потенційно видимі.

Інша проблема полягає в тому, що ви не можете отримати віджет RenderBoxпід час buildдзвінка, оскільки віджет ще не відтворений.


Але мені потрібен розмір під час збірки! Що я можу зробити?

Є один класний віджет, який може допомогти: Overlayі його OverlayEntry. Вони використовуються для відображення віджетів поверх усього іншого (подібно до стека).

Але найкрутіше те, що вони йдуть іншим buildпотоком; вони будуються за звичайними віджетами.

Це має одне надзвичайно приємне значення: OverlayEntryможе мати розмір, який залежить від віджетів власне дерева віджетів.


Добре. Але чи не потрібно OverlayEntry перебудовувати вручну?

Так, вони це роблять. Але слід пам’ятати ще про одну річ: ScrollControllerпередана до Scrollable, є послухою, схожою на AnimationController.

Це означає, що ви можете поєднати а AnimatedBuilderз 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(
      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();

5
Гаразд, нарешті встиг перевірити це. Отже, мені насправді потрібна була лише перша частина. Я не знав, що можу використовувати доступ до context.height за допомогою GlobalKey. Чудова відповідь.
Гудін,

1
як імпортувати прив'язку планувальника?
siddhesh

1
я спробував імпортувати 'package: flutter / scheduler.dart.'; але я отримав цільову помилку uri не існує @ rémi-rousselet
siddhesh

@ rémi-rousselet, як мені зробити цю роботу, коли за списком ListView у мене є віджет, висотою якого я хочу керувати відповідно до висоти ListView?
RoyalGriffin

2
Ремі, Дякую за розумне рішення та чудове пояснення. Я маю питання. Що робити, якщо ми хочемо мати можливість знати Rectбудь-який ListItem того, ListView.builderколи вони натискаються. Чи було б надмірним встановити GlobalKey listItemKey = GlobalKey();для кожного елемента списку? Скажімо, у мене є +10000 предметів. Чи є розумний спосіб впоратися з цим без проблем із продуктивністю / пам'яттю?
aytunch

50

Це (я думаю) найпростіший спосіб зробити це.

Скопіюйте та вставте наступне у свій проект.

ОБНОВЛЕННЯ: використання RenderProxyBoxрезультатів у дещо правильнішій реалізації, оскільки воно вимагається при кожній перебудові дочірньої організації та її нащадків, що не завжди має місце для методу build () верхнього рівня.

ПРИМІТКА: Це не зовсім ефективний спосіб зробити це, на що вказує Хіксі тут . Але це найпростіше.

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, які люблять займати абсурдну кількість місця.


Те, що ви отримали тут, чудове. Жодного божевільного пояснення .. просто код, який працює. Дякую!
Денні Даглас,

1
Чудове рішення брат. зробити це як паб-пакет.
Harsh Bhikadia

Це дуже приємне рішення для багатьох питань висоти та ширини, Дякую
alireza daryani

Це працює, але в деяких місцях це важко використовувати. Так , наприклад, в PreferredSizeWidget, preferredSizeвикликається тільки один раз, так що ви не можете встановити висоту в легкому шляху.
user2233706

Гей, я оновив реалізацію для підтримки випадків, коли build () не викликається для відновлення. Сподіваюся, це правильніше.
Dev Aggarwal

6

Якщо я правильно розумію, ви хочете виміряти розмір деяких довільних віджетів, і ви можете обернути ці віджети іншим віджетом. У такому випадку метод у цій відповіді повинен працювати для вас.

В основному рішення полягає у прив’язуванні зворотного виклику у життєвому циклі віджета, який буде викликаний після надання першого кадру, звідки ви зможете отримати доступ context.size. Фішка полягає в тому, що ви повинні обернути віджет, який ви хочете виміряти, у віджет із увімкненням. І якщо вам абсолютно потрібен розмір, build()тоді ви можете отримати до нього доступ лише у другому візуалізації (він доступний лише після першого візуалізації).


1
Дякую за вказівник, перегляньте моє повне рішення - stackoverflow.com/a/60868972/7061265
Dev Aggarwal

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

не працює з динамічним розміром
Ренан Коельо

Не могли б ви детально розказати про "динамічний розмір", який спричиняє проблему? У вас є мінімальний репро, який я можу перевірити?
Оматт

щоб динамічно отримувати ширину / висоту віджета, вам потрібно зателефонувати, .findRenderObejct()а потім .size. RenderBox box = widget.context.findRenderObject(); print(box.size);
Ренан Коельо

Ви також можете передати GlobalKey як ключ віджета, а потім зателефонувати_myKey.currentContext.findRenderObject()
Ренан Коельо

1

Дозвольте мені надати вам віджет для цього


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

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

class _SizeProviderWidgetState extends State<SizeProviderWidget> {

  @override
  void initState() {
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      widget.onChildSize(context.size);
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return widget.child;
  }
}

Тут відбувається те, що в будь-якому віджеті змінна розміру буде доступна після побудови віджета, тож, передавши функцію до згаданого віджета, яка буде викликана відразу після побудови віджета з розміром вбудованого віджета, який ви можете використовувати.

SizeProviderWidget(
 child:MyWidget(),//the widget we want the size of,
 onChildSize:(size){
  //the size of the rendered MyWidget() is available here
 }
)

EDIT Просто обернути SizeProviderWidgetз , OrientationBuilderщоб зробити його поважати орієнтацію пристрою


Привіт! Спочатку це спрацювало добре, але я виявив одне застереження: розмір не оновлювався при зміні орієнтації пристрою. Я підозрюю, що це пов’язано з тим, що стан для віджета Stateful не реаніціалізується при обертанні пристрою.
mattorb

привіт, флаттер настільки модульний, як лего, просто оберни вищезгаданий віджет, OrientationBuilderякий починає поважати будь-яку орієнтацію, я маю на увазі пристрій ха-ха
Яду

0

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досить
Ренан Коельо

0

використовуйте пакет: 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)}');
    },
),

0

Найпростіший спосіб - використовувати MeasuredSize - це віджет, який обчислює розмір дочірньої дитини під час роботи.

Ви можете використовувати його так:

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

Ви можете знайти його тут: https://pub.dev/packages/measured_size


-2
<p>@override
<br/>
  Widget build(BuildContext context) {<br/>
   &nbsp;&nbsp; &nbsp;return Container(<br/>
     &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; width: MediaQuery.of(context).size.width,<br/>
     &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; height: MediaQuery.of(context).size.height,<br/>
    &nbsp;&nbsp;&nbsp; &nbsp;&nbsp; color: Colors.blueAccent,<br/>
   &nbsp;&nbsp; &nbsp;);<br/>
  }</p>
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.