🚶

歩数計アプリのカードデザインを再現してみる 

に公開

綺麗なカードデザインをするには?

普段は単純なデザインのアプリしか作ってないのでレイアウト苦手です😇
Flutter Webの案件をやったときはレイアウトするには多くのWidgetを使います。でも普段はモバイルです。

ヘルケアアプリでよく見るカードデザインを再現するのをやってみようと思います。意外と知らない使い方が多かった👀

Step Counter Card

この歩数計カードのUIを実装するために必要なWidgetと知識を説明します。

example

カードのコンポーネント

import 'package:flutter/material.dart';

class StepCounterCard extends StatelessWidget {
  const StepCounterCard({
    super.key,
    required this.currentSteps,
    required this.goalSteps,
    required this.message,
  });

  final int currentSteps;
  final int goalSteps;
  final String message;

  
  Widget build(BuildContext context) {
    return Card(
      color: Colors.white,
      margin: const EdgeInsets.all(16),
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16),
      ),
      elevation: 4,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: [
            // ヘッダー部分
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                const Text(
                  '今日の歩数',
                  style: TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                IconButton(
                  icon: const Icon(Icons.info_outline),
                  onPressed: () {}, // 情報ボタンの処理
                ),
              ],
            ),

            // 歩数表示
            Row(
              crossAxisAlignment: CrossAxisAlignment.baseline,
              textBaseline: TextBaseline.alphabetic,
              children: [
                Text(
                  '$currentSteps',
                  style: const TextStyle(
                    fontSize: 32,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const Text(
                  ' 歩',
                  style: TextStyle(
                    fontSize: 16,
                  ),
                ),
              ],
            ),

            const SizedBox(height: 16),

            // プログレスバー
            LinearProgressIndicator(
              value: currentSteps / goalSteps,
              backgroundColor: Colors.grey[200],
              valueColor: const AlwaysStoppedAnimation<Color>(Colors.blue),
              minHeight: 8,
            ),

            // 目標値表示
            Padding(
              padding: const EdgeInsets.only(top: 8),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  const Text('0 歩'),
                  Text('${goalSteps ~/ 2} 歩'),
                  Text('$goalSteps 歩'),
                ],
              ),
            ),

            // メッセージ
            Padding(
              padding: const EdgeInsets.only(top: 16),
              child: Text(
                message,
                style: const TextStyle(
                  fontSize: 14,
                  color: Colors.grey,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

main.dartで表示する例)

main.dart
import 'package:flutter/material.dart';
import 'package:widget_cookbook_demo/example/step_counter_card/step_counter_card.dart';

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

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyWidget(),
    );
  }
}

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: StepCounterCard(
          currentSteps: 3500,
          goalSteps: 8000,
          message: '3500歩きました。',
        ),
      ),
    );
  }
}

必要なWidgetと知識を説明します:

  1. 基本レイアウトWidget
  • Card: 影付きのカードデザイン
  • Column: 縦方向のレイアウト
  • Row: 横方向のレイアウト
  • Padding: 内側の余白
  • SizedBox: スペース確保
  1. 軸設定(Main/Cross Axis)
// 横方向の配置
mainAxisAlignment: MainAxisAlignment.spaceBetween,
// 縦方向の配置
crossAxisAlignment: CrossAxisAlignment.start,
// サイズ調整
mainAxisSize: MainAxisSize.min,
  1. 進捗バーの実装
LinearProgressIndicator(
  value: currentSteps / goalSteps,  // 0.0 ~ 1.0の値
  backgroundColor: Colors.grey[200],
  valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
  minHeight: 8,
)
  1. スタイリング
  • Cardshapeでカードの角丸設定
  • elevationで影の強さ設定
  • TextStyleでテキストスタイリング
  • BorderRadiusで角の丸みの制御
  1. レイアウトの調整
// マージン
margin: EdgeInsets.all(16)
// パディング
padding: EdgeInsets.all(16)
// 特定方向のみのパディング
padding: EdgeInsets.only(top: 16)
  1. Flexibleは不要
    このUIではFlexibleExpandedは必要ありません。なぜなら:
  • カードは固定サイズで十分
  • 内部のコンテンツは自然なサイズで配置
  • スクロールが不要
  1. その他の重要な知識
  • テキストのベースライン揃え
  • アイコンボタンの配置
  • プログレスバーの値の計算
  • 条件付きスタイリング

使用例:

StepCounterCard(
  currentSteps: 3500,
  goalSteps: 8000,
  message: '3500歩きました。',
)

このUIを改良する方法:

  1. アニメーション追加
  2. カスタムテーマ対応
  3. ダークモード対応
  4. アクセシビリティ対応
  5. レスポンシブ対応

必要なパッケージ:

  • 特別なパッケージは不要(Flutter標準Widgetのみで実装可能)

このUIは標準的なFlutterウィジェットの組み合わせで実装でき、Flexibleは必要ありません。代わりに、適切なMainAxisAlignmentCrossAxisAlignmentの設定で目的のレイアウトを実現できます。

Discussion