¿Cómo obtener la altura de un widget?

82

No entiendo cómo LayoutBuilderse usa para obtener la altura de un widget.

Necesito mostrar la lista de widgets y obtener su altura para poder calcular algunos efectos especiales de desplazamiento. Estoy desarrollando un paquete y otros desarrolladores proporcionan un widget (no los controlo). Leí que LayoutBuilder se puede usar para obtener altura.

En un caso muy simple, traté de envolver Widget en LayoutBuilder.builder y ponerlo en Stack, pero siempre obtengo minHeight 0.0, y maxHeight INFINITY. ¿Estoy haciendo un mal uso de LayoutBuilder?

EDITAR : Parece que LayoutBuilder no es posible. Encontré CustomSingleChildLayout, que es casi una solución.

Extendí ese delegado y pude obtener la altura del widget en el getPositionForChild(Size size, Size childSize)método. PERO, el primer método que se llama es Size getSize(BoxConstraints constraints)y, como restricciones, obtengo 0 a INFINITY porque estoy colocando estos CustomSingleChildLayouts en un ListView.

Mi problema es que SingleChildLayoutDelegate getSizefunciona como si necesitara devolver la altura de una vista. No sé la altura de un niño en ese momento. Solo puedo devolver restricints.smallest (que es 0, la altura es 0), o constraints.biggest que es infinito y bloquea la aplicación.

En los documentos incluso dice:

... pero el tamaño del padre no puede depender del tamaño del niño.

Y esa es una limitación extraña.

Gudin
fuente
1
LayoutBuilder le dará las restricciones de caja del padre. Si quieres los tamaños del niño necesitas una estrategia diferente. Un ejemplo al que puedo señalar es el widget Wrap, hace un diseño basado en el tamaño de sus hijos en la clase RenderWrap asociada. Sin embargo, esto sucede durante el diseño, no durante la construcción ().
Jonah Williams
@JonahWilliams Hmm. No veo cómo Wrap puede ayudarme, ya que es un widget diseñado para distribuir a los niños (funciona como una cuadrícula de flexbox de la web). Tengo un widget secundario del que necesito encontrar la altura. Por favor, vea la edición en la pregunta. Casi resolví el problema con CustomSingleChildLayout pero me quedé atascado en su limitación.
Gudin
¿Puede explicar lo que quiere más específicamente? Existen múltiples soluciones. Pero cada uno tiene diferentes casos de uso.
Rémi Rousselet
Por supuesto. Estoy desarrollando un paquete. El usuario / desarrollador proporciona widgets a mi clase. Estamos hablando de cualquier widget aquí, desde los new Text("hello")más complejos. Coloco estos widgets en ListView y necesito su altura para calcular algunos efectos de desplazamiento. Estoy de acuerdo con obtener la altura en el momento del diseño, al igual que lo que hace SingleChildLayoutDelegate.
Gudin
¿Qué quieres decir con "efectos de desplazamiento"? Tienes un ejemplo ?
Rémi Rousselet

Respuestas:

150

Para obtener el tamaño / posición de un widget en la pantalla, puede usar GlobalKeypara obtenerlo BuildContexty luego encontrar el RenderBoxde ese widget específico, que contendrá su posición global y tamaño renderizado.

Solo una cosa a tener en cuenta: ese contexto puede no existir si el widget no se representa. Lo que puede causar un problema ListViewya que los widgets se procesan solo si son potencialmente visibles.

Otro problema es que no puede obtener un widget RenderBoxdurante la buildllamada, ya que aún no se ha renderizado.


¡Pero necesito el tamaño durante la construcción! ¿Que puedo hacer?

Hay un widget genial que puede ayudar: Overlayy es OverlayEntry. Se utilizan para mostrar widgets encima de todo lo demás (similar a la pila).

Pero lo mejor es que están en un buildflujo diferente ; se construyen después de los widgets normales.

Eso tiene una implicación genial: OverlayEntrypuede tener un tamaño que depende de los widgets del árbol de widgets real.


Bueno. Pero, ¿no es necesario reconstruir OverlayEntry manualmente?

Ellos si. Pero hay otra cosa a tener en cuenta:, ScrollControllerpasado a Scrollable, es un similar escuchable a AnimationController.

Lo que significa que podría combinar un AnimatedBuildercon un ScrollController, tendría el efecto encantador de reconstruir su widget automáticamente en un pergamino. Perfecto para esta situación, ¿verdad?


Combinando todo en un ejemplo:

En el siguiente ejemplo, verá una superposición que sigue a un widget en el interior ListViewy comparte la misma altura.

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

Nota :

Cuando navegue a una pantalla diferente, llame a seguir, de lo contrario, permanecerá visible.

sticky.remove();
Rémi Rousselet
fuente
5
Ok, finalmente tuve tiempo de probarlo. Entonces, en realidad solo necesitaba la primera parte. No sabía que podía usar access the context.height con GlobalKey. Gran respuesta.
Gudin
1
¿Cómo importar la carpeta del planificador?
siddhesh
1
he intentado importar 'paquete: flutter / planificador.dart.'; pero tengo un error de destino uri no existe @ rémi-rousselet
siddhesh
@ rémi-rousselet ¿cómo hago que esto funcione cuando tengo un widget detrás de ListView cuya altura quiero controlar de acuerdo con la altura de ListView?
RoyalGriffin
2
Remi, gracias por la inteligente solución y la gran explicación. Tengo una pregunta. ¿Qué pasa si queremos poder saber el Rectde cualquier ListItem de un ListView.buildercuando se presionan? ¿Sería una exageración establecerlo GlobalKey listItemKey = GlobalKey();para cada listItem? Digamos que tengo más de 10000 artículos. ¿Existe una forma inteligente de gestionar esto sin problemas de rendimiento / memoria?
aytunch
38

Esta es (creo) la forma más sencilla de hacer esto.

Copie y pegue lo siguiente en su proyecto.

ACTUALIZACIÓN: el uso RenderProxyBoxda como resultado una implementación un poco más correcta, porque se llama en cada reconstrucción del hijo y sus descendientes, lo que no siempre es el caso del método build () de nivel superior.

NOTA: Esta no es exactamente una forma eficiente de hacer esto, como lo señala Hixie aquí . Pero es el más sencillo.

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

Luego, simplemente envuelva el widget cuyo tamaño le gustaría medir MeasureSize.


var myChildSize = Size.zero;

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

Entonces, sí, el tamaño del padre no puede depender del tamaño del niño si se esfuerza lo suficiente.


Anécdota personal: esto es útil para restringir el tamaño de los widgets como Align, que les gusta ocupar una cantidad absurda de espacio.

Dev Aggarwal
fuente
Lo que tienes aquí es genial. Sin explicación loca ... solo código que funciona. ¡Gracias!
Danny Daglas
Hermano de solución impresionante. haz esto como un paquete de pub.
Harsh Bhikadia
Es una solución muy agradable para muchos problemas de altura y ancho, gracias
alireza daryani
Esto funciona, pero es difícil de usar en algunos lugares. Por ejemplo, en a PreferredSizeWidget, preferredSizesolo se llama una vez, por lo que no puede establecer la altura de forma sencilla.
user2233706
Gran idea, pero esto no funcionará en TextFormField
Iworb
6

Si lo entiendo correctamente, desea medir la dimensión de algunos widgets arbitrarios y puede envolver esos widgets con otro widget. En ese caso, el método en esta respuesta debería funcionar para usted.

Básicamente, la solución es vincular una devolución de llamada en el ciclo de vida del widget, que se llamará después de que se procese el primer fotograma, desde allí puede acceder context.size. El problema es que debe ajustar el widget que desea medir dentro de un widget con estado. Y, si es absolutamente necesario el tamaño dentro build(), solo puede acceder a él en el segundo render (solo está disponible después del primer render).

Gan Quan
fuente
1
Gracias por el puntero, consulte mi solución completa - stackoverflow.com/a/60868972/7061265
Dev Aggarwal
2

Aquí hay una muestra de cómo se puede utilizar LayoutBuilderpara determinar el tamaño del widget.

Dado que el LayoutBuilderwidget puede determinar las restricciones de su widget principal, uno de sus casos de uso es poder hacer que sus widgets secundarios se adapten a las dimensiones de sus principales.

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

Manifestación

manifestación

Registros

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

findRenderObject()devuelve el RenderBoxque se usa para dar el tamaño del widget dibujado y debe ser llamado después de que se construya el árbol de widgets, por lo que debe usarse con algún mecanismo de addPostFrameCallback()devolución de llamada o devoluciones de llamada.

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

Salida:

ingrese la descripción de la imagen aquí

jitsm555
fuente
0

use el paquete: z_tools . Los pasos:

1. cambiar el archivo principal

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. uso en función

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