Saltar al contenido principal

Comportamiento del recorte

Flutter unifica clipBehavior y, por defecto, no realiza recortes en la mayoría de los casos.

Resumen

#

Flutter ahora por defecto no realiza recortes, excepto por unos pocos widgets especializados (como ClipRect). Para invalidar este comportamiento predeterminado de no recortar, establece explícitamente clipBehavior en la construcción de los widgets.

Contexto

#

Flutter solía ser lento debido a los recortes. Por ejemplo, el benchmark de la aplicación Flutter gallery tenía un tiempo promedio de rasterización de fotogramas de aproximadamente 35 ms en mayo de 2018, cuando el presupuesto para un renderizado fluido a 60 fps es de 16 ms. Al eliminar recortes innecesarios y sus operaciones relacionadas, vimos una aceleración de casi el doble, pasando de 35 ms/fotograma a 17.5 ms/fotograma.

El mayor costo asociado con el recorte en ese momento era que Flutter solía agregar una llamada a saveLayer después de cada recorte (a menos que fuera un recorte de rectángulo simple alineado con los ejes) para evitar los artefactos de bordes sangrantes como se describe en el Issue 18057. Esos comportamientos eran universales en las aplicaciones material a través de widgets como Card, Chip, Button, etc., lo que resultaba en que PhysicalShape y PhysicalModel recortaran su contenido.

Una llamada a saveLayer es especialmente costosa en dispositivos más antiguos porque crea un objetivo de renderizado fuera de la pantalla (offscreen render target), y un cambio de objetivo de renderizado a veces puede costar alrededor de 1 ms.

Incluso sin la llamada a saveLayer, un recorte sigue siendo costoso porque se aplica a todos los dibujos subsiguientes hasta que se restaura. Por lo tanto, un solo recorte puede ralentizar el rendimiento en cientos de operaciones de dibujo.

Además de los problemas de rendimiento, Flutter también sufría algunos problemas de corrección, ya que el recorte no se gestionaba ni se implementaba en un único lugar. En varios lugares, se insertaba saveLayer en el lugar equivocado y, por lo tanto, solo aumentaba el costo de rendimiento sin solucionar ningún artefacto de bordes sangrantes.

Por lo tanto, unificamos el control de clipBehavior y su implementación en este cambio disruptivo. El clipBehavior predeterminado es Clip.none para la mayoría de los widgets para ahorrar rendimiento, excepto los siguientes:

  • ClipPath tiene como valor predeterminado Clip.antiAlias
  • ClipRRect tiene como valor predeterminado Clip.antiAlias
  • ClipRect tiene como valor predeterminado Clip.hardEdge
  • Stack tiene como valor predeterminado Clip.hardEdge
  • EditableText tiene como valor predeterminado Clip.hardEdge
  • ListWheelScrollView tiene como valor predeterminado Clip.hardEdge
  • SingleChildScrollView tiene como valor predeterminado Clip.hardEdge
  • NestedScrollView tiene como valor predeterminado Clip.hardEdge
  • ShrinkWrappingViewport tiene como valor predeterminado Clip.hardEdge

Guía de migración

#

Tienes 4 opciones para migrar tu código:

  1. Deja tu código como está si tu contenido no necesita ser recortado (por ejemplo, ninguno de los hijos de los widgets se expande fuera del límite de su padre). Esto probablemente tendrá un impacto positivo en el rendimiento general de tu aplicación.
  2. Agrega clipBehavior: Clip.hardEdge si necesitas recortar, y el recorte sin antialias es lo suficientemente bueno para tus (y los de tus clientes) ojos. Este es el caso común cuando recortas rectángulos o formas con áreas curvas muy pequeñas (como las esquinas de rectángulos redondeados).
  3. Agrega clipBehavior: Clip.antiAlias si necesitas un recorte con antialias. Esto te da bordes más suaves a un costo ligeramente mayor. Este es el caso común cuando se trabaja con círculos y arcos.
  4. Agrega clip.antiAliasWithSaveLayer si quieres exactamente el mismo comportamiento que antes (mayo de 2018). Ten en cuenta que es muy costoso en términos de rendimiento. Es probable que esto solo se necesite en raras ocasiones. Un caso en el que podrías necesitar esto es si tienes una imagen superpuesta sobre un color de fondo muy diferente. En estos casos, considera si puedes evitar la superposición de múltiples colores en un mismo lugar (por ejemplo, haciendo que el color de fondo solo esté presente donde la imagen está ausente).

Para el widget Stack específicamente, si anteriormente usabas overflow: Overflow.visible, reemplázalo con clipBehavior: Clip.none.

Para el widget ListWheelViewport, si anteriormente especificabas clipToSize, reemplázalo con el clipBehavior correspondiente: Clip.none para clipToSize = false y Clip.hardEdge para clipToSize = true.

Código antes de la migración:

dart
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Center(
          child: Stack(
            overflow: Overflow.visible,
            children: const <Widget>[
              SizedBox(
                width: 100,
                height: 100,
              ),
            ],
          ),
        ),
      ),
    );

Código después de la migración:

dart
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Center(
          child: Stack(
            clipBehavior: Clip.none,
            children: const <Widget>[
              SizedBox(
                width: 100.0,
                height: 100.0,
              ),
            ],
          ),
        ),
      ),
    );

Timeline

#

Introducido en la versión: varias
En la versión estable: 2.0.0

Referencias

#

Documentación de la API:

Issues relevantes:

PRs relevantes:

  • PR 5420: Eliminar saveLayer innecesario
  • PR 18576: Agregar el enum Clip a Material y widgets relacionados
  • PR 18616: Eliminar saveLayer después de clip de dart
  • PR 5647: Agregar ClipMode a las capas ClipPath/ClipRRect y PhysicalShape
  • PR 5670: Agregar interruptor de antialias a las llamadas de clip de canvas
  • PR 5853: Renombrar clip mode a clip behavior
  • PR 5868: Renombrar clip a clipBehavior en compositing.dart
  • PR 5973: Llamar a drawPaint en lugar de drawPath si hay un clip
  • PR 5952: Llamar a drawPath sin clip si es posible
  • PR 20205: Establecer el clipBehavior predeterminado en Clip.none y actualizar pruebas
  • PR 20538: Exponer clipBehavior a más Material Buttons
  • PR 20751: Agregar customBorder a InkWell para que pueda recortar ShapeBorder
  • PR 20752: Establecer el recorte predeterminado en Clip.none nuevamente
  • PR 21012: Agregar pruebas de no-recorte por defecto a más botones
  • PR 21703: El clipBehavior predeterminado de ClipRect es hardEdge
  • PR 21826: Falta el recorte hardEdge predeterminado para ClipRectLayer