Wie erhalte ich die Höhe eines Widgets?

81

Ich verstehe nicht, wie LayoutBuilderdie Höhe eines Widgets ermittelt wird.

Ich muss die Liste der Widgets anzeigen und deren Höhe ermitteln, damit ich einige spezielle Bildlaufeffekte berechnen kann. Ich entwickle ein Paket und andere Entwickler stellen Widgets zur Verfügung (ich kontrolliere sie nicht). Ich habe gelesen, dass LayoutBuilder verwendet werden kann, um die Höhe zu ermitteln.

In einem sehr einfachen Fall habe ich versucht, Widget in LayoutBuilder.builder zu verpacken und in den Stapel zu legen, aber ich bekomme immer minHeight 0.0und maxHeight INFINITY. Missbrauche ich den LayoutBuilder?

EDIT : Es scheint, dass LayoutBuilder ein No Go ist. Ich habe das CustomSingleChildLayout gefunden, das fast eine Lösung ist.

Ich habe diesen Delegaten erweitert und konnte die Höhe des Widgets in der getPositionForChild(Size size, Size childSize)Methode ermitteln. ABER die erste Methode, die aufgerufen wird, ist Size getSize(BoxConstraints constraints)und als Einschränkung erhalte ich 0 bis INFINITY, weil ich diese CustomSingleChildLayouts in eine ListView lege.

Mein Problem ist, dass SingleChildLayoutDelegate so getSizefunktioniert, als ob es die Höhe einer Ansicht zurückgeben müsste . Ich kenne die Größe eines Kindes in diesem Moment nicht. Ich kann nur Constraints.klein (das ist 0, die Höhe ist 0) oder Constraints.biggest, das unendlich ist und die App zum Absturz bringt, zurückgeben.

In den Dokumenten steht sogar:

... aber die Größe des Elternteils kann nicht von der Größe des Kindes abhängen.

Und das ist eine seltsame Einschränkung.

Gudin
quelle
1
LayoutBuilder gibt Ihnen die Box-Einschränkungen des übergeordneten Elements. Wenn Sie die Größe des Kindes wollen, brauchen Sie eine andere Strategie. Ein Beispiel, auf das ich verweisen kann, ist das Wrap-Widget. Es erstellt das Layout basierend auf der Größe der untergeordneten Elemente in der zugeordneten RenderWrap-Klasse. Dies geschieht jedoch während des Layouts, nicht während build ().
Jonah Williams
@ Jonah Williams Hmm. Ich sehe nicht, wie Wrap mir helfen kann, da es ein Widget ist, mit dem Kinder herum angeordnet werden können (funktioniert so etwas wie ein Flexbox-Raster aus dem Web). Ich habe ein untergeordnetes Widget, dessen Höhe ich ermitteln muss. Bitte beachten Sie die Bearbeitung in der Frage. Ich habe das Problem mit CustomSingleChildLayout fast gelöst, bin aber an seiner Einschränkung hängen geblieben.
Gudin
Können Sie genauer erklären, was Sie wollen? Es gibt mehrere Lösungen. Aber jeder hat unterschiedliche Anwendungsfälle.
Rémi Rousselet
Sicher. Ich entwickle ein Paket. Benutzer / Entwickler stellt meiner Klasse Widgets zur Verfügung. Wir sprechen hier von jedem Widget, von new Text("hello")komplexeren. Ich lege diese Widgets in ListView und benötige ihre Höhe, um einige Bildlaufeffekte zu berechnen. Ich bin damit einverstanden, die Höhe zum Layoutzeitpunkt zu ermitteln, genau wie SingleChildLayoutDelegate.
Gudin
Was meinst du mit "Scroll-Effekten"? Hast du ein beispiel
Rémi Rousselet

Antworten:

146

Um die Größe / Position eines Widgets auf dem Bildschirm anzuzeigen, können Sie es verwenden GlobalKey, um es BuildContextzu ermitteln und dann das zu findenRenderBox dieses spezielle Widget, das seine globale Position und gerenderte Größe enthält.

Nur eines ist zu beachten: Dieser Kontext existiert möglicherweise nicht, wenn das Widget nicht gerendert wird. Dies kann zu Problemen führen, ListViewda Widgets nur gerendert werden, wenn sie möglicherweise sichtbar sind.

Ein weiteres Problem ist, dass Sie RenderBoxwährend eines buildAnrufs keine Widgets erhalten können , da das Widget noch nicht gerendert wurde.


Aber ich muss die Größe während des Builds! Was kann ich tun?

Es gibt ein cooles Widget, das helfen kann: Overlayund sein OverlayEntry. Sie werden verwendet, um Widgets über allem anderen anzuzeigen (ähnlich wie beim Stapel).

Aber das Coolste ist, dass sie sich in einem anderen buildFluss befinden. Sie werden nach regulären Widgets erstellt.

Das hat eine super coole Implikation: OverlayEntryKann eine Größe haben, die von den Widgets des tatsächlichen Widget-Baums abhängt.


In Ordnung. Aber muss OverlayEntry nicht manuell neu erstellt werden?

Ja, das tun sie. Aber es gibt noch eine andere Sache, die Sie beachten sollten: ScrollControllerÜbergeben an a Scrollable, ist eine hörbare ähnlich wie AnimationController.

Das heißt, Sie könnten ein AnimatedBuildermit einem kombinieren ScrollController, es hätte den schönen Effekt, Ihr Widget automatisch auf einer Schriftrolle neu zu erstellen. Perfekt für diese Situation, oder?


Alles zu einem Beispiel zusammenfassen:

Im folgenden Beispiel sehen Sie eine Überlagerung, die einem Widget im Inneren folgt ListViewund dieselbe Höhe aufweist.

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

Hinweis :

Wenn Sie zu einem anderen Bildschirm navigieren, bleibt der folgende Anruf sichtbar, da er sonst sichtbar bleibt.

sticky.remove();
Rémi Rousselet
quelle
5
Ok, endlich Zeit zum Testen. Also brauchte ich eigentlich nur den ersten Teil. Ich wusste nicht, dass ich Zugriff auf den Kontext verwenden kann GlobalKey. Gute Antwort.
Gudin
1
Wie importiere ich einen Zeitplanordner?
Siddhesh
1
Ich habe versucht, 'package: flutter / scheduler.dart.' zu importieren. aber ich habe Fehler Ziel uri existiert nicht @ rémi-rousselet
siddhesh
@ rémi-rousselet Wie mache ich das, wenn ich ein Widget hinter der ListView habe, dessen Höhe ich entsprechend der Höhe der ListView steuern möchte?
RoyalGriffin
2
Remi, Danke für die clevere Lösung und die tolle Erklärung. Ich habe eine Frage. Was ist, wenn wir in der Lage sein möchten, das RectListItem eines zu kennen, ListView.builderwenn es gedrückt wird ? Wäre es ein Overkill, GlobalKey listItemKey = GlobalKey();für jedes listItem zu setzen? Angenommen, ich habe +10000 Artikel. Gibt es eine clevere Möglichkeit, dies ohne Leistungs- / Speicherprobleme zu verwalten?
aytunch
37

Dies ist (glaube ich) der einfachste Weg, dies zu tun.

Kopieren Sie Folgendes in Ihr Projekt.

UPDATE: Die Verwendung RenderProxyBoxführt zu einer etwas korrekteren Implementierung, da sie bei jeder Neuerstellung des untergeordneten Elements und seiner Nachkommen aufgerufen wird, was bei der build () -Methode der obersten Ebene nicht immer der Fall ist.

HINWEIS: Dies ist nicht gerade ein effizienter Weg, wie Hixie hier ausgeführt hat . Aber es ist am einfachsten.

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

Wickeln Sie dann einfach das Widget ein, mit dessen Größe Sie messen möchten MeasureSize.


var myChildSize = Size.zero;

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

Also ja, die Größe des Elternteils kann nicht kann auf die Größe des Kindes abhängen , wenn man hart genug versuchen.


Persönliche Anekdote - Dies ist praktisch, um die Größe von Widgets wie zu beschränken Align, die gerne absurd viel Platz beanspruchen.

Dev Aggarwal
quelle
Was du hier hast, ist großartig. Keine verrückte Erklärung ... nur Code, der funktioniert. Vielen Dank!
Danny Daglas
Super Lösung Bruder. Machen Sie dies als Pub-Paket.
Harsh Bhikadia
Es ist eine sehr schöne Lösung für viele Probleme mit Höhe und Breite. Danke
alireza daryani
Dies funktioniert, ist aber an einigen Stellen schwierig zu verwenden. Zum Beispiel in einem PreferredSizeWidget, preferredSizewird nur einmal aufgerufen, so dass Sie nicht die Höhe in einer einfachen Art und Weise einstellen.
user2233706
Tolle Idee, aber das funktioniert nicht auf TextFormField
Iworb
6

Wenn ich das richtig verstehe, möchten Sie die Dimension einiger beliebiger Widgets messen und diese Widgets mit einem anderen Widget umschließen. In diesem Fall sollte die Methode in dieser Antwort für Sie funktionieren.

Grundsätzlich besteht die Lösung darin, einen Rückruf im Widget-Lebenszyklus zu binden, der nach dem Rendern des ersten Frames aufgerufen wird. Von dort aus können Sie darauf zugreifen context.size. Der Haken ist, dass Sie das Widget, das Sie messen möchten, in ein zustandsbehaftetes Widget einschließen müssen. Und wenn Sie die Größe innerhalb unbedingt benötigen build(), können Sie nur im zweiten Render darauf zugreifen (sie ist erst nach dem ersten Render verfügbar).

Gan Quan
quelle
1
Vielen Dank für den Hinweis
Dev Aggarwal
2

Hier ist ein Beispiel, wie Sie LayoutBuilderdie Größe des Widgets bestimmen können.

Da das LayoutBuilderWidget in der Lage ist, die Einschränkungen des übergeordneten Widgets zu bestimmen, besteht einer seiner Anwendungsfälle darin, dass die untergeordneten Widgets an die Dimensionen des übergeordneten Widgets angepasst werden können.

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

Demo

Demo

Protokolle

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
quelle
0

findRenderObject()Gibt RenderBoxdas zurück, mit dem die Größe des gezeichneten Widgets angegeben wird. Es sollte aufgerufen werden, nachdem der Widget-Baum erstellt wurde. Daher muss es mit einem Rückrufmechanismus oder addPostFrameCallback()Rückrufen verwendet werden.

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

Ausgabe:

Geben Sie hier die Bildbeschreibung ein

jitsm555
quelle
0

Verwenden Sie das Paket: z_tools . Die Schritte:

1. Hauptdatei ändern

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. Verwendung in Funktion

_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
quelle