Notes flottantes | Composants de mise en page flottante

Les composants de la classe Layout contiendront un ou plusieurs sous-composants. Les composants de la classe Layout sont tous directement ou indirectement hérités SingleChildRenderObjectWidgetet MultiChildRenderObjectWidgetWidgets. Ils ont généralement une propriété childou childrenpour recevoir des sous-Widgets.

Différents composants de mise en page organisent les sous-composants (de mise en page) de différentes manières, comme indiqué dans le tableau suivant :

Widget illustrer utiliser
LeafRenderObjectWidget Classe de base pour les composants non-conteneurs Le nœud feuille de l'arborescence Widget est utilisé pour les widgets sans nœuds enfants, et généralement les composants de base appartiennent à cette catégorie, comme Image .
SingleChildRenderObjectWidget Classe de base de composant enfant unique Contient un Widget enfant, tel que : ConstrainedBox , DecoratedBox , etc.
MultiChildRenderObjectWidget Classe de base de composants multi-enfants Contient plusieurs sous-Widgets et a généralement un paramètre children qui accepte un tableau de Widgets. Tels que ligne, colonne, pile, etc.

Regardons la relation d'héritage Widget > RenderObjectWidget > (Leaf/SingleChild/MultiChild) RenderObjectWidget.

RenderObjectWidgetLes méthodes de création et de mise à jour sont définies dans la classe RenderObject, et les sous-classes doivent les implémenter. RenderObjectNous avons seulement besoin de savoir qu'il s'agit de l'objet de la mise en page finale et de l'interface utilisateur de rendu. C'est-à-dire que pour les composants de mise en page, les algorithmes de mise en page sont tous Il est RenderObjectréalisé via l' objet correspondant , donc si vous êtes intéressé par le principe d'un composant de classe de mise en page, vous pouvez vérifier son RenderObjectimplémentation correspondante, par exemple Stack(mise en page en cascade) l'objet correspondant RenderObjectest RenderStack, et l'implémentation de la mise en page en cascade est dans RenderStack.

Disposition des classes de contraintes dimensionnelles

Le conteneur de limite de taille est utilisé pour limiter la taille du conteneur. Flutter fournit une variété de conteneurs de ce type, tels que ConstrainedBox, SizedBox, UnconstrainedBox, AspectRatioetc.

Modèle de disposition de boîte

Il existe deux modèles de mise en page dans Flutter :

  1. Disposition basée sur RenderBoxle modèle de boîte.
  2. Charger les mises en page de liste à la demande en fonction de Sliver( ).RenderSliver

Les détails des deux méthodes de mise en page sont légèrement différents, mais le processus général est le même. Le processus de mise en page est le suivant :

  1. Les composants de niveau supérieur transmettent des conditions de contraintes (contraintes) aux composants de niveau inférieur.
  2. Le composant inférieur détermine sa propre taille et indique ensuite le composant supérieur. Notez que la taille du composant sous-jacent doit être conforme aux contraintes du composant parent.
  3. Le composant de niveau supérieur détermine le décalage du composant de niveau inférieur par rapport à lui-même et détermine sa propre taille (dans la plupart des cas, il détermine sa propre taille en fonction de la taille du composant enfant).

Par exemple, la contrainte transmise du composant parent au composant enfant est " 最大宽高不能超过100,最小宽高为0", si nous définissons la largeur et la hauteur du composant enfant sur 200, la taille finale du composant enfant est 100*100, car à tout moment le composant enfant doit d'abord respecter les contraintes du composant parent, sur cette base Puis appliquer les contraintes du sous-composant (équivalent à trouver une intersection entre les contraintes du composant parent et sa propre taille).

Le composant de mise en page du modèle de boîte a deux caractéristiques :

  1. Les objets de rendu correspondant aux composants héritent tous de la RenderBoxclasse.
  2. Les informations de contrainte transmises du parent à l'enfant lors de la disposition sont BoxConstraintsdécrites par .

BoxConstraints

BoxConstraintsIl s'agit des informations de contrainte transmises du composant parent au composant enfant lors de la disposition du modèle de boîte. Il est utilisé pour décrire la plage d'espace disponible du composant enfant. Il contient les informations de largeur et de hauteur minimales et maximales. La taille de le composant enfant doit se trouver dans la plage de contraintes. Le BoxConstraintsconstructeur par défaut est le suivant :

const BoxConstraints({
    
    
  this.minWidth = 0.0, //最小宽度
  this.maxWidth = double.infinity, //最大宽度
  this.minHeight = 0.0, //最小高度
  this.maxHeight = double.infinity //最大高度
})

Il contient 4 attributs BoxConstraintset définit également des constructeurs pratiques pour générer rapidement des règles de restriction spécifiques, BoxConstraintspar exemple BoxConstraints.tight(Size size), il peut générer une restriction de largeur et de hauteur fixe ; BoxConstraints.expand()il peut générer un conteneur aussi grand que possible pour remplir un autre conteneur BoxConstraints.

Convention : Pour la commodité de la description, si on dit qu'un composant ne contraint pas ses sous-composants ou annule les contraintes sur les sous-composants, cela signifie que la largeur et la hauteur maximales des contraintes du sous-composant sont infinies, alors que la largeur et la hauteur minimales sont 0, ce qui équivaut à ce que les sous-composants soient tout à fait corrects. Déterminez votre propre taille en fonction de l'espace dont vous avez besoin.

BoîteContrainte

ConstrainedBoxUtilisé pour ajouter des contraintes supplémentaires aux composants enfants. Par exemple, si vous souhaitez qu'un composant enfant ait une hauteur minimale de 80 pixels, vous pouvez utiliser const BoxConstraints(minHeight: 80.0)des contraintes en tant que composant enfant.

exemple

Définissons-en d'abord un redBox, qui est une boîte avec une couleur de fond rouge, sans préciser sa largeur et sa hauteur :

Widget redBox = DecoratedBox(
  decoration: BoxDecoration(color: Colors.red),
);

Nous mettons en place un container rouge d'une hauteur minimum de 50 et d'une largeur la plus grande possible .

ConstrainedBox(
  constraints: BoxConstraints(
    minWidth: double.infinity, //宽度尽可能大
    minHeight: 50.0 //最小高度为50像素
  ),
  child: Container(
    height: 5.0, 
    child: redBox ,
  ),
)

Effet:

insérez la description de l'image ici
On peut voir que bien que nous Containerdéfinissions la hauteur de _ en 5pixels, cela se termine en 50pixels, c'est pourquoi ConstrainedBoxla restriction de hauteur minimale de _ est en vigueur. Si vous Containerdéfinissez la hauteur en 80pixels, la hauteur finale de la zone rouge sera également 80en pixels, car dans cet exemple, seule la hauteur minimaleConstrainedBox est restreinte , pas la hauteur maximale .

SizeBox

SizedBoxUtilisé pour spécifier une largeur et une hauteur fixes pour les éléments enfants, tels que :

SizedBox(
  width: 80.0,
  height: 80.0,
  child: redBox
)

Effet:
insérez la description de l'image ici

En fait, SizedBoxc'est juste ConstrainedBoxune personnalisation, le code ci-dessus est équivalent à :

ConstrainedBox(
  constraints: BoxConstraints.tightFor(width: 80.0,height: 80.0),
  child: redBox, 
)

ce qui BoxConstraints.tightFor(width: 80.0,height: 80.0)équivaut à :

BoxConstraints(minHeight: 80.0,maxHeight: 80.0,minWidth: 80.0,maxWidth: 80.0)

En fait ConstrainedBox, à la fois et SizedBoxsont RenderConstrainedBoxrendus à travers, nous pouvons voir ConstrainedBoxque les méthodes SizedBoxde et createRenderObject()renvoient un RenderConstrainedBoxobjet :


RenderConstrainedBox createRenderObject(BuildContext context) {
    
    
  return RenderConstrainedBox(
    additionalConstraints: ...,
  );
}

restrictions multiples

Si un composant a plusieurs ConstrainedBoxrestrictions parent, laquelle prendra effet ? Regardons un exemple :

ConstrainedBox(
  constraints: BoxConstraints(minWidth: 60.0, minHeight: 60.0), //父
  child: ConstrainedBox(
    constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),//子
    child: redBox,
  ),
)

Ci-dessus nous avons deux pères et fils ConstrainedBox, leurs contraintes sont différentes, et l'effet courant :
insérez la description de l'image ici
l'effet d'affichage final est de 90 en largeur et de 60 en hauteur, c'est-à-dire que l'enfant ConstrainedBoxprend minWidtheffet, minHeightmais le parent ConstrainedBoxprend effet. Sur la base de cet exemple seul, nous ne pouvons conclure aucune règle. Modifions la contrainte parent-enfant dans l'exemple ci-dessus :

ConstrainedBox(
  constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),
  child: ConstrainedBox(
    constraints: BoxConstraints(minWidth: 60.0, minHeight: 60.0),
    child: redBox,
  )
)

résultat courant :

insérez la description de l'image ici

L'effet d'affichage final est toujours de 90 et la hauteur est de 60. L'effet est le même, mais la signification est différente, car minWidthc'est le parent qui prend effet à ce moment ConstrainedBox, minHeightmais l'enfant ConstrainedBox.

Grâce à l'exemple ci-dessus, nous avons constaté que lorsqu'il existe plusieurs restrictions , pour minWidthet minHeight, la plus grande valeur correspondante dans le parent et l'enfant est prise . En fait, c'est le seul moyen de s'assurer que les contraintes parent n'entrent pas en conflit avec les contraintes enfant.

UnconstrainedBox

Bien que les sous-composants doivent respecter les contraintes de leurs composants parents à tout moment, la condition préalable est qu'ils doivent être dans une relation parent-enfant. S'il existe un composant dont les sous-composants sont et dont les sous-composants sont , alors les Acontraintes Bdoivent Bêtre Crespectées C, Bet Bdoivent être Arespectées Contraintes, mais Ales contraintes de ne seront pas directement contraintes Cà moins qu'elles Bne soient transmisesA à ses propres contraintes C. En utilisant ce principe, vous pouvez implémenter un Bcomposant comme celui-ci :

  1. BCIl n'y a pas de contraintes dans la disposition du composant C(il peut être infini).
  2. CDéterminer sa propre taille en fonction de son occupation réelle de l'espace.
  3. BADéterminer sa propre taille en combinaison avec la taille des sous-composants sous réserve de respecter les contraintes.

Et ce Bcomposant est UnconstrainedBoxle composant, ce qui signifie que UnconstrainedBoxles sous-composants de ne seront plus contraints, et la taille dépend entièrement d' elle-même . En général, nous utiliserons rarement ce composant directement, mais cela peut être utile lors de la "suppression" de plusieurs restrictions . Examinons le code suivant :

ConstrainedBox(
  constraints: BoxConstraints(minWidth: 60.0, minHeight: 100.0),  //父
  child: UnconstrainedBox( //“去除”父级限制
    child: ConstrainedBox(
      constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),//子
      child: redBox,
    ),
  )
)

Dans le code ci-dessus, s'il n'y en a pas au milieu UnconstrainedBox, alors selon les multiples règles de restriction décrites ci-dessus, une 90×100boîte rouge finira par s'afficher. Mais comme la restriction du parent est UnconstrainedBox« supprimée » ConstrainedBox, elle sera éventuellement ConstrainedBoxdessinée en fonction de la restriction de l'enfant redBox, c'est-à-dire 90×20, comme le montre la figure :

insérez la description de l'image ici

Cependant, veuillez noter que UnconstrainedBoxla "suppression" des restrictions du composant parent n'est pas une véritable suppression : dans l'exemple ci-dessus, bien que la taille de la zone rouge soit 90×20, 80il y a toujours un . C'est-à-dire que la restriction du parent minHeight(100.0)est toujours en vigueur, mais elle n'affecte pas redBoxla taille de l'élément enfant final, mais il occupe toujours l'espace correspondant. On peut considérer que le parent à ce moment ConstrainedBoxagit sur l'enfant UnconstrainedBoxet redBoxn'est ConstrainedBoxlimité que par l'enfant Ce point Veuillez faire attention.

Existe-t-il un moyen de supprimer complètement la ConstrainedBoxrestriction parentale ? la réponse est négative ! Veuillez garder à l'esprit qu'à tout moment un composant enfant doit respecter les contraintes de son composant parent , donc lors de la définition d'un composant générique, si vous souhaitez spécifier des contraintes sur le composant enfant, vous devez faire attention, car une fois les contraintes spécifiées , le composant enfant lui-même ne peut pas violer la contrainte.

En développement réel, lorsque nous constatons que nous avons utilisé SizedBoxou ConstrainedBoxspécifié une largeur et une hauteur fixes pour l'élément enfant, mais que cela n'a toujours aucun effet, nous pouvons presque conclure que la contrainte a été spécifiée par le composant parent !

Par exemple, AppBardans le menu de droite de la (barre de navigation) dans la bibliothèque de composants Material, nous utilisons SizedBoxla taille spécifiée du bouton de chargement, le code est le suivant :

 AppBar(
   title: Text(title),
   actions: <Widget>[
     SizedBox(
       width: 20, 
       height: 20,
       child: CircularProgressIndicator(
         strokeWidth: 3,
         valueColor: AlwaysStoppedAnimation(Colors.white70),
       ),
     )
   ],
)

résultat courant :

insérez la description de l'image ici

Nous constaterons que la taille du bouton de chargement à droite n'a pas changé ! C'est justement parce que les contraintes du bouton AppBaront été précisées dans actions, donc si on veut personnaliser la taille du bouton de chargement, il faut UnconstrainedBox" supprimer " les contraintes de l'élément parent, le code est le suivant :

AppBar(
  title: Text(title),
  actions: <Widget>[
    UnconstrainedBox(
      child: SizedBox(
        width: 20,
        height: 20,
        child: CircularProgressIndicator(
          strokeWidth: 3,
          valueColor: AlwaysStoppedAnimation(Colors.white70),
        ),
      ),
    )
  ],
)

résultat courant :

insérez la description de l'image ici

Vous pouvez voir que cela fonctionne! En fait, il est également possible de UnconstrainedBoxremplacer Centerpar ou .Align

De plus, il convient de noter que UnconstrainedBoxbien que les contraintes puissent être annulées lors de la mise en page de ses sous-composants (les sous-composants peuvent être infinis), mais UnconstrainedBoxlui-même est contraint par son composant parent, alors lorsque UnconstrainedBoxses sous-composants deviennent plus grands, si UnconstrainedBoxla taille la dépasse Lorsque le parent est contraint, cela provoquera également une erreur de débordement, telle que :

Column(
  children: <Widget>[
    UnconstrainedBox(
      alignment: Alignment.topLeft,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(children: [Text('xx' * 30)]),
      ),
    ),
 ]

résultat courant :

insérez la description de l'image ici

Le texte a dépassé la largeur de l'écran, débordant.

Ratio d'aspect

AspectRatioLa fonction est d' ajuster le rapport d'aspect de l'élément enfant child .

AspectRatioTout d'abord, il s'étendra autant que possible dans la portée autorisée par les contraintes de mise en page. La hauteur du widget est déterminée par la largeur et le rapport, similaire à celui du milieu , BoxFitet containessaie d'occuper la zone autant que possible selon au rapport fixe.

Si une taille réalisable ne peut pas être trouvée après avoir satisfait à toutes les contraintes, AspectRatioil essaiera éventuellement de s'adapter d'abord aux contraintes de mise en page, quel que soit le ratio défini.

Les attributs illustrer
aspectRatio Le rapport d'aspect peut ne pas être disposé en fonction de cette valeur à la fin. Cela dépend de facteurs complets. Le fait que la couche externe soit autorisée à être disposée en fonction de ce rapport n'est qu'une valeur de référence
child Sous-ensemble

Exemple:

import 'package:flutter/material.dart';

void main() {
    
    
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
    
    
  const MyApp({
    
    Key? key}) : super(key: key);

  // This widget is the root of your application.
  
  Widget build(BuildContext context) {
    
    
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(title: const Text("Flutter App")),
        body: const LayoutDemo(),
      ),
    );
  }
}

// 需求:页面上显示一个容器,宽度是屏幕的宽度,高度是容器宽度的一半
class LayoutDemo extends StatelessWidget {
    
    
  const LayoutDemo({
    
    Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    
        
    return AspectRatio(
      aspectRatio: 2/1,
      child: Container(
        color: Colors.red,
      ),
    );
  }
}

insérez la description de l'image ici

LimitedBox

LimitedBoxIl est utilisé pour spécifier childla largeur et la hauteur maximales. Il peut être childlimité à la largeur et à la hauteur maximales définies par celui-ci, mais cette limitation est conditionnelle. Lorsqu'il LimitedBoxn'est pas contraint par le composant parent, sa taille est limitée. Qu'est-ce qui n'est pas contraint par le composant parent ? La plupart des composants contraindront les sous-composants, et les composants parents sans contraintes incluent ListView, Row, Columnetc. Si LimitedBoxle composant parent est contraint, LimitedBoxrien ne sera fait à ce moment, on peut penser qu'il n'y a pas de tel composant.

Par exemple le code suivant :

  
  Widget build(BuildContext context) {
    
    
      return Center(
              child: LimitedBox(
                        maxWidth: 100,
                        maxHeight: 50,
                        child: Container(color: Colors.red, child: Text("ss"*50)),
              ),
      );
  }

insérez la description de l'image ici

TextLes composants ne sont pas contraints 100x50dans la région comme nous le souhaiterions, car les composants leur Centerimposent des contraintes, nous pouvons donc utiliser les contraintes parent supprimées :LimitedBoxUnconstrainedBoxLimitedBox

  
  Widget build(BuildContext context) {
    
    
      return Center(
              child: UnconstrainedBox(
                 child: LimitedBox(
                        maxWidth: 100,
                        maxHeight: 50,
                        child: Container(color: Colors.red, child: Text("ss"*50)),
                 )
              ),
      );
  }

À ce stade, l'effet est le suivant :

insérez la description de l'image ici

Si nous ListViewajoutons le composant directement dans Container, comme suit :

ListView(
  children: <Widget>[
    Container(color: Colors.green, ),
    Container(color: Colors.red, ), 
  ], 
)

À ce stade, vous ne trouverez rien, car la taille sera définie lorsque le conteneur n'est pas contraint, 0enveloppez simplement Containerle dans LimitedBoxun :

ListView(
  children: <Widget>[
    LimitedBox(
      maxHeight: 100, 
      child: Container(color: Colors.green),
    ),
    LimitedBox(
      maxHeight: 100, 
      child: Container(color: Colors.red), 
    ), 
  ], 
)

Effet:

insérez la description de l'image ici

FractionallySizedBox

FractionallySizedBoxLa fonction est de faire en sorte que la largeur et la hauteur du composant enfant puissent être définies en fonction du pourcentage de la largeur et de la hauteur du conteneur parent .

Exemple:

class FractionallySizedBoxWidget extends StatelessWidget {
    
    
  const FractionallySizedBoxWidget({
    
    Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    
    
     return Scaffold(
       appBar: AppBar(title: const Text("FractionallySizedBox"),),
       body: Center(
         //child宽高分别是父组件宽高的80%
         child: FractionallySizedBox(
           widthFactor: 0.8,
           heightFactor: 0.8,
           child: Container(
             color: Colors.red,
           ),
         ),
       ),
     );
  }
}

Effet:

insérez la description de l'image ici

Disposition linéaire (ligne et colonne)

La disposition dite linéaire fait référence à la disposition des sous-composants le long de la direction horizontale ou verticale. Les mises en page linéaires sont implémentées dans Flutter via Rowet , similaires aux contrôles Columndans Android . et les deux héritent de .LinearLayoutRowColumnFlex

Axe principal et axe longitudinal

Pour la mise en page linéaire, il existe un axe principal et un axe vertical. Si la mise en page est dans le sens horizontal, l'axe principal fait référence à la direction horizontale et l'axe vertical fait référence à la direction verticale ; si la mise en page est dans la direction verticale, alors l'axe principal se réfère à la direction verticale, et l'axe vertical est la direction horizontale. En fait, l'axe vertical est l'axe transversal par rapport à la direction de l'axe principal.

MainAxisAlignmentDans la disposition linéaire, il existe deux classes d'énumération et qui définissent l'alignement CrossAxisAlignment, qui représentent respectivement l'alignement de l'axe principal et l'alignement de l'axe transversal.

Ligne

Row peut organiser ses widgets enfants horizontalement. Propriétés communes :

Les attributs illustrer
textDirection Indique l'ordre de mise en page des sous-composants dans le sens horizontal (de gauche à droite ou de droite à gauche) et utilise par défaut la direction du texte de l'environnement Locale actuel du système (par exemple, le chinois et l'anglais sont de gauche à droite, tandis que L'arabe va de droite à gauche)
mainAxisSize Indique l'espace occupé dans la direction de l'axe principal (horizontal). Par défaut MainAxisSize.max, cela signifie qu'il faut occuper autant d'espace que possible dans la direction horizontale. À ce stade, quel que soit widgetsl'espace horizontal réellement occupé par l'enfant, Rowla largeur du sous-marin est toujours égale à la largeur maximale dans le sens horizontal ; alors que cela MainAxisSize.minsignifie aussi peu que possible L'espace horizontal occupé par , lorsque le sous-composant n'occupe pas l'espace horizontal restant, la Rowlargeur réelle de est égale à l'espace horizontal occupé par tous les sous-composants
mainAxisAlignment Indique l'alignement des sous-composants dans le sens horizontal de Row. Si mainAxisSizela valeur est MainAxisSize.min, cette propriété n'a pas de sens, car la largeur des sous-composants est égale à Rowla largeur de . Cette propriété n'a de sens que lorsque mainAxisSizela valeur deMainAxisSize.max
verticalDirection Indique la direction d'alignement de l'axe transversal de la ligne (vertical), la valeur par défaut est VerticalDirection.down, cela signifie de haut en bas
crossAxisAlignment Indique l'alignement des sous-composants dans la direction de l'axe transversal. La hauteur de Row est égale à la hauteur du sous-élément le plus élevé du sous-composant. Sa valeur est identique à ( MainAxisAlignmentcomprenant start、end、 centertrois valeurs). La différence est que crossAxisAlignmentle système de référence est verticalDirectionque verticalDirectionla valeur fait référence VerticalDirection.downà lorsque la valeur est l'alignement en bascrossAxisAlignment.startverticalDirectionVerticalDirection.upcrossAxisAlignment.start ;
children tableau de composants enfants

Les valeurs de mainAxisAlignmentsont les suivantes :

AlignementAxeMain illustrer
MainAxisAlignment.start Indique textDirectionl'alignement le long de la direction initiale. Si textDirectionla valeur TextDirection.ltrest définie sur , cela MainAxisAlignment.startsignifie aligné à gauche, et lorsque textDirectionla valeur est TextDirection.rtl, cela signifie aligné à droite
MainAxisAlignment.end et MainAxisAlignment.startexactement le contraire
MainAxisAlignment.center Indique l'alignement central
MainAxisAlignment.spaceBetween Répartir l'espace libre uniformément entre les sous-composants
MainAxisAlignment.spaceAround Distribue l'espace libre uniformément entre les composants enfants et affiche la moitié de l'espace avant le premier enfant et après le dernier enfant
MainAxisAlignment.spaceEvenly Distribue l'espace libre uniformément entre les composants enfants et affiche également l'espace régulièrement espacé avant le premier enfant et après le dernier enfant

Il peut être compris de cette manière : textDirectionOui est mainAxisAlignmentle cadre de référence.

Exemple:

Column(
  //测试Row对齐方式,排除Column默认居中对齐的干扰
  crossAxisAlignment: CrossAxisAlignment.start,
  children: <Widget>[
    Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text(" hello world "),
        Text(" I am Jack "),
      ],
    ),
    Row(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text(" hello world "),
        Text(" I am Jack "),
      ],
    ),
    Row(
      mainAxisAlignment: MainAxisAlignment.end,
      textDirection: TextDirection.rtl,
      children: <Widget>[
        Text(" hello world "),
        Text(" I am Jack "),
      ],
    ),
    Row(
      crossAxisAlignment: CrossAxisAlignment.start,  
      verticalDirection: VerticalDirection.up,
      children: <Widget>[
        Text(" hello world ", style: TextStyle(fontSize: 30.0),),
        Text(" I am Jack "),
      ],
    ),
  ],
);

Effet:

insérez la description de l'image ici
expliquer:

  • Le premier Rowest très simple et la valeur par défaut est l'alignement au centre ;
  • Le second Row, puisque la largeur de la valeur est égale à la somme des deux largeurs mainAxisSize, l'alignement n'a pas de sens, il sera donc affiché de gauche à droite ;MainAxisSize.minRowText
  • Le troisième Rowdéfinit textDirectionla valeur TextDirection.rtl, de sorte que les sous-composants seront disposés dans l'ordre de droite à gauche, et à ce moment MainAxisAlignment.endcela signifie un alignement à gauche, de sorte que le résultat d'affichage final est comme la troisième ligne de la figure ;
  • Le quatrième Rowtest est l'alignement de l'axe vertical. Étant donné que les deux sous Text-polices sont différentes, leurs hauteurs sont également différentes. Nous spécifions verticalDirectionune valeur VerticalDirection.up, c'est-à-dire organiser de bas en haut, et crossAxisAlignmentla valeur à ce moment CrossAxisAlignment.startsignifie l'alignement en bas.

Colonne

ColumnSes sous-composants peuvent être disposés verticalement. Les paramètres sont Rowles mêmes que, sauf que la direction de mise en page est verticale.

Exemple:

import 'package:flutter/material.dart';

class CenterColumnRoute extends StatelessWidget {
    
    
  
  Widget build(BuildContext context) {
    
    
    return Column(
      crossAxisAlignment: CrossAxisAlignment.center,
      children: <Widget>[
        Text("hi"),
        Text("world"),
      ],
    );
  }
}

Effet:

insérez la description de l'image ici

expliquer:

  • Comme nous n'en avons pas spécifié Column, mainAxisSizel'utilisation de la valeur par défaut prendraMainAxisSize.max autant d'espace que possible verticalement, dans ce cas la hauteur totale de l'écran.Column
  • Puisque nous avons spécifié crossAxisAlignmentl'attribut comme CrossAxisAlignment.center, les éléments enfants Columnseront centrés dans la direction de l'axe transversal (direction horizontale). Notez que l'alignement dans le sens horizontal est limité, la largeur totale est la Columnlargeur réelle de l'espace occupé et la largeur réelle dépend de la plus grande largeur parmi les enfantsWidget . Dans cet exemple, Columnil y a deux enfants Widgetet le "monde" d'affichage Texta la plus grande largeur, donc Columnla largeur réelle de est la largeur de , il sera donc affiché au milieu du centre Text("world")après l'alignement .Text("hi")Text("world")

En fait, Rowles Columndeux ne prendront que le plus d'espace possible dans la direction de l'axe principal, et la longueur de l'axe transversal dépend de la longueur de leur plus grand élément enfant .

Si nous voulons que les deux contrôles de texte dans cet exemple soient alignés au milieu de tout l'écran du téléphone, nous avons deux méthodes :

  1. Spécifiez Columnla largeur de comme largeur d'écran ; c'est simple, nous pouvons forcer la modification de la limite de largeur par ConstrainedBoxou .SizedBox

    Par exemple : réglez sur ConstrainedBox, pour que la largeur occupe le plus d'espace possible.minWidthdouble.infinity

ConstrainedBox(	
  constraints: BoxConstraints(minWidth: double.infinity), 
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.center,
    children: <Widget>[
      Text("hi"),
      Text("world"),
    ],
  ),
);

insérez la description de l'image ici

  1. CenterLes composants peuvent être utilisés

Nidification

S'il Rowest imbriqué à l'intérieur Row, ou Columnà nouveau imbriqué à l'intérieur Column, seul le plus à l'extérieur Rowoccupera Columnautant d'espace que possible, et l'espace occupé par l'intérieur Rowou l'intérieur est la taille réelle. Voici un exemple :ColumnColumn

Container(
  color: Colors.green,
  child: Padding(
    padding: const EdgeInsets.all(16.0),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      mainAxisSize: MainAxisSize.max, //有效,外层Colum高度为整个屏幕
      children: <Widget>[
        Container(
          color: Colors.red,
          child: Column(
            mainAxisSize: MainAxisSize.max,//无效,内层Colum高度为实际高度  
            children: <Widget>[
              Text("hello world "),
              Text("I am Jack "),
            ],
          ),
        )
      ],
    ),
  ),
);

résultat courant :

insérez la description de l'image ici

Si vous voulez que l'intérieur Columnremplisse l'extérieur Column, vous pouvez utiliser Expandedle composant :

Expanded( 
  child: Container(
    color: Colors.red,
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center, //垂直方向居中对齐
      children: <Widget>[
        Text("hello world "),
        Text("I am Jack "),
      ],
    ),
  ),
)

résultat courant :

insérez la description de l'image ici

Mise en page élastique (Flex, Expanded)

La disposition flexible permet aux composants enfants d'allouer l'espace du conteneur parent selon un certain ratio. Le concept de disposition élastique existe également dans d'autres systèmes d'interface utilisateur, tels que la disposition de la boîte élastique dans H5 , AndroidFlexboxLayout , etc. La mise en page flexible dans Flutter est principalement réalisée grâce à la coopération avec Flexet Expanded.

Fléchir

FlexLes composants peuvent organiser les sous-composants le long de la direction horizontale ou verticale. Si vous connaissez la direction de l'axe principal , vous pouvez l'utiliser directement Rowou Columnce sera plus pratique, car ils héritent tous de Flex, et les paramètres sont fondamentalement les mêmes, donc Flexfondamentalement vous peut utiliser Rowou Column.

FlexLa fonction elle-même est très puissante et peut également Expandedcoopérer avec des composants pour obtenir une disposition flexible. Ensuite, nous n'aborderons que Flexles propriétés liées à la mise en page flexible (d'autres propriétés ont été introduites dans l'introduction Rowet ).Column

Flex({
    
    
  ...
  required this.direction, //弹性布局的方向, Row默认为水平方向,Column默认为垂直方向
  List<Widget> children = const <Widget>[],
})

FlexHérité de MultiChildRenderObjectWidget, correspondant RenderObjectà RenderFlex, RenderFleximplémente son algorithme de disposition dans.

Flexible

FlexibleIl ne peut être utilisé qu'en tant Row、Column、Flexqu'enfant de (sinon une erreur sera signalée, car tous deux héritent de Rowet , ils peuvent donc également être utilisés comme leur enfant), le composant peut contrôler les contrôles enfants de pour remplir le contrôle parent, et le l'espace restant peut être utilisé pour agrandir l'espace occupé.ColumnFlexFlexibleFlexibleRow、Column、Flex

Par exemple, Rowil y a 3 sous-contrôles dans , avec une largeur fixe des deux côtés, et celui du milieu occupe l'espace restant, le code est le suivant :

Row(
 children: <Widget>[
    Container(color: Colors.blue, height: 50, width: 100, ),
    Flexible(child: Container(color: Colors.red, height: 50) ),
    Container(color: Colors.blue, height: 50, width: 100, ), 
  ], 
)

L'effet est comme indiqué sur la figure :

insérez la description de l'image ici

Il y a encore 3 sous-contrôles. J'espère que le premier prendra 1/6, le deuxième prendra 2/6, et le troisième prendra 3/6. Le code est le suivant :

Column(
  children: <Widget>[
    Flexible(
      flex: 1, 
      child: Container(
        color: Colors.blue, 
        alignment: Alignment.center, 
        child: const Text('1 Flex/ 6 Total',style: TextStyle(color: Colors.white),), 
      ), 
    ),
    Flexible(
      flex: 2,
      child: Container(
        color: Colors.red, 
        alignment: Alignment.center,
        child: const Text('2 Flex/ 6 Total',style: TextStyle(color: Colors.white),),
      ),
    ),
    Flexible(
      flex: 3, 
      child: Container(
        color: Colors.green, 
        alignment: Alignment.center, 
        child: const Text('3 Flex/ 6 Total',style: TextStyle(color: Colors.white),), 
      ), 
    ), 
  ],
)

L'effet est comme indiqué sur la figure :

insérez la description de l'image ici

Rapport de sous-contrôle = flex de sous-contrôle actuel / somme de tous les flex de sous-contrôle.

FlexibleLe fitparamètre du milieu indique comment remplir l'espace restant :

  • tight: Doit (obligatoirement) remplir l'espace restant.
  • loose: Remplissez l'espace restant aussi grand que possible, mais il n'est pas nécessaire de le remplir.

Ces deux-là ne semblent pas très simples à appréhender, à quoi bon remplir l'espace restant le plus grand possible ? Quand va-t-il se remplir ? Voir l'exemple ci-dessous :

Row(
 children: <Widget>[
   Container(color: Colors.blue, height: 50, width: 100,),
   Flexible(
       child: Container(
         color: Colors.red, 
         height: 50, 
         child: const Text('Container',style: TextStyle(color: Colors.white),),
       )
   ),
   Container(color: Colors.blue, height: 50, width: 100, ), 
 ],
)

Effet:

insérez la description de l'image ici

Ce code est basé sur le code du haut pour Containerajouter Textdes sous-contrôles au rouge au milieu.
À ce stade, le rouge Containerne remplit plus l'espace, puis Containerajoute un alignement. Le code est le suivant :

Row(
 children: <Widget>[
   Container(color: Colors.blue, height: 50, width: 100,),
   Flexible(
       child: Container(
         color: Colors.red, 
         height: 50, 
         alignment: Alignment.center,
         child: const Text('Container',style: TextStyle(color: Colors.white),),
       )
   ),
   Container(color: Colors.blue, height: 50, width: 100, ), 
 ],
)

Effet:

insérez la description de l'image ici
Cela remplit à nouveau l'espace restant. ContainerLa valeur par défaut est d'adapter la taille du contrôle enfant, mais lorsque l'alignement est défini, il remplira le contrôle parent (voir iciContainer pour plus de détails ), donc le fait de remplir l'espace restant dépend si le contrôle enfant doit remplir le contrôle parental.

Si vous faites passer Flexiblele contrôle neutronique de à , le code est le suivant :ContainerOutlineButton

Row(
 children: <Widget>[
   Container(color: Colors.blue, height: 50, width: 100,),
   Flexible(child: OutlineButton(child: Text('OutlineButton'), ),),
   Container(color: Colors.blue, height: 50, width: 100, ), 
 ],
)

OutlineButtonDans des circonstances normales, le contrôle parent n'est pas rempli, donc l'effet final ne doit pas remplir l'espace restant, comme illustré dans la figure :

insérez la description de l'image ici

Étendu

Expandedest Flexibleun cas particulier de , qui hérite de Flexible:

class Expanded extends Flexible {
    
    
  /// Creates a widget that expands a child of a [Row], [Column], or [Flex]
  /// so that the child fills the available space along the flex widget's
  /// main axis.
  const Expanded({
    
    
    super.key,
    super.flex,
    required super.child,
  }) : super(fit: FlexFit.tight);
}

Ne peut donc Expandedêtre utilisé qu'en tant Row、Column、Flexqu'enfant de , il peut "étendre" Flexl'espace occupé par les composants enfants en proportion. Notez que le paramètre Expandedde fitest fixe FlexFit.tight, ce qui signifie Expandedqu'il faut (forcer) pour remplir l'espace restant.

  • flex: Coefficient d'élasticité, s'il est 0ou null, childil est inélastique, c'est-à-dire l'espace qui ne sera pas occupé par la dilatation. Si plus grand 0, tous les espaces libres de l'axe principal sont divisés Expandedselon sa proportion.flex

Ce qui précède OutlineButtonveut remplir l'espace restant peut utiliser Expanded:

Row(
 children: <Widget>[
   Container(color: Colors.blue, height: 50, width: 100,),
   Expanded(child: OutlineButton(child: Text('OutlineButton'), ),),
   Container(color: Colors.blue, height: 50, width: 100, ), 
 ],
)

Effet:

insérez la description de l'image ici

Prenons un autre exemple :

class FlexLayoutTestRoute extends StatelessWidget {
    
    
  
  Widget build(BuildContext context) {
    
    
    return Column(
      children: <Widget>[
        //Flex的两个子widget按1:2来占据水平空间  
        Flex(
          direction: Axis.horizontal,
          children: <Widget>[
            Expanded(
              flex: 1,
              child: Container(
                height: 30.0,
                color: Colors.red,
              ),
            ),
            Expanded(
              flex: 2,
              child: Container(
                height: 30.0,
                color: Colors.green,
              ),
            ),
          ],
        ),
        Padding(
          padding: const EdgeInsets.only(top: 20.0),
          child: SizedBox(
            height: 100.0,
            //Flex的三个子widget,在垂直方向按2:1:1来占用100像素的空间  
            child: Flex(
              direction: Axis.vertical,
              children: <Widget>[
                Expanded(
                  flex: 2,
                  child: Container(
                    height: 30.0,
                    color: Colors.red,
                  ),
                ),
                Spacer(
                  flex: 1,
                ),
                Expanded(
                  flex: 1,
                  child: Container(
                    height: 30.0,
                    color: Colors.green,
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    );
  }
}

résultat courant :

insérez la description de l'image ici

SpacerLa fonction dans l'exemple est d'occuper une proportion d'espace spécifiée. En fait, il s'agit simplement Expandedd'une classe wrapper. SpacerLe code source est le suivant :

class Spacer extends StatelessWidget {
    
    
  const Spacer({
    
    Key? key, this.flex = 1})
    : assert(flex != null),
      assert(flex > 0),
      super(key: key);
  
  final int flex;

  
  Widget build(BuildContext context) {
    
    
    return Expanded(
      flex: flex,
      child: const SizedBox.shrink(),
    );
  }
}

Disposition flexible en pratique

Au quotidien, vous rencontrez souvent une telle UI : il y a un titre devant, et plusieurs labels derrière. Peu importe la longueur du titre, les labels doivent être entièrement affichés, et le titre est tronqué en fonction de l'espace restant . Plus précisément, plusieurs situations typiques illustrées dans la figure ci-dessous doivent être rencontrées.

Figure 11-1 Plusieurs mises en page typiques du style d'interface utilisateur cible

La figure ci-dessus montre plusieurs mises en page du style d'interface utilisateur cible : lorsque le titre et l'étiquette peuvent être entièrement affichés, ils sont affichés séquentiellement, comme indiqué à la ligne 1 de la figure ; lorsque le titre est trop long, l'étiquette est entièrement affichée , et le titre est tronqué de lui-même. Comme indiqué aux lignes 2 et 3 de la figure ; la longueur maximale de l'étiquette ne peut pas dépasser la moitié de l'écran, comme indiqué à la ligne 4 de la figure.

Il convient de noter que l'interface utilisateur ci-dessus ne peut pas Flexibleêtre réalisée à l'aide de plusieurs composants. D'après la description ci-dessus, il ressort que la partie étiquette doit être mise en page en premier et que la largeur maximale est limitée à la moitié de l'écran ; tandis que la partie titre doit être mis en page plus tard, et la largeur maximale est L'espace restant après la fin de la mise en page de l'étiquette. Combiné avec l'analyse du chapitre 6, l'étiquette doit être définie comme un non- Flexnœud et le titre doit être défini comme Flexun nœud, de sorte que l'ordre de mise en page attendu mentionné ci-dessus puisse être atteint. Le code d'implémentation spécifique est le suivant :

class Tile extends StatelessWidget {
    
    
  final String title;
  final String tag;
  const Tile({
    
    Key key, this.title, this.tag}) : super(key: key);
  
  Widget build(BuildContext context) {
    
    
    return Row(
      children: [
        Flexible( // 填充剩余宽度
          child: Text(title,
            maxLines: 1, overflow: TextOverflow.ellipsis,
            style: TextStyle(fontSize: 20, color: Colors.black87),
          ),
        ), // Flexible
        ConstrainedBox( // 约束最大宽度
          constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width/2),
          child: Text(tag, maxLines: 1,
            style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
          ),
        ), // ConstrainedBox
      ],
    ); // Row
  }
} // StatelessWidget 

Dans l'implémentation ci-dessus, premièrement, ConstrainedBoxla largeur maximale de la partie étiquette est contrainte, puis Flexiblele titre peut remplir entièrement l'espace restant à l'aide de composants.

Mise en page de flux (Wrap, Flow)

Lors de l'introduction de Rowet Colum, si l'enfant widgetdépasse la plage d'écran, une erreur de dépassement sera signalée, telle que :

Row(
  children: <Widget>[
    Text("xxx"*100)
  ],
);

insérez la description de l'image ici

Comme vous pouvez le voir, la partie de débordement à droite signale une erreur. C'est parce qu'il Rown'y a qu'une seule ligne par défaut, et elle ne s'enroulera pas lorsqu'elle dépassera l'écran. Nous appelons une mise en page qui s'étend automatiquement au-delà de la plage d'affichage de l'écran une mise en page fluide. Dans Flutter, Wrapet sont Flowutilisés pour prendre en charge la mise en page du flux. Si l'exemple ci-dessus est Rowremplacé Wrappar la partie de débordement, la ligne sera automatiquement pliée. Ensuite, nous introduirons Wrapet respectivement Flow.

Envelopper

WrapRow\ColumnSemblable à la plupart des attributs de et sont à la fois à une seule ligne Rowet Columnà une seule colonne, Wrapce qui dépasse cette limitation. Lorsque mainAxisl'espace supérieur est insuffisant, crossAxisl'affichage sera agrandi vers le haut.

Voici Wrapquelques propriétés couramment utilisées :

Les attributs illustrer
direction La direction de l'axe principal, la valeur par défaut est horizontaleAxis.horizontal
alignment La méthode d'alignement de l'axe principal, la valeur par défautWrapAlignment.start
textDirection sens du texte
verticalDirection Définit l'ordre de placement des enfants, la valeur par défaut est VerticalDirection.down, identique aux Flexattributs associés
spacing L'espacement des sous-widgets dans la direction de l'axe principal
runSpacing Espacement dans le sens de l'axe transversal
runAlignment Alignement dans le sens de l'axe transversal

Exemple:

Wrap(
   spacing: 8.0, // 主轴(水平)方向间距
   runSpacing: 4.0, // 交叉轴(垂直)方向间距
   alignment: WrapAlignment.center, //沿主轴方向居中
   children: <Widget>[
     Chip(
       avatar: CircleAvatar(backgroundColor: Colors.blue, child: Text('A')),
       label: Text('Hamilton'),
     ),
     Chip(
       avatar: CircleAvatar(backgroundColor: Colors.blue, child: Text('M')),
       label: Text('Lafayette'),
     ),
     Chip(
       avatar: CircleAvatar(backgroundColor: Colors.blue, child: Text('H')),
       label: Text('Mulligan'),
     ),
     Chip(
       avatar: CircleAvatar(backgroundColor: Colors.blue, child: Text('J')),
       label: Text('Laurens'),
     ),
   ],
)

Effet:
insérez la description de l'image ici

Couler

Nous l'utilisons généralement rarement Flow, car il est trop compliqué et nécessite de réaliser widgetla conversion de position du sous-marin par lui-même. Dans de nombreux scénarios, la première chose à considérer est Wrapde savoir s'il répond aux exigences. FlowIl est principalement utilisé dans les scènes qui nécessitent des stratégies de mise en page personnalisées ou des exigences de performances élevées (comme dans l'animation). FlowIl a les avantages suivants :

  • Bonnes performances ; Flowc'est un contrôle très efficace pour ajuster la taille et la position des sous-composants. Il Flowutilise la matrice de transformation pour optimiser l'ajustement de la position des sous-composants : après Flowle positionnement, si la taille ou la position des sous-composants change, dans FlowDelegatele La paintChildren()méthode appelle context.paintChildà redessiner , et context.paintChildla matrice de transformation est utilisée lors du redessin, et la position du composant n'est pas réellement ajustée.
  • Flexible ; puisque nous devons implémenter la méthode nous-mêmes FlowDelegate, paintChildren()nous devons calculer nous-mêmes la position de chaque composant, afin de pouvoir personnaliser la stratégie de mise en page.

défaut:

  • Compliqué à utiliser.
  • FlowIl ne peut pas s'adapter à la taille du composant enfant et doit renvoyer une taille fixe en spécifiant la taille du conteneur parent ou en l'implémentant TestFlowDelegate.getSize

Exemple : Nous créons une disposition de flux personnalisée pour six blocs de couleur :

Flow(
  delegate: TestFlowDelegate(margin: EdgeInsets.all(10.0)),
  children: <Widget>[
    Container(width: 80.0, height:80.0, color: Colors.red,),
    Container(width: 80.0, height:80.0, color: Colors.green,),
    Container(width: 80.0, height:80.0, color: Colors.blue,),
    Container(width: 80.0, height:80.0,  color: Colors.yellow,),
    Container(width: 80.0, height:80.0, color: Colors.brown,),
    Container(width: 80.0, height:80.0,  color: Colors.purple,),
  ],
)

atteindre TestFlowDelegate:

class TestFlowDelegate extends FlowDelegate {
    
    
  EdgeInsets margin;

  TestFlowDelegate({
    
    this.margin = EdgeInsets.zero});

  double width = 0;
  double height = 0;

  
  void paintChildren(FlowPaintingContext context) {
    
    
    var x = margin.left;
    var y = margin.top;
    //计算每一个子widget的位置
    for (int i = 0; i < context.childCount; i++) {
    
    
      var w = context.getChildSize(i)!.width + x + margin.right;
      if (w < context.size.width) {
    
    
        context.paintChild(i, transform: Matrix4.translationValues(x, y, 0.0));
        x = w + margin.left;
      } else {
    
    
        x = margin.left;
        y += context.getChildSize(i)!.height + margin.top + margin.bottom;
        //绘制子widget(有优化)
        context.paintChild(i, transform: Matrix4.translationValues(x, y, 0.0));
        x += context.getChildSize(i)!.width + margin.left + margin.right;
      }
    }
  }

  
  Size getSize(BoxConstraints constraints) {
    
    
    // 指定Flow的大小,简单起见我们让宽度竟可能大,但高度指定为200,
    // 实际开发中我们需要根据子元素所占用的具体宽高来设置Flow大小
    return Size(double.infinity, 200.0);
  }

  
  bool shouldRepaint(FlowDelegate oldDelegate) {
    
    
    return oldDelegate != this;
  }
}

résultat courant :

insérez la description de l'image ici

可以看到我们主要的任务就是实现paintChildren,它的主要任务是确定每个子widget位置。由于Flow不能自适应子widget的大小,我们通过在getSize返回一个固定大小来指定Flow的大小。

注意,如果我们需要自定义布局策略,一般首选的方式是通过直接继承RenderObject,然后通过重写 performLayout 的方式实现。

层叠布局(Stack、Positioned)

层叠布局和 Web 中的绝对定位、Android 中的 FrameLayout 相似,子组件可以根据距父容器四个角的位置来确定自身的位置。子组件是按照代码中声明的顺序堆叠起来。Flutter中使用Stack结合PositionedAlign这两个组件来配合实现定位。Stack允许子组件堆叠,而Positioned用于根据Stack的四个角来确定子组件的位置。

Stack

常用属性:

属性 说明
alignment 此参数决定如何去对齐没有定位(没有使用Positioned)或部分定位的子组件。
所谓部分定位,在这里特指没有在某一个轴上定位:leftright为横轴,topbottom为纵轴,只要包含某个轴上的一个定位属性就算在该轴上有定位。默认值是AlignmentDirectional.topStart
textDirection RowWraptextDirection功能一样,都用于确定alignment对齐的参考系,
即:textDirection的值为TextDirection.ltr,则alignmentstart代表左,end代表右,即从左往右的顺序;
textDirection的值为TextDirection.rtl,则alignmentstart代表右,end代表左,即从右往左的顺序
fit 此参数用于确定没有定位的子组件如何去适应Stack的大小。StackFit.loose表示使用子组件的大小,StackFit.expand表示扩伸到Stack的大小,默认是StackFit.loose
clipBehavior 此属性决定对超出Stack显示空间的部分如何剪裁,Clip枚举类中定义了剪裁的方式,默认是Clip.hardEdge 表示直接剪裁,不应用抗锯齿

Positioned

常用属性:

属性 说明
left 子元素距离左侧距离
top 子元素距离顶部的距离
right 子元素距离右侧距离
bottom 子元素距离底部的距离
child 子组件
width **子组件的高度 **
height 子组件的高度

注意,Positionedwidthheight 和其他地方的意义稍微有点区别,此处用于配合left、top 、right、 bottom来定位组件,举个例子,在水平方向时,你只能指定left、right、width三个属性中的两个,如指定leftwidth后,right会自动算出(left+width),如果同时指定三个属性则会报错,垂直方向同理。另外,宽度和高度必须是固定值,没法使用double.infinity

示例:下面代码通过对几个Text组件的定位来演示StackPositioned的特性

//通过ConstrainedBox来确保Stack占满屏幕
ConstrainedBox(
  constraints: BoxConstraints.expand(),
  child: Stack(
    alignment:Alignment.center , //指定未定位或部分定位widget的对齐方式
    //注意:相对于外部容器进行定位,如果没有外部容器就相对于整个屏幕进行定位
    children: <Widget>[
      Container(
        child: Text("Hello world",style: TextStyle(color: Colors.white)),
        color: Colors.red,
      ),
      Positioned(
        left: 18.0,
        child: Text("I am Jack"),
      ),
      Positioned(
        top: 18.0,
        child: Text("Your friend"),
      )        
    ],
  ),
);

效果:

insérez la description de l'image ici

  • 由于第一个子文本组件Text("Hello world")没有指定定位,并且alignment值为Alignment.center,所以它会居中显示。
  • 第二个子文本组件Text("I am Jack")只指定了水平方向的定位(left),所以属于部分定位,即垂直方向上没有定位,那么它在垂直方向的对齐方式则会按照alignment指定的对齐方式对齐,即垂直方向居中。
  • 对于第三个子文本组件Text("Your friend"),和第二个Text原理一样,只不过是水平方向没有定位,则水平方向居中。

我们给上例中的Stack指定一个fit属性,然后将三个子文本组件的顺序调整一下:

Stack(
  alignment:Alignment.center ,
  fit: StackFit.expand, //未定位widget占满Stack整个空间
  children: <Widget>[
    Positioned(
      left: 18.0,
      child: Text("I am Jack"),
    ),
    Container(child: Text("Hello world",style: TextStyle(color: Colors.white)),
      color: Colors.red,
    ),
    Positioned(
      top: 18.0,
      child: Text("Your friend"),
    )
  ],
),

效果:

insérez la description de l'image ici

可以看到,由于第二个子文本组件没有定位,所以fit属性会对它起作用,就会占满Stack。由于Stack子元素是堆叠的,所以第一个子文本组件被第二个遮住了,而第三个在最上层,所以可以正常显示。

StackPositioned实现固定导航案例

import 'package:flutter/material.dart';

void main() {
    
    
  runApp(MaterialApp(
    home: Scaffold(
        appBar: AppBar(title: const Text("你好Flutter")), body: const HomePage()),
  ));
}

class HomePage extends StatelessWidget {
    
    
  const HomePage({
    
    Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    
    
    //获取设备的宽度和高度
    final size = MediaQuery.of(context).size;
    return Stack(
      children: [
        ListView(
          padding: const EdgeInsets.only(top: 50),
          children: List.generate(
              40, (index) => ListTile(title: Text("我是一个列表$index"))),
        ),
        Positioned(
            left: 0,
            top: 0,
            // bottom: 0, // 改为bottom即底部固定
            width: size.width, //配置子元素的宽度和高度  没法使用double.infinity
            height: 44, //配置子元素的宽度和高度
            child: Container(
              alignment: Alignment.center,
              color: Colors.red,
              child: const Text(
                "二级导航",
                style: TextStyle(color: Colors.white),
              ),
            ))
      ],
    );
  }
}

效果:

insérez la description de l'image ici
上面代码中MediaQuery可以到获取屏幕宽度和高度,可以在build方法中调用:

Widget build(BuildContext context) {
    
    
	final size =MediaQuery.of(context).size;
	final width =size.width;
	final height =size.height;
	...
}

对齐与相对定位(Align)

通过StackPositioned,我们可以指定多个子元素相对于父元素各个边的精确偏移,并且可以重叠。但如果我们只想简单的调整一个子元素在父元素中的位置的话,使用 Align 组件会更简单一些。

属性 说明
alignment 需要一个AlignmentGeometry类型的值,表示子组件在父组件中的起始位置。AlignmentGeometry 是一个抽象类,它有两个常用的子类:AlignmentFractionalOffset
widthFactorheightFactor 用于确定 Align 组件本身宽高的属性;它们是两个缩放因子,会分别乘以子元素的宽、高,最终的结果就是 Align 组件的宽高。如果值为null,则组件的宽高将会占用尽可能多的空间。

简单示例:

Container(
  height: 120.0,
  width: 120.0,
  color: Colors.blue.shade50,
  child: Align(
    alignment: Alignment.topRight,
    child: FlutterLogo(size: 60),
  ),
)

效果:

insérez la description de l'image ici

FlutterLogo 是 Flutter SDK 提供的一个组件,内容就是 Flutter 的 logo 。在上面的例子中,我们显式指定了Container的宽、高都为 120。如果我们不显式指定宽高,而通过同时指定widthFactorheightFactor2 也是可以达到同样的效果:

Align(
  widthFactor: 2,
  heightFactor: 2,
  alignment: Alignment.topRight,
  child: FlutterLogo(size: 60),
),

因为FlutterLogo的宽高为 60,则Align的最终宽高都为2*60=120

另外,我们还通过Alignment.topRight将FlutterLogo定位在Container的右上角。那Alignment.topRight是什么呢?通过源码我们可以看到其定义如下:

static const Alignment topRight = Alignment(1.0, -1.0);

可以看到它只是Alignment的一个实例,下面我们介绍一下Alignment

Alignment

Alignment继承自AlignmentGeometry,表示矩形内的一个点,他有两个属性x、y,分别表示在水平和垂直方向的偏移,Alignment定义如下:

Alignment(this.x, this.y)

Alignment Widget会以矩形的中心点作为坐标原点,即Alignment(0.0, 0.0)x、y的值从-11分别代表矩形左边到右边的距离和顶部到底边的距离,因此2个水平(或垂直)单位则等于矩形的宽(或高),如Alignment(-1.0, -1.0) 代表矩形的左侧顶点,而Alignment(1.0, 1.0)代表右侧底部终点,而Alignment(1.0, -1.0) 则正是右侧顶点,即Alignment.topRight。为了使用方便,矩形的原点、四个顶点,以及四条边的终点在Alignment类中都已经定义为了静态常量。

insérez la description de l'image ici
Alignment可以通过其坐标转换公式将其坐标转为子元素的具体偏移坐标:

(Alignment.x * childWidth / 2 + childWidth / 2, Alignment.y * childHeight / 2 + childHeight / 2)

其中childWidth为子元素的宽度,childHeight为子元素高度。

现在我们再看看上面的示例,我们将Alignment(1.0, -1.0)带入上面公式,可得FlutterLogo的实际偏移坐标正是(60,0)

下面再看一个例子:

Align(
  widthFactor: 2,
  heightFactor: 2,
  alignment: Alignment(2,0.0),
  child: FlutterLogo(size: 60),
)

我们可以先想象一下运行效果:将Alignment(2,0.0)带入上述坐标转换公式,可以得到FlutterLogo的实际偏移坐标为(90,30)。实际运行如图所示:

insérez la description de l'image ici

FractionalOffset

FractionalOffset 继承自 Alignment,它和 Alignment唯一的区别就是坐标原点不同!FractionalOffset 的坐标原点为矩形的左侧顶点,这和布局系统的一致,所以理解起来会比较容易。FractionalOffset的坐标转换公式为:

实际偏移 = (FractionalOffse.x * childWidth, FractionalOffse.y * childHeight)

简单示例:

Container(
  height: 120.0,
  width: 120.0,
  color: Colors.blue[50],
  child: Align(
    alignment: FractionalOffset(0.2, 0.6),
    child: FlutterLogo(size: 60),
  ),
)

效果:

insérez la description de l'image ici

我们将FractionalOffset(0.2, 0.6)带入坐标转换公式得FlutterLogo实际偏移为(12,36),和实际运行效果吻合。

建议在需要制定一些精确的偏移时应优先使用FractionalOffset,因为它的坐标原点和布局系统相同,能更容易算出实际偏移。

Align和Stack对比

可以看到,AlignStack/Positioned都可以用于指定子元素相对于父元素的偏移,但它们还是有两个主要区别:

  1. 定位参考系统不同Stack/Positioned定位的的参考系可以是父容器矩形的四个顶点;而Align则需要先通过 alignment 参数来确定坐标原点,不同的alignment会对应不同原点,最终的偏移是需要通过alignment的转换公式来计算出。
  2. Stack可以有多个子元素,并且子元素可以堆叠,而Align只能有一个子元素,不存在堆叠。

Align结合Stack使用:

class HomePage extends StatelessWidget {
    
    
  const HomePage({
    
    Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    
    
    return Center(
      child: Container(
        height: 400,
        width: 300,
        color: Colors.red,
        child: const Stack(
          // alignment: Alignment.center,
          children: <Widget>[
            Align(
              alignment: Alignment(1,-0.2),
              child: Icon(Icons.home,size: 40,color: Colors.white),
            ),
            Align(
              alignment: Alignment.center,
              child: Icon(Icons.search,size: 30,color: Colors.white),
            ),
            Align(
              alignment: Alignment.bottomRight,
              child: Icon(Icons.settings_applications,size: 30,color:
              Colors.white),
            )
          ],
        ),
      ),
    );
  }
} 

Center组件

Center组件的源码定义如下:

class Center extends Align {
    
    
  const Center({
    
     Key? key, double widthFactor, double heightFactor, Widget? child })
    : super(key: key, widthFactor: widthFactor, heightFactor: heightFactor, child: child);
}

可以看到Center继承自Align,它比Align只少了一个alignment 参数;由于Align的构造函数中alignment 默认值为Alignment.center,所以,我们可以认为Center组件其实是对齐方式确定(Alignment.center)了的Align

上面我们讲过当widthFactorheightFactornull时组件的宽高将会占用尽可能多的空间,这一点需要特别注意,我们通过一个示例说明:

...//省略无关代码
DecoratedBox(
  decoration: BoxDecoration(color: Colors.red),
  child: Center(
    child: Text("xxx"),
  ),
),
DecoratedBox(
  decoration: BoxDecoration(color: Colors.red),
  child: Center(
    widthFactor: 1,
    heightFactor: 1,
    child: Text("xxx"),
  ),
)

效果:
insérez la description de l'image ici

熟悉Web开发的同学可能会发现Align组件的特性和Web开发中相对定位(position: relative)非常像,是的!在大多数时候,我们可以直接使用Align组件来实现Web中相对定位的效果。

Card 组件

Card是卡片组件块,内容可以由大多数类型的Widget构成,Card具有圆角和阴影,这让它看起来有立体感。

属性 说明
margin 外边距
child 子组件
elevation 阴影值的深度
color 背景颜色
shadowColor 阴影颜色
margin 外边距
clipBehavior 内容溢出的剪切方式
Clip.none不剪切
Clip.hardEdge裁剪但不应用抗锯齿
Clip.antiAlias裁剪而且抗锯齿
Clip.antiAliasWithSaveLayer带有抗锯齿的剪辑,并在剪辑之后立即保存saveLayer
Shape Card的阴影效果,默认的阴影效果为圆角的长方形边。
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10))
),

Card 实现一个通讯录的卡片效果:

import 'package:flutter/material.dart';

void main() {
    
    
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
    
    
  const MyApp({
    
    Key? key}) : super(key: key);

  // This widget is the root of your application.
  
  Widget build(BuildContext context) {
    
    
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(title: const Text("Flutter App")),
        body: const LayoutDemo(),
      ),
    );
  }
}

class LayoutDemo extends StatelessWidget {
    
    
  const LayoutDemo({
    
    Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    
    
    return ListView(
      children: [
        Card(
          shape: RoundedRectangleBorder(
              //Card的阴影效果
              borderRadius: BorderRadius.circular(10)),
          elevation: 20, //阴影值的深度
          margin: const EdgeInsets.all(10),
          child: Column(
            children: const [
              ListTile(
                title: Text("张三", style: TextStyle(fontSize: 28)),
                subtitle: Text("高级软件工程师"),
              ),
              Divider(),
              ListTile(
                title: Text("电话:152222222"),
              ),
              ListTile(
                title: Text("地址:北京市海淀区 xxx"),
              ),
            ],
          ),
        ),
        Card(
          shape: RoundedRectangleBorder(
              //Card的阴影效果
              borderRadius: BorderRadius.circular(10)),
          elevation: 20,
          margin: const EdgeInsets.all(10),
          // color:Colors.black12,  //背景颜色
          child: Column(
            children: const [
              ListTile(
                title: Text("李四", style: TextStyle(fontSize: 28)),
                subtitle: Text("Flutter高级软件工程师"),
              ),
              Divider(),
              ListTile(
                title: Text("电话:152222222"),
              ),
              ListTile(
                title: Text("地址:北京市海淀区 xxx"),
              ),
            ],
          ),
        ),
         Card(
          shape: RoundedRectangleBorder(
              //Card的阴影效果
              borderRadius: BorderRadius.circular(10)),
          elevation: 20, //阴影值的深度
          margin: const EdgeInsets.all(10),
          child: Column(
            children: const [
              ListTile(
                title: Text("张三", style: TextStyle(fontSize: 28)),
                subtitle: Text("高级软件工程师"),
              ),
              Divider(),
              ListTile(
                title: Text("电话:152222222"),
              ),
              ListTile(
                title: Text("地址:北京市海淀区 xxx"),
              ),
            ],
          ),
        ),
      ],
    );
  }
}

insérez la description de l'image ici

Card 实现一个图文列表卡片效果:

import 'package:flutter/material.dart';

void main() {
    
    
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
    
    
  const MyApp({
    
    Key? key}) : super(key: key);

  // This widget is the root of your application.
  
  Widget build(BuildContext context) {
    
    
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(title: const Text("Flutter App")),
        body: const LayoutDemo(),
      ),
    );
  }
}

class LayoutDemo extends StatelessWidget {
    
    
  const LayoutDemo({
    
    Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    
    
    return ListView(
      children: [
        Card(
          shape: RoundedRectangleBorder(
            borderRadius:BorderRadius.circular(10)
          ),
          elevation: 20,
          margin: const EdgeInsets.all(10),
          child: Column(
            children: [
              AspectRatio(
                aspectRatio: 16 / 9,
                child: Image.network(
                    "https://www.itying.com/images/flutter/3.png",
                    fit: BoxFit.cover),
              ),
              ListTile(
                leading: ClipOval(
                  child:Image.network(
                    "https://www.itying.com/images/flutter/3.png",
                    fit: BoxFit.cover,
                    height: 40,
                    width: 40,
                ),
                ),
                title: const Text("xxxxxxxxx"),
                subtitle: const Text("xxxxxxxxx"),
              )
            ],
          ),
        ),
        Card(
          shape: RoundedRectangleBorder(
            borderRadius:BorderRadius.circular(10)
          ),
          elevation: 20,
          margin: const EdgeInsets.all(10),
          child: Column(
            children: [
              AspectRatio(
                aspectRatio: 16 / 9,
                child: Image.network(
                    "https://www.itying.com/images/flutter/3.png",
                    fit: BoxFit.cover),
              ),
              const ListTile(
                leading: CircleAvatar(
                  backgroundImage: NetworkImage("https://www.itying.com/images/flutter/4.png"),
                ),
                title: Text("xxxxxxxxx"),
                subtitle: Text("xxxxxxxxx"),
              )
            ],
          ),
        )
      ],
    );
  }
}

insérez la description de l'image ici

LayoutBuilder

通过 LayoutBuilder,我们可以在布局过程中拿到父组件传递的约束信息,然后我们可以根据约束信息动态的构建不同的布局。

比如我们实现一个响应式的 Column 组件 ResponsiveColumn,它的功能是当当前可用的宽度小于 200 时,将子组件显示为一列,否则显示为两列。简单来实现一下:

class ResponsiveColumn extends StatelessWidget {
    
    
  const ResponsiveColumn({
    
    Key? key, required this.children}) : super(key: key);
  final List<Widget> children;
  
  Widget build(BuildContext context) {
    
    
    // 通过 LayoutBuilder 拿到父组件传递的约束,然后判断 maxWidth 是否小于200
    return LayoutBuilder(
      builder: (BuildContext context, BoxConstraints constraints) {
    
    
        if (constraints.maxWidth < 200) {
    
     // 最大宽度小于200,显示单列 
          return Column(children: children, mainAxisSize: MainAxisSize.min);
        } else {
    
     // 大于200,显示双列 
          var _children = <Widget>[];
          for (var i = 0; i < children.length; i += 2) {
    
    
            if (i + 1 < children.length) {
    
    
              _children.add(Row(
                children: [children[i], children[i + 1]],
                mainAxisSize: MainAxisSize.min,
              ));
            } else {
    
    
              _children.add(children[i]);
            }
          }
          return Column(children: _children, mainAxisSize: MainAxisSize.min);
        }
      },
    );
  }
}

class LayoutBuilderRoute extends StatelessWidget {
    
    
  const LayoutBuilderRoute({
    
    Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    
    
    var _children = List.filled(6, Text("A"));
    // Column在本示例中在水平方向的最大宽度为屏幕的宽度
    return Column(
      children: [
        // 限制宽度为190,小于 200
        SizedBox(width: 190, child: ResponsiveColumn(children: _children)),
        ResponsiveColumn(children: _children),
        LayoutLogPrint(child:Text("xx")) // 下面介绍
      ],
    );
  }
}

可以发现 LayoutBuilder 的使用很简单,但是不要小看它,因为它非常实用且重要,它主要有两个使用场景:

  1. 可以使用 LayoutBuilder 来根据设备的尺寸来实现响应式布局。
  2. LayoutBuilder 可以帮我们高效排查问题。比如我们在遇到布局问题或者想调试组件树中某一个节点布局的约束时 LayoutBuilder 就很有用。

打印布局时的约束信息

为了便于排错,我们封装一个能打印父组件传递给子组件约束的组件:

class LayoutLogPrint<T> extends StatelessWidget {
    
    
  const LayoutLogPrint({
    
    
    Key? key,
    this.tag,
    required this.child,
  }) : super(key: key);

  final Widget child;
  final T? tag; //指定日志tag

  
  Widget build(BuildContext context) {
    
    
    return LayoutBuilder(builder: (_, constraints) {
    
    
      // assert在编译release版本时会被去除
      assert(() {
    
    
        print('${
      
      tag ?? key ?? child}: $constraints');
        return true;
      }());
      return child;
    });
  }
}

这样,我们就可以使用 LayoutLogPrint 组件树中任意位置的约束信息,比如:

LayoutLogPrint(child:Text("xx"))

控制台输出:

flutter: Text("xx"): BoxConstraints(0.0<=w<=428.0, 0.0<=h<=823.0)

可以看到 Text("xx") 的显示空间最大宽度为 428,最大高度为 823 。

注意!我们的大前提是盒模型布局,如果是Sliver 布局,可以使用 SliverLayoutBuiler 来打印。

运行效果:

insérez la description de l'image ici

Flutter 的 build 和 layout

通过观察 LayoutBuilder 的示例,我们还可以发现一个关于 Flutter 构建(build)和 布局(layout)的结论:Flutter 的 build 和 layout 是可以交错执行的,并不是严格的按照先 buildlayout 的顺序。比如在上例中 ,在build过程中遇到了 LayoutBuilder 组件,而 LayoutBuilderbuilder 是在 layout 阶段执行的(layout阶段才能取到布局过程的约束信息),在 builder 中新新建了一个 widget 后,Flutter 框架随后会调用该 widgetbuild 方法,又进入了 build 阶段。

AfterLayout

1. 获取组件大小和相对于屏幕的坐标

Flutter 是响应式UI框架,而命令式UI框架最大的不同就是:大多数情况下开发者只需要关注数据的变化,数据变化后框架会自动重新构建UI而不需要开发者手动去操作每一个组件,所以我们会发现 Widget 会被定义为不可变的(immutable),并且没有提供任何操作组件的 API,因此如果我们想在 Flutter 中获取某个组件的大小和位置就会很困难,当然大多数情况下不会有这个需求,但总有一些场景会需要,而在命令式UI框架中是不会存在这个问题的。

我们知道,只有当布局完成时,每个组件的大小和位置才能确定,所以获取的时机肯定是布局完成后,那布局完成的时机如何获取呢?至少事件分发肯定是在布局完成之后的,比如:

Builder(
  builder: (context) {
    
    
    return GestureDetector(
      child: Text('flutter'),
      onTap: () => print(context.size), //打印 text 的大小
    );
  },
),

context.size 可以获取当前上下文 RenderObject 的大小,对于BuilderStatelessWidget 以及 StatefulWidget 这样没有对应 RenderObject 的组件(这些组件只是用于组合和代理组件,本身并没有布局和绘制逻辑),获取的是子代中第一个拥有 RenderObject 组件的 RenderObject 对象。

虽然事件点击时可以拿到组件大小,但有两个问题,第一是需要用户手动触发,第二是时机较晚,更多的时候我们更希望在布局一结束就去获取大小和位置信息,为了解决这个问题,我们可以自己封装一个 AfterLayout 组件,它可以在子组件布局完成后执行一个回调,并同时将 RenderObject 对象作为参数传递。

以下是 AfterLayout 实现源码:

typedef AfterLayoutCallback = Function(RenderAfterLayout ral);

/// A widget can retrieve its render object after layout.
///
/// Sometimes we need to do something after the build phase is complete,
/// for example, most of [RenderObject] methods and attributes, such as
/// `renderObject.size`、`renderObject.localToGlobal(...)` only can be used
/// after build.
///
/// Call `setState` in callback is **allowed**, it is safe!
class AfterLayout extends SingleChildRenderObjectWidget {
    
    
  const AfterLayout({
    
    Key? key, required this.callback, Widget? child,}) : super(key: key, child: child);

  
  RenderObject createRenderObject(BuildContext context) {
    
    
    return RenderAfterLayout(callback);
  }

  
  void updateRenderObject(context, RenderAfterLayout renderObject) {
    
    
    renderObject.callback = callback;
  }

  /// 组件树布局结束后会被触发,注意,并不是当前组件布局结束后触发
  /// [callback] will be triggered after the layout phase ends.
  final AfterLayoutCallback callback;
}

class RenderAfterLayout extends RenderProxyBox {
    
    
  RenderAfterLayout(this.callback);

  ValueSetter<RenderAfterLayout> callback;

  
  void performLayout() {
    
    
    super.performLayout();
    // 不能直接回调callback,在 frame 结束的时候再去触发回调。 
    SchedulerBinding.instance.addPostFrameCallback((timeStamp) => callback(this));
  }

  /// 组件在在屏幕坐标中的起始偏移坐标
  Offset get offset => localToGlobal(Offset.zero);

  /// 组件在屏幕上占有的矩形空间区域
  Rect get rect => offset & size;
}

AfterLayout 可以在布局结束后拿到子组件的代理渲染对象 (RenderAfterLayout), RenderAfterLayout 对象会代理子组件渲染对象 ,因此,通过RenderAfterLayout 对象也就可以获取到子组件渲染对象上的属性,比如件大小、位置等。

AfterLayout 使用示例:

AfterLayout(
  callback: (RenderAfterLayout ral) {
    
    
    print(ral.size); //子组件的大小
    print(ral.offset); // 子组件在屏幕中坐标
  },
  child: Text('flutter'),
),

运行后控制台输出:

flutter: Size(105.0, 17.0)
flutter: Offset(42.5, 290.0)

可以看到 Text 文本的实际长度是 105,高度是 17,它的起始位置坐标是(42.5, 290.0)。

2. 获取组件相对于某个父组件的坐标

RenderAfterLayout 类继承自 RenderBoxRenderBox 有一个 localToGlobal 方法,它可以将坐标转化为相对与指定的祖先节点的坐标,比如下面代码可以打印出 Text('A') 在 父 Container 中的坐标:

Builder(builder: (context) {
    
    
  return Container(
    color: Colors.grey.shade200,
    alignment: Alignment.center,
    width: 100,
    height: 100,
    child: AfterLayout(
      callback: (RenderAfterLayout ral) {
    
    
        Offset offset = ral.localToGlobal(
          Offset.zero,
          // 传一个父元素 Container 对应的 RenderObject 对象
          ancestor: context.findRenderObject(),
        );
        print('A 在 Container 中占用的空间范围为:${
      
      offset & ral.size}');
      },
      child: Text('A'),
    ),
  );
}),

下面是一个 AfterLayout 的完整测试示例:

class AfterLayoutRoute extends StatefulWidget {
    
    
  const AfterLayoutRoute({
    
    Key? key}) : super(key: key);

  
  _AfterLayoutRouteState createState() => _AfterLayoutRouteState();
}

class _AfterLayoutRouteState extends State<AfterLayoutRoute> {
    
    
  String _text = 'flutter 实战 ';
  Size _size = Size.zero;

  
  Widget build(BuildContext context) {
    
    
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Builder(
            builder: (context) {
    
    
              return GestureDetector(
                child: Text(
                  'Text1: 点我获取我的大小',
                  textAlign: TextAlign.center,
                  style: TextStyle(color: Colors.blue),
                ),
                onTap: () => print('Text1: ${
      
      context.size}'),
              );
            },
          ),
        ),
        AfterLayout(
          callback: (RenderAfterLayout ral) {
    
    
            print('Text2: ${
      
      ral.size}, ${
      
      ral.offset}');
          },
          child: Text('Text2:flutter@wendux'),
        ),
        Builder(builder: (context) {
    
    
          return Container(
            color: Colors.grey.shade200,
            alignment: Alignment.center,
            width: 100,
            height: 100,
            child: AfterLayout(
              callback: (RenderAfterLayout ral) {
    
    
                Offset offset = ral.localToGlobal(
                  Offset.zero,
                  ancestor: context.findRenderObject(),
                );
                print('A 在 Container 中占用的空间范围为:${
      
      offset & ral.size}');
              },
              child: Text('A'),
            ),
          );
        }),
        Divider(),
        AfterLayout(
          child: Text(_text), 
          callback: (RenderAfterLayout value) {
    
    
            setState(() {
    
    
              //更新尺寸信息
              _size = value.size;
            });
          },
        ),
        //显示上面 Text 的尺寸
        Padding(
          padding: const EdgeInsets.symmetric(vertical: 8.0),
          child: Text(
            'Text size: $_size ',
            style: TextStyle(color: Colors.blue),
          ),
        ),
        ElevatedButton(
          onPressed: () {
    
    
            setState(() {
    
    
              _text += 'flutter 实战 ';
            });
          },
          child: Text('追加字符串'),
        ),
      ],
    );
  }
}

运行效果:

insérez la description de l'image ici

Après l'exécution, cliquez sur Text1 pour voir sa taille dans le panneau du journal. Cliquez sur le bouton "Append String", une fois la taille de la chaîne modifiée, la taille modifiée de la zone de texte sera également affichée à l'écran (à côté du bouton ci-dessus).


Référence : "Flutter Combat Deuxième édition"

Je suppose que tu aimes

Origine blog.csdn.net/lyabc123456/article/details/130843115
conseillé
Classement