🍸

UI/UX DISIGN 美しい、体験が良いデザインを作りたい

2024/12/29に公開

ユーザー体験がよくなるデザインを作りたい

こんにちわ俺ちゃんだよ🐈‍⬛
今日は俺ちゃんが美しいUIの作り方を勉強したから記事にしたんだぞ。

綺麗なUIいっぱい作っちゃったもんね。
なんで作ろうと思ったのかというと、今まで業務系アプリばかり作っていたので、一般ユーザーが使っているであろうToCアプリのデザインを作ってみたかったのです😭

ピンタレストというデザインが紹介されているサイトを参考に作ってみました。
https://jp.pinterest.com/

こちらが完成品です。ぜひぜひ参考にしてみてください。

Imperium Coffee Shop

コーヒーショップのアプリをイメージしたUIデザイン。画像まで用意できなかったので、絵文字で再現しております🙇

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

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFB7472A),
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 24.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // ヘッダー部分
              Padding(
                padding: const EdgeInsets.only(top: 32.0),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    // ロゴの代わりにアイコン
                    Row(
                      children: [
                        Icon(
                          Icons.coffee,
                          color: Colors.white,
                          size: 24,
                        ),
                        const SizedBox(width: 8),
                        Text(
                          'Imperium',
                          style: TextStyle(
                            color: Colors.white,
                            fontSize: 20,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                      ],
                    ),
                    // ステータスバーアイコン
                    Row(
                      children: [
                        Icon(
                          Icons.signal_cellular_alt,
                          color: Colors.white.withOpacity(0.7),
                          size: 16,
                        ),
                        const SizedBox(width: 8),
                        Icon(
                          Icons.wifi,
                          color: Colors.white.withOpacity(0.7),
                          size: 16,
                        ),
                        const SizedBox(width: 8),
                        Icon(
                          Icons.battery_full,
                          color: Colors.white.withOpacity(0.7),
                          size: 16,
                        ),
                      ],
                    ),
                  ],
                ),
              ),

              // メインコンテンツ
              Expanded(
                child: Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      // コーヒーカップの代わりにコーヒー絵文字
                      Container(
                        width: 200,
                        height: 200,
                        decoration: BoxDecoration(
                          color: Colors.white,
                          shape: BoxShape.circle,
                          boxShadow: [
                            BoxShadow(
                              color: Colors.black.withOpacity(0.1),
                              spreadRadius: 5,
                              blurRadius: 15,
                              offset: const Offset(0, 5),
                            ),
                          ],
                        ),
                        child: Center(
                          child: Text(
                            '☕️',
                            style: TextStyle(
                              fontSize: 80,
                            ),
                          ),
                        ),
                      ),
                      const SizedBox(height: 40),
                      const Text(
                        'The Best Coffee.',
                        style: TextStyle(
                          color: Colors.white,
                          fontSize: 28,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                      const SizedBox(height: 16),
                      Text(
                        'This shop is the perfect choice for anyone looking for style and comfort all in one. With a modern and elegant design, it has a premium...',
                        textAlign: TextAlign.center,
                        style: TextStyle(
                          color: Colors.white.withOpacity(0.8),
                          fontSize: 16,
                          height: 1.5,
                        ),
                      ),
                    ],
                  ),
                ),
              ),

              // 下部のボタン
              Padding(
                padding: const EdgeInsets.only(bottom: 48.0),
                child: Container(
                  width: double.infinity,
                  height: 56,
                  decoration: BoxDecoration(
                    gradient: LinearGradient(
                      colors: [
                        Colors.white.withOpacity(0.2),
                        Colors.white.withOpacity(0.1),
                      ],
                      begin: Alignment.centerLeft,
                      end: Alignment.centerRight,
                    ),
                    borderRadius: BorderRadius.circular(28),
                  ),
                  child: MaterialButton(
                    onPressed: () {
                      // ボタンのアクション
                    },
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(28),
                    ),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        const Text(
                          'Get started',
                          style: TextStyle(
                            color: Colors.white,
                            fontSize: 16,
                            fontWeight: FontWeight.w600,
                          ),
                        ),
                        const SizedBox(width: 8),
                        Icon(
                          Icons.arrow_forward,
                          color: Colors.white,
                          size: 20,
                        ),
                      ],
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
  1. ロゴの代替:
  • Icons.coffeeアイコンと'Imperium'テキストの組み合わせ
  1. コーヒーカップ画像の代替:
  • コーヒー絵文字(☕️)を使用
  • 白い円形のコンテナ内に配置
  • 適切なサイズとシャドウ効果を維持

使用したアイコン:

  • Icons.coffee - ロゴ用
  • Icons.signal_cellular_alt - 電波強度
  • Icons.wifi - Wi-Fi状態
  • Icons.battery_full - バッテリー状態
  • Icons.arrow_forward - ボタンの矢印

画像やアセットがない場合でも、マテリアルアイコンと絵文字を使うことで、見栄えの良いUIを実装できます。必要に応じて、アイコンのサイズや色、配置を調整できます。

実装のポイント:

  1. 絵文字のサイズ調整
  2. アイコンの色と透明度
  3. 適切な間隔(padding, margin)の設定
  4. グラデーションの効果

グリーンのグラデーションとサラダの絵文字を使用したUI

サラダバーのアプリでしょうか?。そんな感じのUIをイメージしてみました。

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

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      // グラデーション背景を実装
      body: Container(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            colors: [
              Color(0xFF90EE90), // ライトグリーン
              Color(0xFF32CD32), // ライムグリーン
            ],
          ),
        ),
        child: SafeArea(
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 24.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                // ステータスバー部分
                Padding(
                  padding: const EdgeInsets.only(top: 16.0),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Container(
                        width: 40,
                        height: 4,
                        decoration: BoxDecoration(
                          color: Colors.white,
                          borderRadius: BorderRadius.circular(2),
                        ),
                      ),
                    ],
                  ),
                ),

                // メインコンテンツ(中央配置)
                Expanded(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      // サラダの円形コンテナ
                      Container(
                        width: 250,
                        height: 250,
                        decoration: BoxDecoration(
                          shape: BoxShape.circle,
                          color: Colors.black,
                          boxShadow: [
                            BoxShadow(
                              color: Colors.black.withOpacity(0.2),
                              spreadRadius: 5,
                              blurRadius: 15,
                              offset: const Offset(0, 5),
                            ),
                          ],
                        ),
                        child: Center(
                          child: Text(
                            '🥗',
                            style: TextStyle(
                              fontSize: 100,
                            ),
                          ),
                        ),
                      ),
                      const SizedBox(height: 40),

                      // テキストコンテンツ
                      Text(
                        'Food suggestion based on\nyour preferences and\ncalories intake',
                        textAlign: TextAlign.center,
                        style: TextStyle(
                          color: Colors.white,
                          fontSize: 24,
                          fontWeight: FontWeight.bold,
                          height: 1.5,
                        ),
                      ),
                      const SizedBox(height: 16),
                      Text(
                        'Lorem ipsum dolor sit amet, consectetur',
                        textAlign: TextAlign.center,
                        style: TextStyle(
                          color: Colors.white.withOpacity(0.8),
                          fontSize: 16,
                        ),
                      ),

                      // 次へボタン
                      const SizedBox(height: 40),
                      Container(
                        width: 56,
                        height: 56,
                        decoration: BoxDecoration(
                          shape: BoxShape.circle,
                          color: Colors.white.withOpacity(0.2),
                        ),
                        child: IconButton(
                          icon: Icon(
                            Icons.arrow_forward,
                            color: Colors.white,
                          ),
                          onPressed: () {
                            // ボタンのアクション
                          },
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

使用している主なWidgetの解説:

  1. Container with BoxDecoration
Container(
  decoration: BoxDecoration(
    gradient: LinearGradient(...)
  ),
)
  • 背景のグラデーションを実現
  • beginとendで方向を指定
  • colorsで使用する色を指定
  1. SafeArea
SafeArea(
  child: ...
)
  • デバイスのノッチやステータスバーを考慮
  • コンテンツを安全な領域に配置
  1. Column
Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [...],
)
  • 縦方向にWidgetを配置
  • alignmentで配置位置を調整
  1. 円形コンテナ
Container(
  decoration: BoxDecoration(
    shape: BoxShape.circle,
    color: Colors.black,
    boxShadow: [...],
  ),
)
  • 円形の背景を作成
  • boxShadowで影を付加
  • 中央にサラダ絵文字を配置
  1. Text Widget
Text(
  'タイトル',
  style: TextStyle(
    color: Colors.white,
    fontSize: 24,
    fontWeight: FontWeight.bold,
  ),
)
  • テキストのスタイリング
  • フォントサイズや色を指定
  • 複数行のテキストをセンタリング
  1. IconButton
IconButton(
  icon: Icon(Icons.arrow_forward),
  onPressed: () {...},
)
  • タップ可能なアイコンボタン
  • onPressedで押下時の動作を定義

これらのWidgetを組み合わせることで、モダンでスタイリッシュなUIを実現しています。グラデーションや影、円形要素などを使用して深みのあるデザインを作成しています。

Orange Product Screen

オレンジの商品画像を表示するUIをイメージしてみました。スライドしませんが💦

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

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFF5F5F5),
      body: SafeArea(
        child: Column(
          children: [
            // 上部のヘッダー
            Padding(
              padding: const EdgeInsets.all(16.0),
              child: Row(
                children: [
                  Container(
                    decoration: BoxDecoration(
                      color: Colors.white,
                      borderRadius: BorderRadius.circular(20),
                    ),
                    child: IconButton(
                      icon: Icon(Icons.arrow_back),
                      onPressed: () {},
                    ),
                  ),
                ],
              ),
            ),

            // メイン画像エリア(白背景、下部が丸くカットされている)
            Expanded(
              child: Container(
                width: double.infinity,
                margin: const EdgeInsets.symmetric(horizontal: 16),
                decoration: BoxDecoration(
                  color: Colors.white,
                  borderRadius: BorderRadius.vertical(
                    bottom: Radius.circular(30),
                  ),
                ),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    // オレンジの絵文字
                    Text(
                      '🍊',
                      style: TextStyle(
                        fontSize: 120,
                      ),
                    ),
                    const SizedBox(height: 20),
                    // ドット型のページインジケーター
                    Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Container(
                          width: 8,
                          height: 8,
                          decoration: BoxDecoration(
                            color: Colors.black,
                            shape: BoxShape.circle,
                          ),
                        ),
                        const SizedBox(width: 8),
                        Container(
                          width: 8,
                          height: 8,
                          decoration: BoxDecoration(
                            color: Colors.grey[300],
                            shape: BoxShape.circle,
                          ),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ),

            // 商品情報エリア
            Padding(
              padding: const EdgeInsets.all(24.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text(
                    'Fresh Orange',
                    style: TextStyle(
                      fontSize: 24,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    'Product Description',
                    style: TextStyle(
                      fontSize: 16,
                      color: Colors.grey[600],
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}
  1. ベースとなる構造
Scaffold(
  backgroundColor: const Color(0xFFF5F5F5),
  body: SafeArea(...)
)
  • Scaffold: アプリの基本的な視覚構造を提供
  • backgroundColor: 薄いグレー(#F5F5F5)を背景色に設定
  • SafeArea: ノッチやステータスバーを避けて安全な領域にコンテンツを配置
  1. 最上部のヘッダー部分
Padding(
  padding: const EdgeInsets.all(16.0),
  child: Row(
    children: [
      Container(
        decoration: BoxDecoration(...),
        child: IconButton(...)
      ),
    ],
  ),
)
  • Padding: 全方向に16.0の余白を追加
  • Row: 子要素を水平方向に配置
  • Container + BoxDecoration: 白背景の丸い形状を作成
  • IconButton: 戻るボタンの実装
  1. メインの画像エリア
Expanded(
  child: Container(
    width: double.infinity,
    margin: const EdgeInsets.symmetric(horizontal: 16),
    decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.vertical(
        bottom: Radius.circular(30),
      ),
    ),
    child: Column(...),
  ),
)
  • Expanded: 利用可能な縦方向のスペースを最大限に使用
  • Container: 幅いっぱい(double.infinity)で白背景のエリアを作成
  • margin: 左右に16ポイントの余白
  • BorderRadius.vertical: 下部のみ30の角丸を適用
  1. 商品画像とインジケーター
Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    Text('🍊', style: TextStyle(fontSize: 120)),
    const SizedBox(height: 20),
    Row(...) // インジケーター
  ],
)
  • Column: 子要素を垂直方向に中央揃えで配置
  • Text: 大きなサイズ(120)のオレンジ絵文字
  • SizedBox: 20ポイントの縦方向スペース
  • Row: インジケーターのドットを水平に配置
  1. 下部の商品情報エリア
Padding(
  padding: const EdgeInsets.all(24.0),
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text('Fresh Orange', ...),
      const SizedBox(height: 8),
      Text('Product Description', ...),
    ],
  ),
)
  • Padding: 全方向に24.0の余白
  • Column: 左揃え(crossAxisAlignment.start)で垂直配置
  • Text: 商品名(太字、24サイズ)と説明文(グレー、16サイズ)

このデザインは、明確な視覚的階層と適切な余白によって、ユーザーフレンドリーなインターフェースを実現しています。

HabitOnboardingScreen

立体的な切り抜いたようなUIを重ねたUIデザインをイメージしてみました。
作るにはCustomPainterとClipPathを使います。美しいデザインは作る難易度が高い💦

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

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          // 下部のピンク部分
          Container(
            color: Color(0xFFFFC0CB), // ピンク色
          ),

          // 上部の黒い部分(波形の形状)
          ClipPath(
            clipper: WaveClipper(),
            child: Container(
              height: MediaQuery.of(context).size.height * 0.65, // 高さを65%に修正
              width: double.infinity, // 幅を最大に
              color: Colors.black,
              padding: EdgeInsets.fromLTRB(24, 60, 24, 24),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  SizedBox(
                      height:
                          MediaQuery.of(context).padding.top), // ステータスバーの高さを考慮
                  Text(
                    'We will help you to',
                    style: TextStyle(
                      color: Colors.white,
                      fontSize: 24,
                    ),
                  ),
                  Text(
                    'make habits!',
                    style: TextStyle(
                      color: Colors.white,
                      fontSize: 32,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ],
              ),
            ),
          ),

          // 中央の自転車アイコン
          Positioned(
            top: MediaQuery.of(context).size.height * 0.45, // 位置を下に調整
            left: 0,
            right: 0,
            child: Center(
              child: Text(
                '🚲',
                style: TextStyle(
                  fontSize: 100,
                ),
              ),
            ),
          ),

          // 下部のテキストとボタン
          Positioned(
            bottom: 40, // 下からの距離を調整
            left: 24,
            right: 24,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  'This application helps you get rid of bad habits and making good ones in more a better life',
                  style: TextStyle(
                    color: Colors.black87,
                    fontSize: 16,
                    height: 1.5,
                  ),
                ),
                SizedBox(height: 40),
                // 円形の次へボタン
                Align(
                  alignment: Alignment.centerRight,
                  child: Container(
                    width: 60,
                    height: 60,
                    decoration: BoxDecoration(
                      color: Colors.black,
                      shape: BoxShape.circle,
                    ),
                    child: Icon(
                      Icons.arrow_forward,
                      color: Colors.white,
                    ),
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

// 修正された波形のクリッパー
class WaveClipper extends CustomClipper<Path> {
  
  Path getClip(Size size) {
    var path = Path();
    path.lineTo(0, size.height * 0.85); // 開始位置を下に

    // よりなだらかな曲線を描く
    var firstControlPoint = Offset(size.width * 0.25, size.height);
    var firstEndPoint = Offset(size.width * 0.5, size.height * 0.85);
    path.quadraticBezierTo(
      firstControlPoint.dx,
      firstControlPoint.dy,
      firstEndPoint.dx,
      firstEndPoint.dy,
    );

    var secondControlPoint = Offset(size.width * 0.75, size.height * 0.7);
    var secondEndPoint = Offset(size.width, size.height * 0.85);
    path.quadraticBezierTo(
      secondControlPoint.dx,
      secondControlPoint.dy,
      secondEndPoint.dx,
      secondEndPoint.dy,
    );

    path.lineTo(size.width, 0); // 右端
    path.close();
    return path;
  }

  
  bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}
  1. 基本構造: ScaffoldStack
Scaffold(
  body: Stack(
    children: [...],
  ),
)
  • Scaffold: マテリアルデザインの基本的な画面構造を提供
  • Stack: 複数のWidgetを重ね合わせて配置。子Widgetを順番に重ねていく
  1. 背景レイヤー: Container
Container(
  color: Color(0xFFFFC0CB), // ピンク色
)
  • 画面全体のピンク色の背景を設定
  • Colorには16進数のカラーコードを指定
  1. 波形の黒いエリア: ClipPath
ClipPath(
  clipper: WaveClipper(),
  child: Container(
    height: MediaQuery.of(context).size.height * 0.65,
    color: Colors.black,
    // ...
  ),
)
  • ClipPath: カスタムな形状でコンテンツを切り抜く
  • WaveClipper: カスタムの波形を定義するクラス
  • MediaQuery: 画面サイズに応じた相対的なサイズ設定
  1. 位置指定: Positioned
Positioned(
  top: MediaQuery.of(context).size.height * 0.45,
  left: 0,
  right: 0,
  child: Widget,
)
  • Stack内での要素の絶対位置を指定
  • top, left, right, bottomで位置を制御
  1. テキストレイアウト: Column
Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Text(...),
    Text(...),
  ],
)
  • 垂直方向に要素を配置
  • crossAxisAlignment: 水平方向の配置を制御
  1. ボタンデザイン: ContainerBoxDecoration
Container(
  width: 60,
  height: 60,
  decoration: BoxDecoration(
    color: Colors.black,
    shape: BoxShape.circle,
  ),
  child: Icon(...),
)
  • 円形のボタンを作成
  • BoxDecoration: 形状やスタイルを定義
  1. 余白の制御: SizedBoxPadding
SizedBox(height: 40)
Padding(
  padding: EdgeInsets.all(24),
  child: Widget,
)
  • SizedBox: 特定のサイズの空白を作成
  • Padding: ウィジェットの周りに余白を追加
  1. アライメント制御: Align
Align(
  alignment: Alignment.centerRight,
  child: Widget,
)
  • 子Widgetの配置位置を制御
  • centerRightで右中央に配置

これらのWidgetを適切に組み合わせることで、複雑なレイアウトと視覚効果を実現しています。特にStackClipPathの組み合わせが、独特な波形デザインの実現に重要な役割を果たしています。

最後に

今回作成したデザインを作成するのに必要なWidgetの知識をまとめました。外国のサンプルコードを参考にしたりしましがそれだけだとうまくいかないこともあり最近流行りの生成AIも活用しました。

でもたまに生成AIは情報漏洩するから使用禁止の職場あるんですよね😇
VS Codeもインストール禁止とか?。どうやって仕事するんだろうか。。。

👹「生成AIは情報漏洩するので使用禁止です」
🐈「うるせーよ🧑‍🦲レガシーなんよ💢作れんのかい?」
👹「あっ🌸エディターでは開発できないですね💦申請してきます🫡」
🐈「俺ちゃんマーク導入してやるよ。プライバシーマーク知らんわんなもん!」
👹「すいませーんCursorとWindserf Editor申請しておきました😭」

  1. 基本的なレイアウト知識

主要なWidgetとその用途:

  1. レイアウト関連Widget
  1. コンテナとデコレーション
  1. カスタム形状
  1. 配置とアライメント
  1. スペーシング
  1. レスポンシブ対応
  1. カスタムペイント

学習のステップ:

  1. 基本レイアウト
// 基本的なレイアウト構造の理解
Scaffold(
  body: Stack(
    children: [
      // 背景
      // コンテンツ
    ],
  ),
)
  1. カスタム形状
// CustomClipperの実装
class WaveClipper extends CustomClipper<Path> {
  
  Path getClip(Size size) {
    // Path APIを使用した形状の定義
  }
}
  1. レスポンシブ対応
// 画面サイズに応じた調整
MediaQuery.of(context).size.height * 0.65

参考になる追加リソース:

  1. Flutter公式チュートリアル: https://docs.flutter.dev/get-started/codelab
  2. Flutter UI Challenges: https://flutter.dev/docs/development/ui/widgets/cupertino
  3. Material Design Guidelines: https://m3.material.io/develop/flutter

これらの知識を組み合わせることで、このようなカスタムデザインのUIを実装できます。

Discussion