UPSIDER Tech Blog

Flutter で実現するカード券面の描画

こんにちは。

今回は UPSIDER でモバイルアプリの開発を担当している app team から、UPSIDER / PRESIDENT CARD アプリにおいて、カード券面をどのように描画しているのかについて紹介します。

カード券面の描画は、一見すると単なる UI 表示のように見えますが、どのデバイスで見ても同じカードとして認識できることが求められる、少し特殊な領域です。

ただし、カード上に表示されるすべての情報が、同じ粒度で“一体として描画されるべき”というわけではありません。

どこまでをカードとして扱い、どこからを通常の UI として扱うのか。その線引きが、設計上の重要なポイントになります。

本記事では、その考え方と、UPSIDER における具体的な実装について解説します。

カード券面の描画における前提

まず前提として、UPSIDER / PRESIDENT CARD の券面には、以下のような要素があります。

  • 複数種別のカード券面
  • 利用可能額
  • カード名
  • カードステータス(カードがロック中の表示など)

ここで重要なのは、すべてを画像のように描画する必要はないという点です。

カード自体の形状や背景、ロゴ、丸みといった要素は、どのデバイスで見ても同じ見た目であるべきです。これらは「カードという物理的な存在」を表現する要素だからです。

一方で、利用可能額などの情報は事情が異なります。

利用可能額は、ユーザーが日常的に確認する情報であり、デバイスサイズやユーザーの文字サイズ設定などに応じて、読みやすい形で表示されるべき情報です。

そのため、UPSIDER では以下のように役割を分けています。

  • カードの丸み、背景、ロゴなど
    • 一体として描画されるカード券面
  • 利用可能額などの情報
    • 通常の UI として、可読性を優先して描画

この切り分けを前提として、以降の実装を考えていきます。

カード券面自体の描画

カード券面のグラデーションなどが比較的シンプルな場合、背景を Canvas などで描画し、VISA ロゴなどの各種アイコンを個別に配置する方法があります。

この方法のメリットは以下の通りです。

  • リソースを最小限に抑えられ、バイナリサイズを削減できる
  • 一度作ってしまえば、カード情報の描画範囲を指定しやすい

一方で、デメリットも存在します。

  • 各要素の配置がやや複雑になる
  • 背景の描画ロジックが必要になる
  • 券面の種類が増えた際に、ロゴ配置などエンジニア側の実装工数が一定発生する

UPSIDER でも以前はこの方式を採用していました。しかし、カード券面の種類が増えたり、描画ロジックが徐々に複雑化してきたことから、現在は別の方法へと移行しています。

現在採用しているのは、ロゴなども含めたカード券面全体を画像として保持する方法です。この方法の最大のメリットは、

  • 券面が増えた場合でも、画像を追加・差し替えるだけで対応できる

という点にあります。

ただし、ロゴの配置などをあらかじめ前提として描画位置を決める必要があり、画像変更時に注意が必要なため、描画範囲がより細かく分かれていたり、券面自体ではなくロゴやパーツが状態によって切り替わるようなケースであれば、前者の方が適しているでしょう。

描画範囲の考え方と実装上の工夫

カード券面の上に情報を配置するためには、「どこに描画してよいか」という描画領域を定義する必要があります。

UPSIDER では、描画領域を考える際にカード中央部や VISA ロゴ周辺を意識していますが、それらを単純に複数の矩形として分割する実装にはしていません。

実際の描画領域の考え方は、以下のようになります。

見ると分かる通り、

  • カード中央部から VISA ロゴ部分までを含めた、ひとつの大きな矩形
  • VISA ロゴが存在するスペースを判断するための Spacer Widget

この二つを組み合わせて描画を行っています。

VISA ロゴに被る形の矩形を定義しロゴ自体の領域は Spacer で制御することで、

  • 描画範囲の考え方がシンプルになる
  • Widget の使い方が直感的になる
  • 特殊な矩形や複雑な条件分岐が不要になる

といったメリットがあると考えています。

複数の描画領域を細かく管理するよりも、「大きな描画領域 + 空白を表現する Widget」という構成の方が、実装・保守の両面で扱いやすい設計になっています。

カード情報をどう描画するか

カード券面そのものは一体として描画されますが、その上に重ねる情報の配置には、FractionallySizedBox を活用しています。

FractionallySizedBox を使うことで、カード全体のサイズに対する比率で描画領域を定義できます。これにより、画面サイズが変わっても、カードと情報の位置関係を保つことができます。以下は、描画範囲を定義するためのウィジェットです。

class _FractionallyBox extends StatelessWidget {
  const _FractionallyBox({
    required this.translation,
    required this.widthFactor,
    required this.heightFactor,
    required this.child,
  });

  final Offset translation;
  final double widthFactor;
  final double heightFactor;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return FractionalTranslation(
      translation: translation,
      child: FractionallySizedBox(
        widthFactor: widthFactor,
        heightFactor: heightFactor,
        alignment: Alignment.topLeft,
        child: child,
      ),
    );
  }
}

Alignment.center での配置では位置調整が難しいため、FractionalTranslation を併用して微調整します。ロゴの位置などを基準に translation、widthFactor、heightFactor を算出し、カード情報を描画可能なエリアとして定義します。

カード全体の構成は以下のようになります。

class _Card extends StatelessWidget {
  const _Card();

  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: onTap,
      borderRadius: corderRadius,
      child: Ink(
        decoration: _Decoration(
          image: image, // カード画像
          border: border,
          shadow: shadow,
          radius: radius,
        ),
        child: Stack(
          children: [
            _FractionallyBox( // 描画可能エリア
              translation: translation,
              widthFactor: widthFactor,
              heightFactor: heightFactor,
              child: child, // カード情報
            ),
            overlay, // カードステータス
          ],
        ),
      ),
    );
  }
}

イメージとしては、カード画像の上に FractionallySizedBox で定義した領域を重ねる形です。overlay は、カードの上に重ねて表示する要素に使用しています。

また、_Decoration は独自実装ですが、BoxDecoration とほぼ同等です。内部でカードの幅や高さに応じて radius を計算しており、どのデバイスでも一貫した角の丸みを表現しています。

まとめ

カード券面の描画は、カード会社に限らず、ポイントカードなどを扱う多くのサービスに共通する課題です。

私自身も複数の FinTech での開発経験を通じて、Android などでも同じようなカード券面の描画実装を行ってきましたが、「どれだけ美しく、かつ保守しやすく実装できるか」という点に、この領域の面白さがあります。

個人的にも好きな分野の一つなので、もし機会があればぜひ挑戦してみてほしいテーマです。

We Are Hiring !!

UPSIDERでは現在積極採用をしています。 ぜひお気軽にご応募ください。

herp.careers

herp.careers

UPSIDER Engineering Deckはこちら📣

speakerdeck.com