O colapso: texto flutuante

Mar 26 2023
Explore o funcionamento interno do Flutter Text Widget
Por que esta série? Embora haja muito conteúdo sobre uso e personalizações para Flutter Widgets, há relativamente pouco conteúdo sobre como o Widget realmente alcança a funcionalidade para a qual foi projetado. Mesmo um Widget bastante simples tem muita simplificação sob o capô, mesmo excluindo seu objetivo principal.

Por que esta série?

Embora haja muito conteúdo sobre uso e personalizações para Flutter Widgets, há relativamente pouco conteúdo sobre como o Widget realmente alcança a funcionalidade para a qual foi projetado. Mesmo um Widget bastante simples tem muita simplificação sob o capô, mesmo excluindo seu objetivo principal. Esta série divide vários Widgets em seu nível fundamental, que geralmente desce para o nível da tela.

Compreendendo a função do texto

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

Mas aqui está o problema: o TextWidget não é responsável por renderizar o texto, ele é responsável por integrar aspectos do aplicativo Flutter ao texto exibido. Esta é a razão pela qual o TextWidget não espera que um TextStyleou outros parâmetros sejam fornecidos. Ele herda o estilo de texto padrão do BuildContexte constrói o texto de acordo. À medida que você se aprofunda em como o Widget pinta o texto na tela, você encontrará mais componentes cada vez mais puros - o que significa que eles não herdam nenhuma informação do contexto do Flutter e precisam que todas as informações sejam passadas para eles .

Aqui está o código simplificado para o Textmétodo de construção Widget. Como você pode ver, a maior parte do método incorpora estilos existentes, adiciona uma região do mouse e adiciona semântica. O restante do método delega a renderização a um 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;
  }

Sob o capô: RichText

O RichTextWidget é usado para exibir texto formatado com estilos variados em um único parágrafo ou frase. Ele permite que os desenvolvedores criem layouts de texto complexos com diferentes fontes, tamanhos, cores e estilos.

O RichTextWidget aceita um TextSpanobjeto para definir o texto e os estilos que serão exibidos na tela. A TextSpanclasse permite que os desenvolvedores definam várias opções de formatação de texto, como família de fontes, tamanho, peso, estilo, cor e cor de fundo. Ele também permite decorações de texto, como sublinhado e tachado. Além disso, TextSpantambém pode conter objetos filho TextSpanque permitem texto aninhado com diferentes estilos e formatação para cada extensão.

Aqui está um exemplo de como usar o RichTextWidget para exibir algum texto simples com estilos diferentes:

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

Aqui está uma versão simplificada do createRenderObject()método 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,
  );
}

Indo mais fundo: RenderParagraph

A RenderParagraphclasse é RenderObjectresponsável por renderizar um parágrafo de texto na tela. Ele lida com recursos de layout de texto, quebra de linha e seleção de texto. Ele é usado como o componente de renderização por trás do Texte TextFieldWidget no Flutter. RenderParagraphfornece recursos de seleção de texto, permitindo que o usuário selecione um intervalo de texto com um mouse ou gesto de toque.

RenderParagraphestá focado na renderização de texto, o que significa que não herda nenhuma informação do Flutter e precisa de todos os parâmetros definidos - por usuários ou como padrão. Como é um RenderObject, ele lida com o layout do texto. No entanto, enquanto o normal RenderObjectslida apenas com o tamanho da caixa, o RenderParagraphtambém tem que lidar com o layout interno do texto a ser exibido - levando em consideração a altura da linha, a direção do texto, a quebra de linha e muito mais.

Existem vários métodos que lidam com esse procedimento de layout. A seguir estão alguns métodos que lidam com o dimensionamento 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) {...}

Aqui está uma versão simplificada do método:

@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
  }
}

No entanto, isso ainda não responde o que realmente pinta o texto na tela.

Até agora, nós:

1) Reuniu tudo o que é necessário para o texto: estilos, direção, semântica, seleções, etc.

2) Espaços de texto criados com base nos dados.

3) Efetuei a diagramação do texto e acrescentei considerações para estouro.

Como os aplicativos do Flutter são apenas pinturas gigantes em uma tela, a parte final é a TextPainterclasse que pinta o texto para nós.

Base: Canvas e TextPainter

Para pintar o texto na tela, existem alguns passos a seguir.

Primeiro, passamos a árvore TextSpanscriada pelos Widgets antes para o TextPainter. O RenderParagraphtambém cria uma instância TextPainterdentro dele.

Em seguida, chamamos o layout()método dentro da TextPainterclasse que dimensiona a altura e a largura do texto e também limpa qualquer cache de cursor (já que a classe também é usada para o TextFieldWidget).

Aqui está o código simplificado para o layout()método:

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

Conclusão

É muito fácil dizer “ É apenas um widget de texto ”, mas há uma incrível personalização e complexidade escondidas até mesmo no mais simples dos widgets. A renderização de texto, em particular, sempre foi uma coisa complexa de lidar e espero que este artigo ajude você a apreciar a complexidade que está escondida de você para permitir que você crie suas melhores ideias com o mínimo de esforço.

É isso para este artigo! Espero que você tenha gostado e certifique-se de me seguir para mais artigos do Flutter e comentar para qualquer feedback que você possa ter sobre este artigo.

Siga meu blog para mais: