208 votes

Comment obtenir la hauteur d'un Widget ?

Je ne comprends pas comment LayoutBuilder est utilisé pour obtenir la hauteur d'un Widget.

J'ai besoin d'afficher la liste des widgets et d'obtenir leur hauteur afin de pouvoir calculer certains effets de défilement spéciaux. Je développe un paquet et d'autres développeurs fournissent des widgets (je ne les contrôle pas). J'ai lu que LayoutBuilder peut être utilisé pour obtenir la hauteur.

Dans un cas très simple, j'ai essayé d'envelopper le widget dans LayoutBuilder.builder et de le placer dans la pile, mais j'obtiens toujours les résultats suivants minHeight 0.0 et maxHeight INFINITY . Est-ce que j'utilise mal le LayoutBuilder ?

EDIT : Il semble que LayoutBuilder soit un échec. J'ai trouvé le CustomSingleChildLayout ce qui est presque une solution.

J'ai étendu ce délégué, et j'ai pu obtenir la hauteur du widget en getPositionForChild(Size size, Size childSize) méthode. MAIS, la première méthode qui est appelée est Size getSize(BoxConstraints constraints) et comme contraintes, j'obtiens 0 à INFINITY parce que je pose ces CustomSingleChildLayouts dans une ListView.

Mon problème est que SingleChildLayoutDelegate getSize fonctionne comme si elle avait besoin de retourner la hauteur d'une vue. Je ne connais pas la hauteur d'un enfant à ce moment-là. Je ne peux que renvoyer constraints.smallest (qui est 0, la hauteur est 0), ou constraints.biggest qui est infini et fait planter l'application.

Dans les docs, il est même dit :

...mais la taille du parent ne peut pas dépendre de la taille de l'enfant.

Et c'est une limitation bizarre.

1 votes

LayoutBuilder vous donnera les contraintes de boîte du parent. Si vous voulez les tailles des enfants, vous devez utiliser une stratégie différente. Un exemple que je peux citer est le widget Wrap, qui effectue la mise en page en fonction de la taille de ses enfants dans la classe RenderWrap associée. Mais cela se produit pendant la mise en page, et non pendant la fonction build().

0 votes

@JonahWilliams Hmm. Je ne vois pas comment Wrap peut m'aider puisque c'est un widget conçu pour disposer les enfants autour (fonctionne quelque chose comme la grille flexbox du web). J'ai un widget enfant dont j'ai besoin de trouver la hauteur. Veuillez voir l'édition dans la question. J'ai presque résolu le problème avec CustomSingleChildLayout mais je suis resté bloqué sur ses limites.

0 votes

Pouvez-vous expliquer plus précisément ce que vous voulez ? Il existe de multiples solutions. Mais chacune a des cas d'utilisation différents.

304voto

Rémi Rousselet Points 45139

Pour obtenir la taille/position d'un widget à l'écran, vous pouvez utiliser GlobalKey pour obtenir son BuildContext pour ensuite trouver le RenderBox de ce widget spécifique, qui contiendra sa position globale et sa taille rendue.

Juste une chose à laquelle il faut faire attention : Ce contexte peut ne pas exister si le widget n'est pas rendu. Ce qui peut poser un problème avec ListView car les widgets ne sont rendus que s'ils sont potentiellement visibles.

Un autre problème est qu'il n'est pas possible d'obtenir le nom du widget. RenderBox pendant build car le widget n'a pas encore été rendu.


Mais j'ai besoin de la taille pendant la construction ! Que puis-je faire ?

Il y a un widget sympa qui peut vous aider : Overlay et son OverlayEntry . Ils sont utilisés pour afficher les widgets au dessus de tout le reste (similaire à la pile).

Mais le plus cool, c'est qu'ils sont sur un autre site. build flux ; ils sont construits après des widgets ordinaires.

Ça a une implication super cool : OverlayEntry peut avoir une taille qui dépend des widgets de l'arbre de widgets actuel.


D'accord. Mais les OverlayEntry ne doivent-elles pas être reconstruites manuellement ?

Oui, ils le font. Mais il y a une autre chose dont il faut être conscient : ScrollController transmis à un Scrollable est une écoute similaire à AnimationController .

Ce qui signifie que vous pouvez combiner un AnimatedBuilder avec un ScrollController cela aurait pour effet de reconstruire automatiquement votre widget lors d'un défilement. Parfait pour cette situation, non ?


En combinant le tout dans un exemple :

Dans l'exemple suivant, vous verrez une superposition qui suit un widget à l'intérieur de ListView et partage la même hauteur.

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

8 votes

Ok, j'ai finalement eu le temps de le tester. En fait, je n'avais besoin que de la première partie. Je ne savais pas que je pouvais utiliser l'accès au context.height avec GlobalKey . Excellente réponse.

1 votes

Comment importer le classeur schedular ?

1 votes

J'ai essayé d'importer 'package:flutter/scheduler.dart.' ; mais j'ai obtenu l'erreur target uri doesn't exists @rémi-rousselet

125voto

Dev Aggarwal Points 457

C'est (je pense) la façon la plus simple de procéder.

Copiez-collez ce qui suit dans votre projet.

MISE À JOUR : utilisation RenderProxyBox donne une implémentation légèrement plus correcte, car elle est appelée à chaque reconstruction de l'enfant et de ses descendants, ce qui n'est pas toujours le cas pour la méthode build() de niveau supérieur.

REMARQUE : Ce n'est pas exactement une façon efficace de procéder, comme l'a souligné Hixie. aquí . Mais c'est le plus facile.

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

Ensuite, il suffit d'entourer le widget dont vous souhaitez mesurer la taille avec MeasureSize .

var myChildSize = Size.zero;

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

Donc oui, la taille du parent ne peut pas peut dépendent de la taille de l'enfant si vous essayez suffisamment fort.


Anecdote personnelle - C'est pratique pour restreindre la taille des widgets tels que Align qui aime prendre une quantité absurde d'espace.

24voto

Omatt Points 1098

Voici un exemple de la manière dont vous pouvez utiliser LayoutBuilder pour déterminer la taille du widget.

Depuis LayoutBuilder est capable de déterminer les contraintes de son widget parent, l'un de ses cas d'utilisation est de permettre à ses widgets enfants de s'adapter aux dimensions de leur parent.

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

Démo

demo

Bûches

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

Mise à jour : Vous pouvez également utiliser MediaQuery pour obtenir une fonction similaire.

@override
Widget build(BuildContext context) {
  var screenSize = MediaQuery.of(context).size ;
  if (screenSize.width > layoutSize){
    return Widget(); 
  } else {
    return Widget(); /// Widget if doesn't match the size
  }
}

18voto

Yadu Points 2085

Laissez-moi vous donner un widget pour cela

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

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

class _SizeProviderWidgetState extends State<SizeProviderWidget> {
  @override
  void initState() {
    ///add size listener for first build
    _onResize();
    super.initState();
  }

  void _onResize() {
    WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
      if (context.size is Size) {
        widget.onChildSize(context.size!);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    ///add size listener for every build uncomment the fallowing
    ///_onResize();
    return widget.child;
  }
}

EDIT Il suffit d'envelopper le SizeProviderWidget con OrientationBuilder pour qu'il respecte l'orientation de l'appareil

9voto

Brian Manuputty Points 272

J'ai créé ce widget comme une simple solution sans état :

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

Utilisez-le comme ceci

ChildSizeNotifier(
  builder: (context, size, child) {
    // size is the size of the text
    return Text(size.height > 50 ? 'big' : 'small');
  },
)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X