画像を角丸にしたり端にくっつける方法

2024/12/30に公開

縦長だったり横並びだったり

Flutterで画像のサイズを設定して角丸にしたり縦長にしたりする方法ってどうするのか?
たまにしかやらないのでやり方をメモする目的で記事を書くことにした。

こんなUI作ります

タイトル変なのありますがお気になさらず😅


画像素材はGithubあります。自分の好みの画像を使っていただいて構いません。下のサイズは640×359ぐらいですね。pixabayというフリー画像のサービスからもらってきました。

ソースコードはGithubにあります

画像はassets/iamgesに配置してpubspec.yamlで読み込めるように設定します。

https://github.com/sakurakotubaki/widget_cookbook_demo/blob/main/pubspec.yaml

カードコンポーネントを作成。

画像を加工して表示するコンポーネントです。とあるプロジェクトに入ったときは関数で作られたコンポーネントがあったが、コードが見ずらかったりカスタマイズしずらいのでオススメしません💦

  • クラスで作ると可読性が上がる。
  • プロパティを追加できる。関数だと引数だからね。
  • 後でRiverpodのコードに書き換えることができる。関数でもできるみたいだが。
import 'package:flutter/material.dart';

// 1. 縦長の画像カード
class VerticalImageCard extends StatelessWidget {
  final String imageUrl;
  final String title;
  final String subtitle;
  final String price;

  const VerticalImageCard({
    super.key,
    required this.imageUrl,
    required this.title,
    required this.subtitle,
    required this.price,
  });

  
  Widget build(BuildContext context) {
    return Container(
      width: 180,
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withAlpha(0x05),
            blurRadius: 10,
            offset: const Offset(0, 5),
          ),
        ],
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          ClipRRect(
            borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
            child: Image.asset(
              imageUrl,
              height: 240,
              width: double.infinity,
              fit: BoxFit.cover,
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(12),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  title,
                  style: const TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 4),
                Text(
                  subtitle,
                  style: TextStyle(
                    fontSize: 14,
                    color: Colors.grey[600],
                  ),
                ),
                const SizedBox(height: 8),
                Text(
                  price,
                  style: const TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.bold,
                    color: Colors.black87,
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

// 2. 正方形に近い画像カード(ショッピングアプリスタイル)
class SquareImageCard extends StatelessWidget {
  final String imageUrl;
  final String title;
  final String subtitle;
  final String price;

  const SquareImageCard({
    super.key,
    required this.imageUrl,
    required this.title,
    required this.subtitle,
    required this.price,
  });

  
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withAlpha(0x05),
            blurRadius: 10,
            offset: const Offset(0, 5),
          ),
        ],
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Expanded(
            child: ClipRRect(
              borderRadius:
                  const BorderRadius.vertical(top: Radius.circular(16)),
              child: Image.asset(
                imageUrl,
                width: double.infinity,
                fit: BoxFit.cover,
              ),
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(12),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  title,
                  style: const TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 4),
                Text(
                  subtitle,
                  style: TextStyle(
                    fontSize: 14,
                    color: Colors.grey[600],
                  ),
                ),
                const SizedBox(height: 8),
                Text(
                  price,
                  style: const TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.bold,
                    color: Colors.black87,
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

// 3. 横長の画像カード(画像左、テキスト右)
class HorizontalImageCard extends StatelessWidget {
  final String imageUrl;
  final String title;
  final String subtitle;
  final String price;

  const HorizontalImageCard({
    super.key,
    required this.imageUrl,
    required this.title,
    required this.subtitle,
    required this.price,
  });

  
  Widget build(BuildContext context) {
    return Container(
      height: 120,
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withAlpha(0x05),
            blurRadius: 10,
            offset: const Offset(0, 5),
          ),
        ],
      ),
      child: Row(
        children: [
          ClipRRect(
            borderRadius:
                const BorderRadius.horizontal(left: Radius.circular(16)),
            child: Image.asset(
              imageUrl,
              width: 120,
              height: 120,
              fit: BoxFit.cover,
            ),
          ),
          Expanded(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text(
                    title,
                    style: const TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    subtitle,
                    style: TextStyle(
                      fontSize: 14,
                      color: Colors.grey[600],
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    price,
                    style: const TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.bold,
                      color: Colors.black87,
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

main.dartでモジュールを呼び出してUIに表示するのをやってみましょうか。ただUIを作るだけなのですが時々見る綺麗なショッピングアプリのようなものがあると白い背景に文字を配置するだけよりは楽しく感じます。

import 'package:flutter/material.dart';
import 'image_cards.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
        scaffoldBackgroundColor: Colors.grey[100],
      ),
      home: const CardDemoPage(),
    );
  }
}

class CardDemoPage extends StatelessWidget {
  const CardDemoPage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('カードデザインデモ'),
        backgroundColor: Colors.white,
        foregroundColor: Colors.black,
      ),
      body: Padding(
        padding: const EdgeInsets.all(10.0),
        child: ListView(
          children: [
            const Text(
              '1. 縦長カード',
              style: TextStyle(
                fontSize: 18,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 16),
            SizedBox(
              height: 360,
              child: ListView(
                scrollDirection: Axis.horizontal,
                children: [
                  Padding(
                    padding: const EdgeInsets.only(right: 16),
                    child: VerticalImageCard(
                      imageUrl: 'assets/images/couch.jpg',
                      title: 'モダンソファ',
                      subtitle: '快適な座り心地',
                      price: '¥29,800',
                    ),
                  ),
                  Padding(
                    padding: const EdgeInsets.only(right: 16),
                    child: VerticalImageCard(
                      imageUrl: 'assets/images/da.jpg',
                      title: 'ダイニングセット',
                      subtitle: '4人掛けテーブル',
                      price: '¥45,800',
                    ),
                  ),
                  VerticalImageCard(
                    imageUrl: 'assets/images/living.jpg',
                    title: 'リビングセット',
                    subtitle: 'シンプルデザイン',
                    price: '¥89,800',
                  ),
                ],
              ),
            ),
            const SizedBox(height: 32),
            const Text(
              '2. 正方形カード',
              style: TextStyle(
                fontSize: 18,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 16),
            GridView.count(
              shrinkWrap: true,
              physics: const NeverScrollableScrollPhysics(),
              crossAxisCount: 2,
              mainAxisSpacing: 16,
              crossAxisSpacing: 16,
              childAspectRatio: 0.8,
              children: [
                SquareImageCard(
                  imageUrl: 'assets/images/couch.jpg',
                  title: 'モダンソファ',
                  subtitle: '快適な座り心地',
                  price: '¥29,800',
                ),
                SquareImageCard(
                  imageUrl: 'assets/images/da.jpg',
                  title: 'ダイニングセット',
                  subtitle: '4人掛けテーブル',
                  price: '¥45,800',
                ),
              ],
            ),
            const SizedBox(height: 32),
            const Text(
              '3. 横長カード',
              style: TextStyle(
                fontSize: 18,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 16),
            HorizontalImageCard(
              imageUrl: 'assets/images/couch.jpg',
              title: 'モダンソファ',
              subtitle: '快適な座り心地',
              price: '¥29,800',
            ),
            const SizedBox(height: 16),
            HorizontalImageCard(
              imageUrl: 'assets/images/da.jpg',
              title: 'ダイニングセット',
              subtitle: '4人掛けテーブル',
              price: '¥45,800',
            ),
            const SizedBox(height: 16),
            HorizontalImageCard(
              imageUrl: 'assets/images/living.jpg',
              title: 'リビングセット',
              subtitle: 'シンプルデザイン',
              price: '¥89,800',
            ),
          ],
        ),
      ),
    );
  }
}

画像サイズの変更と角丸にする方法

Flutter Image Handling Guide

主要なウィジェットとプロパティ

1. ClipRRect

角丸の画像を実現するための主要なウィジェット

ClipRRect(
  borderRadius: BorderRadius.circular(16),
  child: Image.asset(...),
)

公式ドキュメント: ClipRRect class

2. Image.asset

アセット画像を表示するためのウィジェット

Image.asset(
  'assets/images/example.jpg',
  width: double.infinity,
  height: 240,
  fit: BoxFit.cover,
)

公式ドキュメント: Image class

3. BoxFit プロパティ

画像のサイズ調整方法を指定する重要なプロパティ

  • BoxFit.cover: アスペクト比を保ちながら、指定された領域を完全に覆うようにサイズ調整
  • BoxFit.contain: アスペクト比を保ちながら、指定された領域内に収まるようにサイズ調整
  • BoxFit.fill: アスペクト比を無視して、指定された領域いっぱいに引き伸ばす

公式ドキュメント: BoxFit enum

レイアウトパターン

1. 縦長カード

Container(
  width: 180,
  child: ClipRRect(
    borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
    child: Image.asset(
      imageUrl,
      height: 240,
      width: double.infinity,
      fit: BoxFit.cover,
    ),
  ),
)

2. 正方形カード

Expanded(
  child: ClipRRect(
    borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
    child: Image.asset(
      imageUrl,
      width: double.infinity,
      fit: BoxFit.cover,
    ),
  ),
)

3. 横長カード

ClipRRect(
  borderRadius: BorderRadius.horizontal(left: Radius.circular(16)),
  child: Image.asset(
    imageUrl,
    width: 120,
    height: 120,
    fit: BoxFit.cover,
  ),
)

ベストプラクティス

  1. 画像の最適化

    • 適切なサイズの画像を使用
    • WebPやPNGなど適切なフォーマットを選択
    • pubspec.yamlで画像アセットを正しく設定
  2. パフォーマンス考慮

    • ClipRRectは必要な場合のみ使用(パフォーマンスに影響あり)
    • 大きな画像は必要に応じてキャッシュを検討
  3. レスポンシブ対応

    • double.infinityを使用して親ウィジェットに合わせる
    • ExpandedFlexibleを使用して柔軟なレイアウトを実現

関連するFlutterドキュメント

Discussion