Il vetro smerigliato è tornato di moda: barre di navigazione traslucide, card che lasciano intravedere lo sfondo, dialog con il contenuto sottostante sfocato. In Flutter tutto questo si ottiene con due widget apparentemente simili ma profondamente diversi — BackdropFilter e ImageFiltered — e con una buona dose di attenzione alle performance, perché la sfocatura è uno degli effetti più costosi che possiamo chiedere alla GPU.
In questa guida vediamo come usarli correttamente, quali errori evitare e come sfruttare BackdropGroup, l'ottimizzazione introdotta in Flutter 3.35 per i blur multipli.
BackdropFilter vs ImageFiltered: la differenza fondamentale
Entrambi applicano un ImageFilter, ma su soggetti diversi:
ImageFilteredfiltra il proprio child. È come applicare un effetto Photoshop al widget stesso: una foto sfocata, un'icona con un alone, un testo fuori fuoco.BackdropFilterfiltra tutto ciò che è già stato disegnato dietro di lui nello stesso layer, e poi disegna il proprio child sopra, nitido. È questo il widget del glassmorphism.
// Sfoca l'immagine stessa
ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 8, sigmaY: 8),
child: Image.asset('assets/cover.jpg'),
)
// Sfoca ciò che sta sotto e ci appoggia sopra un testo nitido
Stack(
children: [
Image.asset('assets/cover.jpg', fit: BoxFit.cover),
BackdropFilter(
filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
child: Container(
color: Colors.white.withValues(alpha: 0.12),
alignment: Alignment.center,
child: const Text('Vetro smerigliato'),
),
),
],
)
Due regole da ricordare subito:
BackdropFiltersenza child (o con un child completamente trasparente) non si vede. Il filtro viene applicato all'area coperta dal child: serve almeno unContainercon un colore semi-trasparente oSizedBox.expand.- Per impostazione predefinita si espande fino a riempire il genitore. Dentro uno
Stacknon vincolato, sfocherà l'intero schermo. Il confinamento si ottiene con il clipping.
Clipping: il passo che quasi tutti sbagliano
Per ottenere una card di vetro con angoli arrotondati bisogna ritagliare il BackdropFilter, non il suo contenuto. Il modo corretto è avvolgerlo in un ClipRRect (o ClipPath), perché il filtro viene applicato al rettangolo di disegno e senza clip "esce" dai bordi.
class GlassCard extends StatelessWidget {
const GlassCard({
super.key,
required this.child,
this.blur = 16,
this.borderRadius = const BorderRadius.all(Radius.circular(20)),
});
final Widget child;
final double blur;
final BorderRadius borderRadius;
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: borderRadius,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur),
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.15),
borderRadius: borderRadius,
border: Border.all(
color: Colors.white.withValues(alpha: 0.25),
width: 1,
),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
}
}
Il bordo chiaro a 1px non è un vezzo estetico: è ciò che dà l'impressione dello spessore del vetro e separa la superficie dallo sfondo.
TileMode: eliminare gli aloni ai bordi
ImageFilter.blur accetta un parametro tileMode che decide come viene campionato il contenuto oltre i limiti dell'area filtrata. Il default è TileMode.clamp, che replica i pixel di bordo e può creare strisciate visibili quando il blur è vicino al margine dello schermo. TileMode.decal tratta l'esterno come trasparente, risultato spesso più pulito:
ImageFilter.blur(sigmaX: 20, sigmaY: 20, tileMode: TileMode.decal)
Una app bar traslucida
Un classico: la barra superiore che sfoca il contenuto che le scorre sotto. Si usa flexibleSpace perché è il livello che riempie l'intera area della AppBar, inclusa la status bar.
AppBar(
title: const Text('Esplora'),
backgroundColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
flexibleSpace: ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 18, sigmaY: 18),
child: Container(
color: Theme.of(context)
.colorScheme
.surface
.withValues(alpha: 0.6),
),
),
),
)
Perché il contenuto scorra realmente sotto la barra, lo Scaffold deve avere extendBodyBehindAppBar: true (e extendBody: true per la bottom bar). Ricordate poi di compensare il padding con MediaQuery.paddingOf(context).top o con un SliverAppBar appropriato.
Sfocare lo sfondo di un dialog
Un pattern molto efficace è sostituire il barrier scuro standard con un barrier sfocato animato:
Future<T?> showBlurredDialog<T>(BuildContext context, Widget child) {
return showGeneralDialog<T>(
context: context,
barrierDismissible: true,
barrierLabel: 'Chiudi',
barrierColor: Colors.black.withValues(alpha: 0.25),
transitionDuration: const Duration(milliseconds: 220),
pageBuilder: (_, __, ___) => child,
transitionBuilder: (context, animation, _, child) {
final curved = CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
);
return AnimatedBuilder(
animation: curved,
builder: (context, _) => BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 10 * curved.value,
sigmaY: 10 * curved.value,
),
child: Opacity(opacity: curved.value, child: child),
),
child: child,
);
},
);
}
Attenzione: un sigma pari a 0 con Impeller è comunque un'operazione di layer. Se volete davvero azzerare il costo quando l'animazione è ferma a zero, restituite il child senza BackdropFilter.
BackdropGroup: blur multipli senza moltiplicare il costo
Ogni BackdropFilter forza il motore a leggere il contenuto già renderizzato, applicare la sfocatura e ricomporre: internamente equivale a un saveLayer. Se in una schermata ne avete cinque (una barra, tre card e un bottone flottante), il costo si somma, perché ciascuno ricalcola la propria sfocatura in modo indipendente.
Da Flutter 3.35 esiste BackdropGroup, che permette ai filtri discendenti di condividere il risultato della sfocatura. Basta avvolgere il sottoalbero e usare il costruttore BackdropFilter.grouped:
BackdropGroup(
child: Column(
children: [
BackdropFilter.grouped(
filter: ImageFilter.blur(sigmaX: 14, sigmaY: 14),
child: const GlassPanel(title: 'Meteo'),
),
BackdropFilter.grouped(
filter: ImageFilter.blur(sigmaX: 14, sigmaY: 14),
child: const GlassPanel(title: 'Agenda'),
),
],
),
)
Il guadagno è tangibile quando i filtri usano lo stesso sigma e insistono sullo stesso sfondo: invece di N passate di blur se ne paga sostanzialmente una. Se usate ancora una versione precedente, la soluzione alternativa è ridurre il numero di filtri accorpando le superfici di vetro in un unico widget.
Strategie per non perdere frame
- Evitate i blur dentro le liste scrollabili. Un
BackdropFilterper ogni elemento di unaListViewsignifica ricalcolare la sfocatura a ogni frame di scroll. Se proprio serve, limitatelo agli elementi visibili o usate un'immagine pre-sfocata. - Attenzione a
RepaintBoundary. Isolare il contenuto dietro al filtro aiuta, ma mettere unRepaintBoundaryfra lo sfondo e ilBackdropFilterpuò spezzare il layer e far sì che il filtro non "veda" più nulla da sfocare. - Il sigma conta. Con Impeller il costo cresce con il raggio: valori fra 8 e 20 sono quasi sempre sufficienti. Non serve arrivare a 50.
- Preferite
ImageFilteredquando lo sfondo è statico. Se dovete sfocare una foto che non cambia, filtratela una volta sola invece di filtrare il backdrop a ogni frame. Ancora meglio: generate la versione sfocata lato server o a build time. - Misurate. Attivate l'overlay delle performance (
flutter run --profile+ DevTools, oppureshowPerformanceOverlay: true) e osservate la barra GPU: i blur si manifestano quasi sempre lì, non nella barra UI. - Occhio al Web. Su Flutter Web il
BackdropFilterè supportato ma può essere nettamente più costoso rispetto al mobile, soprattutto con il renderer HTML dei canali meno recenti. Testate sempre sul target reale.
Oltre il blur: comporre filtri
ImageFilter non fa solo sfocature. Potete combinare più filtri con ImageFilter.compose per ottenere, ad esempio, un vetro desaturato e leggermente più luminoso:
final saturate = ColorFilter.mode(
Colors.white.withValues(alpha: 0.08),
BlendMode.overlay,
);
BackdropFilter(
filter: ImageFilter.compose(
outer: ImageFilter.matrix(
(Matrix4.identity()..scale(1.04, 1.04)).storage,
),
inner: ImageFilter.blur(sigmaX: 18, sigmaY: 18),
),
child: ColorFiltered(
colorFilter: saturate,
child: const SizedBox.expand(),
),
)
Il leggero ingrandimento (ImageFilter.matrix) nasconde i bordi trasparenti che il blur può lasciare ed è un trucco usato spesso nelle UI di sistema.
Leggibilità e accessibilità
Il vetro è bello ma pericoloso per il contrasto: un testo bianco su un pannello traslucido può diventare illeggibile quando dietro passa un'immagine chiara. Qualche accorgimento:
- aggiungete sempre un colore di tinta sotto il testo (
surface.withValues(alpha: 0.6)), non affidatevi solo alla sfocatura; - verificate il rapporto di contrasto nel caso peggiore, non su uno sfondo di comodo;
- rispettate
MediaQuery.of(context).disableAnimationse, su iOS, considerate di ridurre la trasparenza quando l'utente ha attivato le relative impostazioni di accessibilità, offrendo un fallback a superficie piena.
In sintesi
ImageFiltered filtra il contenuto, BackdropFilter filtra lo sfondo: scegliere il widget giusto è metà del lavoro. L'altra metà è il contenimento (clip corretto, TileMode adeguato) e il controllo del costo: pochi filtri, sigma ragionevoli, nessun blur dentro le liste e, su Flutter 3.35 o superiore, BackdropGroup per condividere il lavoro fra più superfici. Con queste regole il glassmorphism smette di essere un lusso da schermata statica e diventa un elemento utilizzabile nella UI di tutti i giorni.