La répartition : texte flottant

Mar 26 2023
Explorez le fonctionnement interne du widget Flutter Text
Pourquoi cette série ? Bien qu'il y ait beaucoup de contenu sur l'utilisation et les personnalisations des widgets Flutter, il y a relativement peu de contenu sur la façon dont le widget atteint réellement la fonctionnalité pour laquelle il est conçu. Même un widget assez simple a beaucoup de simplification sous le capot, même en excluant son objectif principal.

Pourquoi cette série ?

Bien qu'il y ait beaucoup de contenu sur l'utilisation et les personnalisations des widgets Flutter, il y a relativement peu de contenu sur la façon dont le widget atteint réellement la fonctionnalité pour laquelle il est conçu. Même un widget assez simple a beaucoup de simplification sous le capot, même en excluant son objectif principal. Cette série décompose divers Widgets à leur niveau fondamental qui descend généralement au niveau de la toile.

Comprendre la fonction du texte

Text(
  "Hello World!",
  style: TextStyle(
    fontSize: 24.0,
  ),
)

Mais voici le hic : le TextWidget n'est pas responsable du rendu du texte, il est responsable de l'intégration des aspects de l'application Flutter dans le texte affiché. C'est la raison pour laquelle le TextWidget ne s'attend pas à ce qu'un TextStyleou d'autres paramètres soient fournis. Il hérite du style de texte par défaut de BuildContextet construit le texte en conséquence. Au fur et à mesure que vous approfondissez la façon dont le Widget peint le texte à l'écran, vous trouverez de plus en plus de composants qui sont de plus en plus purs - ce qui signifie qu'ils n'héritent d'aucune information du contexte Flutter et ont plutôt besoin que toutes les informations leur soient transmises .

Voici le code simplifié pour la Textméthode de construction Widget. Comme vous pouvez le voir, la plupart de la méthode incorpore des styles existants, ajoute une zone de souris et ajoute de la sémantique. Le reste de la méthode délègue le rendu à un RichTextWidget.

// The build() method of the Text Widget
  Widget build(BuildContext context) {

    // Set the effective text style
    final defaultTextStyle = DefaultTextStyle.of(context);
    TextStyle? effectiveTextStyle = style;
    if (style == null || style!.inherit) {
      effectiveTextStyle = defaultTextStyle.style.merge(style);
    }
    if (MediaQuery.boldTextOverride(context)) {
      effectiveTextStyle = effectiveTextStyle!.merge(const TextStyle(fontWeight: FontWeight.bold));
    }
    final registrar = SelectionContainer.maybeOf(context);

    // Parameters passed down to RichText
    Widget result = RichText(...);

    // Create a mouse region
    if (registrar != null) {
      result = MouseRegion(...);
    }

    // Set any semantics for the text
    if (semanticsLabel != null) {
      result = Semantics(...);
    }
    return result;
  }

Sous le capot : RichText

Le RichTextwidget est utilisé pour afficher du texte formaté avec des styles variés dans un seul paragraphe ou une seule phrase. Il permet aux développeurs de créer des mises en page de texte complexes avec différentes polices, tailles, couleurs et styles.

Le RichTextWidget accepte un TextSpanobjet pour définir le texte et les styles qui sont affichés à l'écran. La TextSpanclasse permet aux développeurs de définir diverses options de formatage du texte telles que la famille de polices, la taille, l'épaisseur, le style, la couleur et la couleur d'arrière-plan. Il permet également des décorations de texte telles que le soulignement et le barré. En outre, TextSpanpeut également contenir des objets enfants TextSpanqui autorisent le texte imbriqué avec différents styles et mises en forme pour chaque étendue.

Voici un exemple d'utilisation du RichTextWidget pour afficher du texte simple avec différents styles :

RichText(
  text: TextSpan(
    text: 'Hello ',
    style: TextStyle(
      fontSize: 20.0,
      fontWeight: FontWeight.bold,
      color: Colors.black,
    ),
    children: <TextSpan>[
      TextSpan(
        text: 'World',
        style: TextStyle(
          fontSize: 24.0,
          fontWeight: FontWeight.normal,
          color: Colors.blue,
        ),
      ),
      TextSpan(
        text: '!',
        style: TextStyle(
          fontSize: 20.0,
          fontWeight: FontWeight.bold,
          color: Colors.black,
        ),
      ),
    ],
  ),
),

Voici une version simplifiée de la createRenderObject()méthode de RichText:

@override
RenderParagraph createRenderObject(BuildContext context) {
  return RenderParagraph(text,
    textAlign: textAlign,
    // Defaults to inherited directionality
    textDirection: textDirection ?? Directionality.of(context),
    softWrap: softWrap,
    overflow: overflow,
    textScaleFactor: textScaleFactor,
    maxLines: maxLines,
    strutStyle: strutStyle,
    textWidthBasis: textWidthBasis,
    textHeightBehavior: textHeightBehavior,
    // Defaults to inherited locale
    locale: locale ?? Localizations.maybeLocaleOf(context),
    registrar: selectionRegistrar,
    selectionColor: selectionColor,
  );
}

Plonger plus profondément : RenderParagraph

La RenderParagraphclasse est RenderObjectresponsable du rendu d'un paragraphe de texte à l'écran. Il gère la mise en page du texte, les sauts de ligne et les fonctionnalités de sélection de texte. Il est utilisé comme composant de rendu derrière Textet TextFieldWidget dans Flutter. RenderParagraphfournit des fonctionnalités de sélection de texte, permettant à l'utilisateur de sélectionner une plage de texte avec une souris ou un geste tactile.

RenderParagraphse concentre sur le rendu du texte, ce qui signifie qu'il n'hérite d'aucune information Flutter et nécessite que tous les paramètres soient définis - soit par les utilisateurs, soit par défaut. Puisqu'il s'agit d'un RenderObject, il traite de la mise en page du texte. Cependant, alors que d'habitude RenderObjectsne traiterait que de la taille de la boîte, le RenderParagraphdoit également gérer la disposition interne du texte à afficher - en tenant compte de la hauteur de ligne, de la direction du texte, de l'habillage, etc.

Il existe différentes méthodes qui traitent de cette procédure de mise en page. Voici quelques méthodes qui traitent du dimensionnement de RenderBox:

@override
double computeMinIntrinsicWidth(double height) {...}

@override
double computeMaxIntrinsicWidth(double height) {...}

@override
double computeMinIntrinsicHeight(double width) {...}

@override
double computeMaxIntrinsicHeight(double width) {...}

@override
double computeDistanceToActualBaseline(TextBaseline baseline) {...}

void _computeChildrenWidthWithMaxIntrinsics(double height) {...}

void _computeChildrenWidthWithMinIntrinsics(double height) {...}

void _computeChildrenHeightWithMinIntrinsics(double width) {...}

Voici une version simplifiée de la méthode :

@override
void performLayout() {
  // Calculate size of placeholders
  final BoxConstraints constraints = this.constraints;
  _placeholderDimensions = _layoutChildren(constraints);
  _layoutTextWithConstraints(constraints);
  _setParentData();

  final Size textSize = _textPainter.size;
  final bool textDidExceedMaxLines = _textPainter.didExceedMaxLines;
  size = constraints.constrain(textSize);

  // Check if text overflows any boundary
  final bool didOverflowHeight = size.height < textSize.height || textDidExceedMaxLines;
  final bool didOverflowWidth = size.width < textSize.width;
  final bool hasVisualOverflow = didOverflowWidth || didOverflowHeight;

  if (hasVisualOverflow) {
    switch (_overflow) {
      case TextOverflow.visible:
        _needsClipping = false;
        _overflowShader = null;
        break;
      case TextOverflow.clip:
      case TextOverflow.ellipsis:
        _needsClipping = true;
        _overflowShader = null;
        break;
      case TextOverflow.fade:
        _needsClipping = true;
        final fadeSizePainter = TextPainter(...)..layout();
        if (didOverflowWidth) {
          double fadeEnd, fadeStart;
          switch (textDirection) {
            ... // Set the direction of fade
          }
          _overflowShader = ui.Gradient.linear(...);
        } else {
          final double fadeEnd = size.height;
          final double fadeStart = fadeEnd - fadeSizePainter.height / 2.0;
          _overflowShader = ui.Gradient.linear(...);
        }
        break;
    }
  } else {
    ... // No overflow
  }
}

Cependant, cela ne répond toujours pas à ce qui peint réellement le texte à l'écran.

Jusqu'à présent, nous avons :

1) Rassemblez tout ce qui est nécessaire pour le texte : styles, direction, sémantique, sélections, etc.

2) Création d'étendues de texte basées sur les données.

3) Mise en page effectuée pour le texte et ajout de considérations pour le débordement.

Étant donné que les applications Flutter ne sont que des peintures géantes sur une toile, le dernier élément est la TextPainterclasse qui peint le texte pour nous.

Socle rocheux : Canvas et TextPainter

Pour peindre le texte à l'écran, il y a quelques étapes à suivre.

Tout d'abord, nous passons l'arbre des TextSpanscréé par les Widgets avant au TextPainter. Le RenderParagraphcrée également une instance de TextPainterà l'intérieur.

Ensuite, nous appelons la layout()méthode à l'intérieur de la TextPainterclasse qui dimensionne la hauteur et la largeur du texte, et efface également tout cache caret (puisque la classe est également utilisée pour le TextFieldWidget).

Voici le code simplifié de la layout()méthode :

void layout({ double minWidth = 0.0, double maxWidth = double.infinity }) {

  if (_paragraph != null && minWidth == _lastMinWidth && maxWidth == _lastMaxWidth) {
    return;
  }

  if (_rebuildParagraphForPaint || _paragraph == null) {
    _createParagraph();
  }

  _lastMinWidth = minWidth;
  _lastMaxWidth = maxWidth;

  // A change in layout invalidates the cached caret and line metrics as well.
  _lineMetricsCache = null;
  _previousCaretPosition = null;
  _previousCaretPrototype = null;
  _layoutParagraph(minWidth, maxWidth);
  _inlinePlaceholderBoxes = _paragraph!.getBoxesForPlaceholders();

}

void paint(Canvas canvas, Offset offset) {
  final double? minWidth = _lastMinWidth;
  final double? maxWidth = _lastMaxWidth;

  if (_rebuildParagraphForPaint) {
    _createParagraph();
    _layoutParagraph(minWidth, maxWidth);
  }
  canvas.drawParagraph(_paragraph!, offset);
}

Conclusion

Il est très facile de dire " Ce n'est qu'un widget de texte ", mais il y a une personnalisation et une complexité incroyables cachées derrière même le plus simple des widgets. Le rendu de texte en particulier a toujours été une chose complexe à gérer et j'espère que cet article vous aidera à apprécier la complexité qui vous est cachée pour vous permettre de créer vos meilleures idées avec le moindre effort.

Voilà pour cet article ! J'espère que vous l'avez apprécié, et assurez-vous de me suivre pour plus d'articles Flutter et commentez pour tout commentaire que vous pourriez avoir sur cet article.

Suivez mon blog pour plus: