Jak uzyskać wysokość widżetu?

90

Nie rozumiem, jak LayoutBuilderjest używany do uzyskania wysokości widżetu.

Muszę wyświetlić listę widżetów i uzyskać ich wysokość, aby móc obliczyć specjalne efekty przewijania. Rozwijam pakiet, a inni programiści zapewniają widżet (nie kontroluję ich). Czytałem, że LayoutBuilder może służyć do uzyskania wysokości.

W bardzo prostym przypadku próbowałem zawinąć Widget w LayoutBuilder.builder i umieścić go w stosie, ale zawsze otrzymuję minHeight 0.0i maxHeight INFINITY. Czy nadużywam LayoutBuilder?

EDYCJA : Wygląda na to, że LayoutBuilder nie ma wyjścia. Znalazłem CustomSingleChildLayout, który jest prawie rozwiązaniem.

Rozszerzyłem tego delegata i udało mi się uzyskać wysokość widżetu w getPositionForChild(Size size, Size childSize)metodzie. ALE, pierwsza wywoływana metoda to Size getSize(BoxConstraints constraints)i jako ograniczenie, otrzymuję 0 do INFINITY, ponieważ układam te CustomSingleChildLayouts w ListView.

Mój problem polega na tym, że SingleChildLayoutDelegate getSizedziała tak, jakby zwracał wysokość widoku. W tej chwili nie znam wzrostu dziecka. Mogę zwrócić tylko constraints.smallest (czyli 0, wysokość to 0) lub constraints.biggest, co oznacza nieskończoność i powoduje awarię aplikacji.

W dokumentach jest nawet napisane:

... ale wielkość rodzica nie może zależeć od wzrostu dziecka.

I to jest dziwne ograniczenie.

Gudin
źródło
1
LayoutBuilder poda ograniczenia pola elementu nadrzędnego. Jeśli chcesz mieć rozmiary dziecka, potrzebujesz innej strategii. Jednym z przykładów, na który mogę wskazać, jest widget Wrap, który tworzy układ oparty na rozmiarze swoich dzieci w skojarzonej klasie RenderWrap. Dzieje się tak jednak podczas tworzenia układu, a nie build ().
Jonah Williams
@JonahWilliams Hmm. Nie widzę, jak Wrap może mi pomóc, ponieważ jest to widget zaprojektowany do układania elementów podrzędnych (działa podobnie jak siatka flexbox z sieci). Mam jeden widżet podrzędny, którego potrzebuję, aby znaleźć wysokość. Zobacz zmianę w pytaniu. Prawie rozwiązałem problem z CustomSingleChildLayout, ale utknąłem na jego ograniczeniach.
Gudin
Czy możesz dokładniej wyjaśnić, czego chcesz? Istnieje wiele rozwiązań. Ale każdy ma inne przypadki użycia.
Rémi Rousselet
Pewnie. Opracowuję pakiet. Użytkownik / deweloper zapewnia widżety mojej klasie. Mówimy tutaj o każdym widżecie, od new Text("hello")bardziej złożonych. Umieszczam te widżety w ListView i potrzebuję ich wysokości, aby obliczyć niektóre efekty przewijania. Mogę uzyskać wysokość w czasie układu, tak jak robi to SingleChildLayoutDelegate.
Gudin
Co rozumiesz przez „efekty przewijania”? Masz przykład?
Rémi Rousselet

Odpowiedzi:

159

Aby uzyskać rozmiar / pozycję widżetu na ekranie, możesz użyć GlobalKeygo, BuildContextaby następnie znaleźć RenderBoxten konkretny widżet, który będzie zawierał jego globalną pozycję i renderowany rozmiar.

Tylko jedna rzecz, na którą należy uważać: ten kontekst może nie istnieć, jeśli widżet nie jest renderowany. Co może powodować problem z ListViewwidżetami as jest renderowany tylko wtedy, gdy są one potencjalnie widoczne.

Innym problemem jest to, że nie możesz pobrać widżetu RenderBoxpodczas buildpołączenia, ponieważ widżet nie został jeszcze wyrenderowany.


Ale potrzebuję rozmiaru podczas budowy! Co mogę zrobić?

Jest jeden fajny widget, który może pomóc: Overlayi jego OverlayEntry. Służą do wyświetlania widżetów na wszystkim innym (podobnie do stosu).

Ale najfajniejsze jest to, że są na innym buildnurcie; są tworzone na podstawie zwykłych widgetów.

Ma to jedną super fajną konsekwencję: OverlayEntrymoże mieć rozmiar zależny od widżetów w aktualnym drzewie widżetów.


W porządku. Ale czy OverlayEntry nie wymaga ręcznej przebudowy?

Tak, robią. Ale jest jeszcze jedna rzecz, o której należy pamiętać: ScrollControllerprzekazana do a Scrollable, jest podobna do słuchania AnimationController.

Oznacza to, że możesz połączyć widżet AnimatedBuilderz a ScrollController, miałoby cudowny efekt, gdybyś automatycznie przebudował swój widżet na zwoju. Idealny w tej sytuacji, prawda?


Łącząc wszystko w przykład:

W poniższym przykładzie zobaczysz nakładkę, która podąża za widżetem wewnątrz ListViewi ma tę samą wysokość.

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

Uwaga :

Podczas przechodzenia do innego ekranu wywołanie śledzone w przeciwnym razie pozostanie widoczne.

sticky.remove();
Rémi Rousselet
źródło
5
OK, w końcu mam czas, aby to przetestować. Tak więc właściwie potrzebowałem tylko pierwszej części. Nie wiedziałem, że mogę użyć access the context.height with GlobalKey. Świetna odpowiedź.
Gudin
1
jak zaimportować segregator harmonogramowy?
siddhesh
1
próbowałem zaimportować „pakiet: flutter / scheduleer.dart.”; ale mam błąd docelowy adres Uri nie istnieje @ rémi-rousselet
siddhesh
@ rémi-rousselet Jak to zrobić, gdy mam widżet za ListView, którego wysokość chcę kontrolować zgodnie z wysokością ListView?
RoyalGriffin
2
Remi, dzięki za sprytne rozwiązanie i świetne wyjaśnienie. Mam pytanie. A co jeśli chcemy być w stanie poznać Rectdowolne ListItem a ListView.builderkiedy są wciśnięte. Czy ustawienie GlobalKey listItemKey = GlobalKey();dla każdego elementu listItem byłoby przesadą? Powiedzmy, że mam +10000 przedmiotów. Czy istnieje sprytny sposób radzenia sobie z tym bez problemów z wydajnością / pamięcią?
aytunch
50

Jest to (myślę) najprostszy sposób, aby to zrobić.

Skopiuj i wklej następujące elementy do swojego projektu.

AKTUALIZACJA: użycie RenderProxyBoxskutkuje nieco poprawniejszą implementacją, ponieważ jest wywoływana przy każdej przebudowie dziecka i jego potomków, co nie zawsze ma miejsce w przypadku metody build () najwyższego poziomu.

UWAGA: To nie jest dokładnie to skuteczny sposób na to, jak wskazał przez Hixie tutaj . Ale to najłatwiejsze.

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

Następnie po prostu zawiń widżet, którego rozmiar chcesz mierzyć MeasureSize.


var myChildSize = Size.zero;

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

Więc tak, wielkość rodzica nie może zależeć od wielkości dziecka, jeśli będziesz wystarczająco się starać.


Osobista anegdota - przydaje się przy ograniczaniu rozmiaru widżetów, takich jak ten Align, który lubi zajmować absurdalną ilość miejsca.

Dev Aggarwal
źródło
To, co tu masz, jest świetne. Żadnego szalonego wyjaśnienia ... tylko kod, który działa. Dzięki!
Danny Daglas
1
Niesamowite rozwiązanie, brat. zrób to jako pakiet wydawniczy.
Harsh Bhikadia
To bardzo fajne rozwiązanie dla wielu problemów związanych z wysokością i szerokością. Dzięki
alireza daryani
To działa, ale w niektórych miejscach jest trudne w użyciu. Na przykład w a PreferredSizeWidget, preferredSizejest wywoływana tylko raz, więc nie można łatwo ustawić wysokości.
user2233706
Hej, zaktualizowałem implementację, aby obsługiwała przypadki, w których build () nie jest wywoływany przy przebudowie. Mam nadzieję, że to jest bardziej poprawne.
Dev Aggarwal
6

Jeśli dobrze rozumiem, chcesz zmierzyć rozmiar niektórych dowolnych widżetów i możesz owinąć te widżety innym widżetem. W takim przypadku metoda podana w tej odpowiedzi powinna działać dla Ciebie.

Zasadniczo rozwiązaniem jest powiązanie wywołania zwrotnego w cyklu życia widgetu, które zostanie wywołane po wyrenderowaniu pierwszej klatki, z którego można uzyskać dostęp context.size. Problem polega na tym, że widżet, który chcesz zmierzyć, musisz opakować w widżet stanowy. A jeśli absolutnie potrzebujesz rozmiaru w ramach build(), możesz uzyskać do niego dostęp tylko w drugim renderowaniu (jest dostępny tylko po pierwszym renderowaniu).

Gan Quan
źródło
1
Dzięki za wskazówkę, sprawdź moje kompletne rozwiązanie - stackoverflow.com/a/60868972/7061265
Dev Aggarwal
3

Oto przykład, jak LayoutBuilderokreślić rozmiar widżetu.

Ponieważ LayoutBuilderwidget jest w stanie określić ograniczenia swojego widgetu nadrzędnego, jednym z jego przypadków użycia jest możliwość dostosowania widgetów podrzędnych do wymiarów ich rodzica.

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

Próbny

próbny

Dzienniki

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
Omatt
źródło
nie działa z rozmiarem dynamicznym
Renan Coelho
Czy możesz dokładniej określić „rozmiar dynamiczny”, który powoduje problem? Czy masz minimalną kopię, którą mogę sprawdzić?
Omatt
aby dynamicznie uzyskać szerokość / wysokość widżetu, musisz zadzwonić, .findRenderObejct()a następnie .size. RenderBox box = widget.context.findRenderObject(); print(box.size);
Renan Coelho
możesz również przekazać GlobalKey jako klucz do widżetu, a następnie zadzwonić_myKey.currentContext.findRenderObject()
Renan Coelho
1

Pozwól, że dam ci do tego widget


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

Tutaj dzieje się tak, że w każdym Widgecie zmienna rozmiaru będzie dostępna po zbudowaniu widgetu, więc przekazując funkcję do powyższego widgetu, która zostanie wywołana zaraz po zbudowaniu widgetu z rozmiarem zbudowanego widgetu, którego możesz użyć.

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

EDIT Wystarczy owinąć SizeProviderWidgetsię OrientationBuilder, aby respektować orientację urządzenia

Yadu
źródło
Cześć! Początkowo wydawało się, że działa to dobrze, ale odkryłem jedno zastrzeżenie: rozmiar nie aktualizował się przy zmianie orientacji urządzenia. Podejrzewam, że dzieje się tak, ponieważ stan Stateful Widget nie jest ponownie inicjowany przy obracaniu urządzenia.
mattorb
cześć, flutter jest tak modularny jak lego, po prostu zawiń powyższy Widget z, OrientationBuilderktóry zaczyna szanować każdą orientację, mam na myśli urządzenie haha
Yadu
0

findRenderObject()zwraca wartość, RenderBoxktóra jest używana do podania rozmiaru rysowanego widgetu i powinna być wywoływana po zbudowaniu drzewa widgetów, więc musi być używana z jakimś mechanizmem addPostFrameCallback()wywołania zwrotnego lub wywołaniami zwrotnymi.

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

Wynik:

wprowadź opis obrazu tutaj

jitsm555
źródło
_textKey.currentContext.sizewystarczy
Renan Coelho
0

użyj pakietu: z_tools . Kroki:

1. zmień plik główny

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. używać w funkcji

_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)}');
    },
),
qqzhao
źródło
0

Najłatwiej jest użyć MeasuredSize , czyli widgetu, który oblicza rozmiar swojego elementu podrzędnego w czasie wykonywania.

Możesz go używać w ten sposób:

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

Możesz go znaleźć tutaj: https://pub.dev/packages/measured_size

Ayham Orfali
źródło
-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>
Zaw Htet Aung
źródło