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 SingleChildRenderObjectWidget
et MultiChildRenderObjectWidget
Widgets. Ils ont généralement une propriété child
ou children
pour 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
.
RenderObjectWidget
Les méthodes de création et de mise à jour sont définies dans la classe RenderObject
, et les sous-classes doivent les implémenter. RenderObject
Nous 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 RenderObject
ré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 RenderObject
implémentation correspondante, par exemple Stack
(mise en page en cascade) l'objet correspondant RenderObject
est 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
, AspectRatio
etc.
Modèle de disposition de boîte
Il existe deux modèles de mise en page dans Flutter :
- Disposition basée sur
RenderBox
le modèle de boîte. - 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 :
- Les composants de niveau supérieur transmettent des conditions de contraintes (contraintes) aux composants de niveau inférieur.
- 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.
- 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 :
- Les objets de rendu correspondant aux composants héritent tous de la
RenderBox
classe. - Les informations de contrainte transmises du parent à l'enfant lors de la disposition sont
BoxConstraints
décrites par .
BoxConstraints
BoxConstraints
Il 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 BoxConstraints
constructeur 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 BoxConstraints
et définit également des constructeurs pratiques pour générer rapidement des règles de restriction spécifiques, BoxConstraints
par 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
ConstrainedBox
Utilisé 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:
On peut voir que bien que nous Container
définissions la hauteur de _ en 5
pixels, cela se termine en 50
pixels, c'est pourquoi ConstrainedBox
la restriction de hauteur minimale de _ est en vigueur. Si vous Container
définissez la hauteur en 80
pixels, la hauteur finale de la zone rouge sera également 80
en pixels, car dans cet exemple, seule la hauteur minimaleConstrainedBox
est restreinte , pas la hauteur maximale .
SizeBox
SizedBox
Utilisé 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:
En fait, SizedBox
c'est juste ConstrainedBox
une 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 SizedBox
sont RenderConstrainedBox
rendus à travers, nous pouvons voir ConstrainedBox
que les méthodes SizedBox
de et createRenderObject()
renvoient un RenderConstrainedBox
objet :
RenderConstrainedBox createRenderObject(BuildContext context) {
return RenderConstrainedBox(
additionalConstraints: ...,
);
}
restrictions multiples
Si un composant a plusieurs ConstrainedBox
restrictions 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 :
l'effet d'affichage final est de 90 en largeur et de 60 en hauteur, c'est-à-dire que l'enfant ConstrainedBox
prend minWidth
effet, minHeight
mais le parent ConstrainedBox
prend 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 :
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 minWidth
c'est le parent qui prend effet à ce moment ConstrainedBox
, minHeight
mais l'enfant ConstrainedBox
.
Grâce à l'exemple ci-dessus, nous avons constaté que lorsqu'il existe plusieurs restrictions , pour minWidth
et 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 A
contraintes B
doivent B
être C
respectées C
, B
et B
doivent être A
respectées Contraintes, mais A
les contraintes de ne seront pas directement contraintes C
à moins qu'elles B
ne soient transmisesA
à ses propres contraintes C
. En utilisant ce principe, vous pouvez implémenter un B
composant comme celui-ci :
B
C
Il n'y a pas de contraintes dans la disposition du composantC
(il peut être infini).C
Déterminer sa propre taille en fonction de son occupation réelle de l'espace.B
A
Déterminer sa propre taille en combinaison avec la taille des sous-composants sous réserve de respecter les contraintes.
Et ce B
composant est UnconstrainedBox
le composant, ce qui signifie que UnconstrainedBox
les 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×100
boîte rouge finira par s'afficher. Mais comme la restriction du parent est UnconstrainedBox
« supprimée » ConstrainedBox
, elle sera éventuellement ConstrainedBox
dessinée en fonction de la restriction de l'enfant redBox
, c'est-à-dire 90×20
, comme le montre la figure :
Cependant, veuillez noter que UnconstrainedBox
la "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
, 80
il y a toujours un . C'est-à-dire que la restriction du parent minHeight(100.0)
est toujours en vigueur, mais elle n'affecte pas redBox
la taille de l'élément enfant final, mais il occupe toujours l'espace correspondant. On peut considérer que le parent à ce moment ConstrainedBox
agit sur l'enfant UnconstrainedBox
et redBox
n'est ConstrainedBox
limité que par l'enfant Ce point Veuillez faire attention.
Existe-t-il un moyen de supprimer complètement la ConstrainedBox
restriction 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é SizedBox
ou ConstrainedBox
spé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, AppBar
dans le menu de droite de la (barre de navigation) dans la bibliothèque de composants Material, nous utilisons SizedBox
la 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 :
Nous constaterons que la taille du bouton de chargement à droite n'a pas changé ! C'est justement parce que les contraintes du bouton AppBar
ont é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 :
Vous pouvez voir que cela fonctionne! En fait, il est également possible de UnconstrainedBox
remplacer Center
par ou .Align
De plus, il convient de noter que UnconstrainedBox
bien que les contraintes puissent être annulées lors de la mise en page de ses sous-composants (les sous-composants peuvent être infinis), mais UnconstrainedBox
lui-même est contraint par son composant parent, alors lorsque UnconstrainedBox
ses sous-composants deviennent plus grands, si UnconstrainedBox
la 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 :
Le texte a dépassé la largeur de l'écran, débordant.
Ratio d'aspect
AspectRatio
La fonction est d' ajuster le rapport d'aspect de l'élément enfant child .
AspectRatio
Tout 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 , BoxFit
et contain
essaie 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, AspectRatio
il 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,
),
);
}
}
LimitedBox
LimitedBox
Il est utilisé pour spécifier child
la largeur et la hauteur maximales. Il peut être child
limité à la largeur et à la hauteur maximales définies par celui-ci, mais cette limitation est conditionnelle. Lorsqu'il LimitedBox
n'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
, Column
etc. Si LimitedBox
le composant parent est contraint, LimitedBox
rien 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)),
),
);
}
Text
Les composants ne sont pas contraints 100x50
dans la région comme nous le souhaiterions, car les composants leur Center
imposent des contraintes, nous pouvons donc utiliser les contraintes parent supprimées :LimitedBox
UnconstrainedBox
LimitedBox
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 :
Si nous ListView
ajoutons 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, 0
enveloppez simplement Container
le dans LimitedBox
un :
ListView(
children: <Widget>[
LimitedBox(
maxHeight: 100,
child: Container(color: Colors.green),
),
LimitedBox(
maxHeight: 100,
child: Container(color: Colors.red),
),
],
)
Effet:
FractionallySizedBox
FractionallySizedBox
La 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:
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 Row
et , similaires aux contrôles Column
dans Android . et les deux héritent de .LinearLayout
Row
Column
Flex
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.
MainAxisAlignment
Dans 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 widgets l'espace horizontal réellement occupé par l'enfant, Row la largeur du sous-marin est toujours égale à la largeur maximale dans le sens horizontal ; alors que cela MainAxisSize.min signifie aussi peu que possible L'espace horizontal occupé par , lorsque le sous-composant n'occupe pas l'espace horizontal restant, la Row largeur 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 mainAxisSize la valeur est MainAxisSize.min , cette propriété n'a pas de sens, car la largeur des sous-composants est égale à Row la largeur de . Cette propriété n'a de sens que lorsque mainAxisSize la 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 à ( MainAxisAlignment comprenant start、end、 center trois valeurs). La différence est que crossAxisAlignment le système de référence est verticalDirection que verticalDirection la valeur fait référence VerticalDirection.down à lorsque la valeur est l'alignement en bascrossAxisAlignment.start verticalDirection VerticalDirection.up crossAxisAlignment.start ; |
children |
tableau de composants enfants |
Les valeurs de mainAxisAlignment
sont les suivantes :
AlignementAxeMain | illustrer |
---|---|
MainAxisAlignment.start |
Indique textDirection l'alignement le long de la direction initiale. Si textDirection la valeur TextDirection.ltr est définie sur , cela MainAxisAlignment.start signifie aligné à gauche, et lorsque textDirection la valeur est TextDirection.rt l, cela signifie aligné à droite |
MainAxisAlignment.end |
et MainAxisAlignment.start exactement 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 : textDirection
Oui est mainAxisAlignment
le 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:
expliquer:
- Le premier
Row
est 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 largeursmainAxisSize
, l'alignement n'a pas de sens, il sera donc affiché de gauche à droite ;MainAxisSize.min
Row
Text
- Le troisième
Row
définittextDirection
la valeurTextDirection.rtl
, de sorte que les sous-composants seront disposés dans l'ordre de droite à gauche, et à ce momentMainAxisAlignment.end
cela 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
Row
test est l'alignement de l'axe vertical. Étant donné que les deux sousText
-polices sont différentes, leurs hauteurs sont également différentes. Nous spécifionsverticalDirection
une valeurVerticalDirection.up
, c'est-à-dire organiser de bas en haut, etcrossAxisAlignment
la valeur à ce momentCrossAxisAlignment.start
signifie l'alignement en bas.
Colonne
Column
Ses sous-composants peuvent être disposés verticalement. Les paramètres sont Row
les 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:
expliquer:
- Comme nous n'en avons pas spécifié
Column
,mainAxisSize
l'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é
crossAxisAlignment
l'attribut commeCrossAxisAlignment.center
, les éléments enfantsColumn
seront 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 laColumn
largeur réelle de l'espace occupé et la largeur réelle dépend de la plus grande largeur parmi les enfantsWidget
. Dans cet exemple,Column
il y a deux enfantsWidget
et le "monde" d'affichageText
a la plus grande largeur, doncColumn
la largeur réelle de est la largeur de , il sera donc affiché au milieu du centreText("world")
après l'alignement .Text("hi")
Text("world")
En fait, Row
les Column
deux 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 :
-
Spécifiez
Column
la largeur de comme largeur d'écran ; c'est simple, nous pouvons forcer la modification de la limite de largeur parConstrainedBox
ou .SizedBox
Par exemple : réglez sur
ConstrainedBox
, pour que la largeur occupe le plus d'espace possible.minWidth
double.infinity
ConstrainedBox(
constraints: BoxConstraints(minWidth: double.infinity),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text("hi"),
Text("world"),
],
),
);
Center
Les composants peuvent être utilisés
Nidification
S'il Row
est imbriqué à l'intérieur Row
, ou Column
à nouveau imbriqué à l'intérieur Column
, seul le plus à l'extérieur Row
occupera Column
autant d'espace que possible, et l'espace occupé par l'intérieur Row
ou l'intérieur est la taille réelle. Voici un exemple :Column
Column
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 :
Si vous voulez que l'intérieur Column
remplisse l'extérieur Column
, vous pouvez utiliser Expanded
le composant :
Expanded(
child: Container(
color: Colors.red,
child: Column(
mainAxisAlignment: MainAxisAlignment.center, //垂直方向居中对齐
children: <Widget>[
Text("hello world "),
Text("I am Jack "),
],
),
),
)
résultat courant :
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 Flex
et Expanded
.
Fléchir
Flex
Les 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 Row
ou Column
ce sera plus pratique, car ils héritent tous de Flex
, et les paramètres sont fondamentalement les mêmes, donc Flex
fondamentalement vous peut utiliser Row
ou Column
.
Flex
La fonction elle-même est très puissante et peut également Expanded
coopérer avec des composants pour obtenir une disposition flexible. Ensuite, nous n'aborderons que Flex
les propriétés liées à la mise en page flexible (d'autres propriétés ont été introduites dans l'introduction Row
et ).Column
Flex({
...
required this.direction, //弹性布局的方向, Row默认为水平方向,Column默认为垂直方向
List<Widget> children = const <Widget>[],
})
Flex
Hérité de MultiChildRenderObjectWidget
, correspondant RenderObject
à RenderFlex
, RenderFlex
implémente son algorithme de disposition dans.
Flexible
Flexible
Il ne peut être utilisé qu'en tant Row、Column、Flex
qu'enfant de (sinon une erreur sera signalée, car tous deux héritent de Row
et , 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é.Column
Flex
Flexible
Flexible
Row、Column、Flex
Par exemple, Row
il 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 :
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 :
Rapport de sous-contrôle = flex de sous-contrôle actuel / somme de tous les flex de sous-contrôle.
Flexible
Le fit
paramè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:
Ce code est basé sur le code du haut pour Container
ajouter Text
des sous-contrôles au rouge au milieu.
À ce stade, le rouge Container
ne remplit plus l'espace, puis Container
ajoute 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:
Cela remplit à nouveau l'espace restant. Container
La 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 Flexible
le contrôle neutronique de à , le code est le suivant :Container
OutlineButton
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, ),
],
)
OutlineButton
Dans 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 :
Étendu
Expanded
est Flexible
un 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、Flex
qu'enfant de , il peut "étendre" Flex
l'espace occupé par les composants enfants en proportion. Notez que le paramètre Expanded
de fit
est fixe FlexFit.tight
, ce qui signifie Expanded
qu'il faut (forcer) pour remplir l'espace restant.
flex
: Coefficient d'élasticité, s'il est0
ounull
,child
il est inélastique, c'est-à-dire l'espace qui ne sera pas occupé par la dilatation. Si plus grand0
, tous les espaces libres de l'axe principal sont divisésExpanded
selon sa proportion.flex
Ce qui précède OutlineButton
veut 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:
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 :
Spacer
La fonction dans l'exemple est d'occuper une proportion d'espace spécifiée. En fait, il s'agit simplement Expanded
d'une classe wrapper. Spacer
Le 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.
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- Flex
nœud et le titre doit être défini comme Flex
un 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, ConstrainedBox
la largeur maximale de la partie étiquette est contrainte, puis Flexible
le titre peut remplir entièrement l'espace restant à l'aide de composants.
Mise en page de flux (Wrap, Flow)
Lors de l'introduction de Row
et Colum
, si l'enfant widget
dépasse la plage d'écran, une erreur de dépassement sera signalée, telle que :
Row(
children: <Widget>[
Text("xxx"*100)
],
);
Comme vous pouvez le voir, la partie de débordement à droite signale une erreur. C'est parce qu'il Row
n'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, Wrap
et sont Flow
utilisés pour prendre en charge la mise en page du flux. Si l'exemple ci-dessus est Row
remplacé Wrap
par la partie de débordement, la ligne sera automatiquement pliée. Ensuite, nous introduirons Wrap
et respectivement Flow
.
Envelopper
Wrap
Row\Column
Semblable à la plupart des attributs de et sont à la fois à une seule ligne Row
et Column
à une seule colonne, Wrap
ce qui dépasse cette limitation. Lorsque mainAxis
l'espace supérieur est insuffisant, crossAxis
l'affichage sera agrandi vers le haut.
Voici Wrap
quelques 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 Flex attributs 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:
Couler
Nous l'utilisons généralement rarement Flow
, car il est trop compliqué et nécessite de réaliser widget
la conversion de position du sous-marin par lui-même. Dans de nombreux scénarios, la première chose à considérer est Wrap
de savoir s'il répond aux exigences. Flow
Il 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). Flow
Il a les avantages suivants :
- Bonnes performances ;
Flow
c'est un contrôle très efficace pour ajuster la taille et la position des sous-composants. IlFlow
utilise la matrice de transformation pour optimiser l'ajustement de la position des sous-composants : aprèsFlow
le positionnement, si la taille ou la position des sous-composants change, dansFlowDelegate
le LapaintChildren()
méthode appellecontext.paintChild
à redessiner , etcontext.paintChild
la 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.
Flow
Il 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émentantTestFlowDelegate
.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 :
可以看到我们主要的任务就是实现paintChildren
,它的主要任务是确定每个子widget
位置。由于Flow
不能自适应子widget
的大小,我们通过在getSize
返回一个固定大小来指定Flow
的大小。
注意,如果我们需要自定义布局策略,一般首选的方式是通过直接继承RenderObject
,然后通过重写 performLayout
的方式实现。
层叠布局(Stack、Positioned)
层叠布局和 Web 中的绝对定位、Android 中的 FrameLayout
相似,子组件可以根据距父容器四个角的位置来确定自身的位置。子组件是按照代码中声明的顺序堆叠起来。Flutter中使用Stack结合Positioned、Align这两个组件来配合实现定位。Stack
允许子组件堆叠,而Positioned
用于根据Stack
的四个角来确定子组件的位置。
Stack
常用属性:
属性 | 说明 |
---|---|
alignment |
此参数决定如何去对齐没有定位(没有使用Positioned )或部分定位的子组件。所谓部分定位,在这里特指没有在某一个轴上定位: left 、right 为横轴,top 、bottom 为纵轴,只要包含某个轴上的一个定位属性就算在该轴上有定位。默认值是AlignmentDirectional.topStart |
textDirection |
和Row 、Wrap 的textDirection 功能一样,都用于确定alignment 对齐的参考系,即: textDirection 的值为TextDirection.ltr ,则alignment 的start 代表左,end 代表右,即从左往右的顺序;textDirection 的值为TextDirection.rtl ,则alignment 的start 代表右,end 代表左,即从右往左的顺序 |
fit |
此参数用于确定没有定位的子组件如何去适应Stack 的大小。StackFit.loose 表示使用子组件的大小,StackFit.expand 表示扩伸到Stack 的大小,默认是StackFit.loose |
clipBehavior |
此属性决定对超出Stack 显示空间的部分如何剪裁,Clip 枚举类中定义了剪裁的方式,默认是Clip.hardEdge 表示直接剪裁,不应用抗锯齿 |
Positioned
常用属性:
属性 | 说明 |
---|---|
left |
子元素距离左侧距离 |
top |
子元素距离顶部的距离 |
right |
子元素距离右侧距离 |
bottom |
子元素距离底部的距离 |
child |
子组件 |
width |
**子组件的高度 ** |
height |
子组件的高度 |
注意,Positioned
的width
、height
和其他地方的意义稍微有点区别,此处用于配合left、top 、right、 bottom
来定位组件,举个例子,在水平方向时,你只能指定left、right、width
三个属性中的两个,如指定left
和width
后,right
会自动算出(left+width
),如果同时指定三个属性则会报错,垂直方向同理。另外,宽度和高度必须是固定值,没法使用double.infinity
。
示例:下面代码通过对几个Text
组件的定位来演示Stack
和Positioned
的特性
//通过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"),
)
],
),
);
效果:
- 由于第一个子文本组件
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"),
)
],
),
效果:
可以看到,由于第二个子文本组件没有定位,所以fit
属性会对它起作用,就会占满Stack
。由于Stack
子元素是堆叠的,所以第一个子文本组件被第二个遮住了,而第三个在最上层,所以可以正常显示。
Stack
和 Positioned
实现固定导航案例
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),
),
))
],
);
}
}
效果:
上面代码中MediaQuery
可以到获取屏幕宽度和高度,可以在build
方法中调用:
Widget build(BuildContext context) {
final size =MediaQuery.of(context).size;
final width =size.width;
final height =size.height;
...
}
对齐与相对定位(Align)
通过Stack
和Positioned
,我们可以指定多个子元素相对于父元素各个边的精确偏移,并且可以重叠。但如果我们只想简单的调整一个子元素在父元素中的位置的话,使用 Align
组件会更简单一些。
属性 | 说明 |
---|---|
alignment |
需要一个AlignmentGeometry 类型的值,表示子组件在父组件中的起始位置。AlignmentGeometry 是一个抽象类,它有两个常用的子类:Alignment 和 FractionalOffset |
widthFactor 和heightFactor |
用于确定 Align 组件本身宽高的属性;它们是两个缩放因子,会分别乘以子元素的宽、高,最终的结果就是 Align 组件的宽高。如果值为null ,则组件的宽高将会占用尽可能多的空间。 |
简单示例:
Container(
height: 120.0,
width: 120.0,
color: Colors.blue.shade50,
child: Align(
alignment: Alignment.topRight,
child: FlutterLogo(size: 60),
),
)
效果:
FlutterLogo
是 Flutter SDK 提供的一个组件,内容就是 Flutter 的 logo 。在上面的例子中,我们显式指定了Container
的宽、高都为 120
。如果我们不显式指定宽高,而通过同时指定widthFactor
和heightFactor
为 2
也是可以达到同样的效果:
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
的值从-1
到1
分别代表矩形左边到右边的距离和顶部到底边的距离,因此2
个水平(或垂直)单位则等于矩形的宽(或高),如Alignment(-1.0, -1.0)
代表矩形的左侧顶点,而Alignment(1.0, 1.0)
代表右侧底部终点,而Alignment(1.0, -1.0)
则正是右侧顶点,即Alignment.topRight
。为了使用方便,矩形的原点、四个顶点,以及四条边的终点在Alignment
类中都已经定义为了静态常量。
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)
。实际运行如图所示:
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),
),
)
效果:
我们将FractionalOffset(0.2, 0.6)
带入坐标转换公式得FlutterLogo实际偏移为(12,36)
,和实际运行效果吻合。
建议在需要制定一些精确的偏移时应优先使用FractionalOffset
,因为它的坐标原点和布局系统相同,能更容易算出实际偏移。
Align和Stack对比
可以看到,Align
和Stack/Positioned
都可以用于指定子元素相对于父元素的偏移,但它们还是有两个主要区别:
- 定位参考系统不同:
Stack/Positioned
定位的的参考系可以是父容器矩形的四个顶点;而Align
则需要先通过alignment
参数来确定坐标原点,不同的alignment
会对应不同原点,最终的偏移是需要通过alignment
的转换公式来计算出。 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
。
上面我们讲过当widthFactor
或heightFactor
为null
时组件的宽高将会占用尽可能多的空间,这一点需要特别注意,我们通过一个示例说明:
...//省略无关代码
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"),
),
)
效果:
熟悉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"),
),
],
),
),
],
);
}
}
用 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"),
)
],
),
)
],
);
}
}
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
的使用很简单,但是不要小看它,因为它非常实用且重要,它主要有两个使用场景:
- 可以使用
LayoutBuilder
来根据设备的尺寸来实现响应式布局。 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
来打印。
运行效果:
Flutter 的 build 和 layout
通过观察 LayoutBuilder
的示例,我们还可以发现一个关于 Flutter 构建(build)和 布局(layout)的结论:Flutter 的 build 和 layout 是可以交错执行的,并不是严格的按照先 build 再 layout 的顺序。比如在上例中 ,在build过程中遇到了 LayoutBuilder
组件,而 LayoutBuilder
的 builder
是在 layout 阶段执行的(layout阶段才能取到布局过程的约束信息),在 builder
中新新建了一个 widget
后,Flutter 框架随后会调用该 widget
的 build
方法,又进入了 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
的大小,对于Builder
、StatelessWidget
以及 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
类继承自 RenderBox
,RenderBox
有一个 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('追加字符串'),
),
],
);
}
}
运行效果:
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"