Immagini arrotondate e forme personalizzate in Flutter con ClipRRect, ClipOval e ClipPath

Foto di Maxim Berg su Unsplash

GuidePrincipiante30 min Flutter 3.x

Immagini arrotondate e forme personalizzate in Flutter con ClipRRect, ClipOval e ClipPath

In Flutter tutto è un widget, anche il ritaglio. Quando devi arrotondare gli angoli di un'immagine, creare un avatar circolare o disegnare un header con un taglio diagonale, non serve preparare PNG già ritagliati: bastano i widget della famiglia Clip.

In questo tutorial, pensato per chi è alle prime armi, vedremo:

  • ClipRRect per angoli arrotondati su immagini e contenitori
  • ClipOval e CircleAvatar per foto profilo circolari
  • ClipRect per mostrare solo una porzione di un widget
  • ClipPath con un CustomClipper<Path> per forme libere (header a onda, taglio diagonale)
  • quando conviene usare BoxDecoration invece di un Clip, e come gestire clipBehavior e antialiasing

Alla fine avrai una piccola schermata "profilo" con header curvo, avatar circolare e card con immagini arrotondate, realizzata interamente con widget standard di Flutter (nessun pacchetto esterno).

  1. 1

    Preparare il progetto e la schermata di base

    Crea un nuovo progetto (o usa uno esistente) e sostituisci il contenuto di lib/main.dart con una schermata minimale che mostra un'immagine di rete senza alcun ritaglio: sarà il nostro punto di partenza per vedere la differenza.

    flutter create clip_demo
    cd clip_demo
    

    Nota: usiamo Image.network, quindi su Android assicurati che il permesso Internet sia presente (nei progetti nuovi lo è già nei manifest di debug).

    import 'package:flutter/material.dart';
    
    void main() => runApp(const ClipDemoApp());
    
    class ClipDemoApp extends StatelessWidget {
      const ClipDemoApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Clip Demo',
          theme: ThemeData(
            colorSchemeSeed: Colors.indigo,
            useMaterial3: true,
          ),
          home: const HomePage(),
        );
      }
    }
    
    class HomePage extends StatelessWidget {
      const HomePage({super.key});
    
      static const imageUrl =
          'https://picsum.photos/600/400';
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Ritagli in Flutter')),
          body: ListView(
            padding: const EdgeInsets.all(16),
            children: [
              const Text('Immagine senza ritaglio'),
              const SizedBox(height: 8),
              Image.network(
                imageUrl,
                height: 180,
                width: double.infinity,
                fit: BoxFit.cover,
              ),
            ],
          ),
        );
      }
    }

    Risultato atteso

    L'app mostra una lista con un'immagine rettangolare a tutta larghezza, con angoli vivi.

  2. 2

    Arrotondare gli angoli con ClipRRect

    ClipRRect (Rounded Rectangle) ritaglia il figlio secondo un BorderRadius. È la soluzione standard per le immagini con angoli morbidi.

    Punti chiave:

    • borderRadius accetta BorderRadius.circular(...), BorderRadius.only(...), BorderRadius.vertical(...) ecc.
    • il ritaglio si applica a qualsiasi figlio, non solo alle immagini (puoi arrotondare anche un Container colorato o una Column)
    • usa fit: BoxFit.cover sull'immagine per evitare deformazioni

    Aggiungi questi widget alla ListView creata nel passo precedente.

    const SizedBox(height: 24),
    const Text('ClipRRect: angoli arrotondati'),
    const SizedBox(height: 8),
    ClipRRect(
      borderRadius: BorderRadius.circular(20),
      child: Image.network(
        imageUrl,
        height: 180,
        width: double.infinity,
        fit: BoxFit.cover,
      ),
    ),
    
    const SizedBox(height: 24),
    const Text('ClipRRect: solo angoli superiori'),
    const SizedBox(height: 8),
    ClipRRect(
      borderRadius: const BorderRadius.only(
        topLeft: Radius.circular(32),
        topRight: Radius.circular(32),
      ),
      child: Container(
        height: 120,
        width: double.infinity,
        color: Colors.indigo,
        alignment: Alignment.center,
        child: const Text(
          'Header arrotondato',
          style: TextStyle(color: Colors.white, fontSize: 18),
        ),
      ),
    ),

    Risultato atteso

    La seconda immagine ha gli angoli arrotondati di 20 px e sotto compare un contenitore indaco con solo i due angoli superiori smussati.

  3. 3

    Avatar circolari con ClipOval e CircleAvatar

    Per una foto profilo hai due strade:

    1. ClipOval: ritaglia il figlio in un'ellisse inscritta nelle sue dimensioni. Se il figlio è quadrato, ottieni un cerchio perfetto.
    2. CircleAvatar: widget Material già pronto, con backgroundImage, radius e child per le iniziali di fallback.

    Regola pratica: usa CircleAvatar per gli avatar classici, ClipOval quando devi ritagliare un widget qualsiasi (un video, un Container con gradiente, una Stack).

    Per un bordo attorno all'avatar, avvolgi il tutto in un Container con BoxDecoration(shape: BoxShape.circle, border: ...) e un po' di padding.

    const SizedBox(height: 24),
    const Text('ClipOval e CircleAvatar'),
    const SizedBox(height: 8),
    Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        // 1. ClipOval su un'immagine quadrata
        ClipOval(
          child: Image.network(
            'https://picsum.photos/200',
            width: 90,
            height: 90,
            fit: BoxFit.cover,
          ),
        ),
    
        // 2. CircleAvatar con bordo
        Container(
          padding: const EdgeInsets.all(3),
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            border: Border.all(color: Colors.indigo, width: 2),
          ),
          child: const CircleAvatar(
            radius: 42,
            backgroundImage:
                NetworkImage('https://picsum.photos/201'),
          ),
        ),
    
        // 3. Fallback con iniziali
        const CircleAvatar(
          radius: 45,
          backgroundColor: Colors.indigo,
          child: Text(
            'MR',
            style: TextStyle(color: Colors.white, fontSize: 24),
          ),
        ),
      ],
    ),

    Risultato atteso

    Una riga con tre avatar circolari: immagine ritagliata, avatar con bordo indaco e avatar con le iniziali.

  4. 4

    Mostrare solo una porzione con ClipRect

    ClipRect taglia il figlio sul suo rettangolo di layout. Da solo sembra inutile, ma diventa potente se combinato con Align: impostando widthFactor o heightFactor puoi mostrare solo una fetta del widget.

    Questo è utile per:

    • anteprime "a banda" di un'immagine grande
    • effetti di reveal parziale
    • impedire che un figlio con Transform.scale o OverflowBox sbordi dal suo spazio

    Nell'esempio mostriamo solo la metà superiore dell'immagine e la metà sinistra.

    const SizedBox(height: 24),
    const Text('ClipRect + Align: mezza immagine'),
    const SizedBox(height: 8),
    Row(
      children: [
        Expanded(
          child: ClipRect(
            child: Align(
              alignment: Alignment.topCenter,
              heightFactor: 0.5, // mostra solo il 50% in altezza
              child: Image.network(
                'https://picsum.photos/400/300',
                fit: BoxFit.cover,
              ),
            ),
          ),
        ),
        const SizedBox(width: 12),
        Expanded(
          child: ClipRect(
            child: Align(
              alignment: Alignment.centerLeft,
              widthFactor: 0.5, // mostra solo il 50% in larghezza
              child: Image.network(
                'https://picsum.photos/400/301',
                fit: BoxFit.cover,
              ),
            ),
          ),
        ),
      ],
    ),

    Risultato atteso

    Due immagini affiancate: la prima mostra solo la fascia superiore, la seconda solo la metà sinistra, senza spazi vuoti o overflow.

  5. 5

    Forme libere con ClipPath e un CustomClipper

    Quando la forma non è un rettangolo né un'ellisse, entra in gioco ClipPath. Devi fornirgli un CustomClipper<Path> in cui disegni il contorno usando le coordinate del widget (size.width, size.height).

    Metodi utili sul Path:

    • lineTo(x, y): linea retta
    • quadraticBezierTo(cx, cy, x, y): curva con un punto di controllo
    • close(): chiude la forma

    In shouldReclip restituisci false se la forma non cambia mai: eviti ricalcoli inutili.

    Creiamo due clipper: uno a onda per un header e uno con taglio diagonale.

    class WaveClipper extends CustomClipper<Path> {
      @override
      Path getClip(Size size) {
        final path = Path();
        path.lineTo(0, size.height - 40);
        path.quadraticBezierTo(
          size.width / 4, size.height,      // punto di controllo
          size.width / 2, size.height - 20, // punto di arrivo
        );
        path.quadraticBezierTo(
          size.width * 3 / 4, size.height - 45,
          size.width, size.height - 15,
        );
        path.lineTo(size.width, 0);
        path.close();
        return path;
      }
    
      @override
      bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
    }
    
    class DiagonalClipper extends CustomClipper<Path> {
      @override
      Path getClip(Size size) {
        return Path()
          ..lineTo(0, size.height)
          ..lineTo(size.width, size.height - 50)
          ..lineTo(size.width, 0)
          ..close();
      }
    
      @override
      bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
    }
    
    // --- uso nella ListView ---
    // const SizedBox(height: 24),
    // const Text('ClipPath: header a onda'),
    // const SizedBox(height: 8),
    // ClipPath(
    //   clipper: WaveClipper(),
    //   child: Container(
    //     height: 160,
    //     color: Colors.indigo,
    //     alignment: Alignment.topCenter,
    //     padding: const EdgeInsets.only(top: 32),
    //     child: const Text('Benvenuto',
    //         style: TextStyle(color: Colors.white, fontSize: 24)),
    //   ),
    // ),
    // const SizedBox(height: 24),
    // ClipPath(
    //   clipper: DiagonalClipper(),
    //   child: Image.network(imageUrl,
    //       height: 180, width: double.infinity, fit: BoxFit.cover),
    // ),

    Risultato atteso

    Compare un header indaco con il bordo inferiore ondulato e, sotto, un'immagine con il bordo inferiore tagliato in diagonale.

  6. 6

    Quando usare BoxDecoration al posto di un Clip

    Non sempre serve un widget Clip. Se devi solo arrotondare uno sfondo (colore, gradiente o immagine), Container con BoxDecoration è più leggero perché evita un livello di ritaglio aggiuntivo.

    Linee guida:

    • sfondo arrotondato (colore/gradiente/immagine di sfondo) → BoxDecoration(borderRadius: ..., image: ...)
    • contenuto figlio da tagliare (immagine Image, lista, video, Stack) → ClipRRect
    • Card, Material, InkWell hanno shape/borderRadius e clipBehavior: usali invece di annidare Clip
    • se combini BoxDecoration arrotondata e figlio che sborda, imposta clipBehavior: Clip.antiAlias sul Container

    Un altro dettaglio: tutti i Clip accettano clipBehavior. Il default Clip.antiAlias va bene quasi sempre; Clip.antiAliasWithSaveLayer è il più costoso e serve solo in casi rari di fusione di colori.

    // A) Sfondo arrotondato: nessun Clip necessario
    Container(
      height: 160,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(24),
        gradient: const LinearGradient(
          colors: [Colors.indigo, Colors.purple],
        ),
        boxShadow: const [
          BoxShadow(blurRadius: 12, color: Colors.black26, offset: Offset(0, 6)),
        ],
        image: const DecorationImage(
          image: NetworkImage('https://picsum.photos/600/401'),
          fit: BoxFit.cover,
          opacity: 0.6,
        ),
      ),
      alignment: Alignment.center,
      child: const Text('BoxDecoration',
          style: TextStyle(color: Colors.white, fontSize: 20)),
    ),
    
    const SizedBox(height: 16),
    
    // B) Card che ritaglia da sola il contenuto
    Card(
      clipBehavior: Clip.antiAlias,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(20),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Image.network('https://picsum.photos/600/402',
              height: 150, fit: BoxFit.cover),
          const Padding(
            padding: EdgeInsets.all(12),
            child: Text('Immagine ritagliata dalla Card'),
          ),
        ],
      ),
    ),

    Risultato atteso

    Il gradiente con immagine di sfondo è arrotondato senza usare Clip, e la Card ritaglia automaticamente l'immagine ai propri angoli.

  7. 7

    Mettere tutto insieme: una schermata profilo

    Uniamo i pezzi in una schermata profilo realistica: header a onda, avatar circolare sovrapposto con Stack, e una griglia di immagini arrotondate.

    Nota su Stack: l'avatar viene posizionato con Positioned e sporge oltre l'header; per questo diamo alla Stack un'altezza sufficiente e usiamo clipBehavior: Clip.none se vuoi permettere lo sbordo.

    Consigli finali

    • ogni Clip ha un costo: non annidarne più del necessario
    • preferisci const dove possibile per ridurre le ricostruzioni
    • verifica il risultato su schermi diversi: le forme di ClipPath dipendono da size, quindi si adattano automaticamente
    • per icone e loghi vettoriali complessi valuta un SVG invece di un CustomClipper
    class ProfilePage extends StatelessWidget {
      const ProfilePage({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: ListView(
            padding: EdgeInsets.zero,
            children: [
              SizedBox(
                height: 220,
                child: Stack(
                  clipBehavior: Clip.none,
                  alignment: Alignment.topCenter,
                  children: [
                    ClipPath(
                      clipper: WaveClipper(),
                      child: Container(height: 170, color: Colors.indigo),
                    ),
                    Positioned(
                      bottom: 0,
                      child: Container(
                        padding: const EdgeInsets.all(4),
                        decoration: const BoxDecoration(
                          shape: BoxShape.circle,
                          color: Colors.white,
                        ),
                        child: const CircleAvatar(
                          radius: 50,
                          backgroundImage:
                              NetworkImage('https://picsum.photos/300'),
                        ),
                      ),
                    ),
                  ],
                ),
              ),
              const SizedBox(height: 12),
              const Center(
                child: Text('Giulia Rossi',
                    style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
              ),
              const SizedBox(height: 16),
              GridView.builder(
                shrinkWrap: true,
                physics: const NeverScrollableScrollPhysics(),
                padding: const EdgeInsets.all(16),
                itemCount: 6,
                gridDelegate:
                    const SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 3,
                  mainAxisSpacing: 10,
                  crossAxisSpacing: 10,
                ),
                itemBuilder: (context, index) => ClipRRect(
                  borderRadius: BorderRadius.circular(14),
                  child: Image.network(
                    'https://picsum.photos/200?random=$index',
                    fit: BoxFit.cover,
                  ),
                ),
              ),
            ],
          ),
        );
      }
    }

    Risultato atteso

    Impostando `home: const ProfilePage()` vedi una schermata profilo con header curvo indaco, avatar circolare bordato di bianco a cavallo dell'onda e una griglia 3x3 di miniature con angoli arrotondati.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!