Operations2026.08.23 · 5 min read

Capacitor から Expo に移したら、アプリアイコンのドット絵が iOS では小さく Android では大きくなった

NCP認証基盤・クラウドインフラ

Capacitor で書いた勤怠アプリ(1.x)を Expo / React Native で作り直し(2.0.0)、同じバンドル ID のアップデートとして出す準備をしている。端末に入れて並べると、ホーム画面のアイコンのドット絵が、iOS では 1.x より小さく、Android では大きくなっていた。原因は移行時に「内側 6 割に収める」という一般論の比率で素材を焼き直したことで、1.x が実際に出荷したアイコンを測ると iOS は 70.3%、Android の前景は 37.5% だった。

iOS では小さく、Android では大きくなっていた

同じバンドル ID で届くアップデートなので、アイコンが変わるとユーザーには別のアプリが入ったように見える。絵柄は同じままだったから、違うのは大きさだけのはずだ。「少し小さい気がする」で止めると直したあとに直ったのか分からないので、先に測った。

測り方は ImageMagick の -trim で背景を落とし、残ったドット絵の矩形の幅と位置を出すだけでいい。1.x 側は生成スクリプトの出力ではなく、Assets.xcassets/AppIcon.appiconset mipmap-xxxhdpi に実際に置かれていたファイルを測った。ストアに出ていたのはそれだからだ。

ドット絵の矩形を測る
$ magick <file> -trim \
    -format '%wx%h+%X+%Y' info:

# 1.x(ストアに出ていたもの)
iOS AppIcon 1024px
  ドット絵 720x450    70.3%
Android 前景 432px (xxxhdpi)
  ドット絵 162x102    37.5%

# 2.0.0 直す前の素材(1024px)
icon.png
  ドット絵 635x397    62%
adaptive-icon.png
  ドット絵 563x352    55%

iOS は 1024px の中で 720px、つまり 70.3% あったドット絵が 62% に縮んでいた。Android の adaptive icon の前景は 432px の中で 162px、37.5% だったものが 55% に膨らんでいた。片方が縮んで片方が膨らんでいる。

一般論の比率で焼いたら、ずれが逆向きに出た

2.0.0 側の素材は、透過のドット絵だけを切り出した画像を目標サイズへ縮め、正方形の中央に置くスクリプトで焼いていた。そのときの commit message にはこうある。「アイコンの素材はドット絵が中心から外れていて、adaptive icon の丸い切り抜きで偏っていた。ドット絵を中央に置き、内側 6 割に収める」。iOS は 0.62、Android は 0.55 という比率が、そこで決まっている。

どちらの数字も 1.x を測って出したものではない。「丸く切り抜かれるから内側に寄せる」という話はどのプラットフォームのガイドにも書いてあり、それ自体は間違っていない。ただし今回の目標は「安全域に収める」ことではなく「1.x と同じに見える」ことで、その答えは 1.x の出荷物の中にしか無かった。

既存アプリのアップデートでアイコンを焼き直すなら、比率はガイドの安全域ではなく、出荷済みのバイナリ(mipmap / appiconset)を測って逆算する。

1.x のスクリプトを読んでも、出荷した比率は分からなかった

「1.x のスクリプトの比率をそのまま持ってくればよかったのでは」と思うが、それでは足りなかった。1.x のアイコン生成スクリプトは Android の前景を比率 0.42 で描いているが、これは 26 列のスプライト(うち見える 24 列)を 512px のキャンバスに整数倍で置いたときの値で、見えるドット絵の幅に直すと 37.5% になる。スクリプトに書いてある数字と、見える大きさは一致していない。

iOS はもっと遠い。1.x のストア版アイコンはそのスクリプトが焼いたものではなく、別に用意した 1024px の原本から @capacitor/assets が生成していた。スクリプトのコメントにも「ストア版の AppIcon は assets/ の原本から @capacitor/assets が焼いたもので、こちらとはドット絵の配置がわずかに違う」とある。つまり 1.x 側のコードをどれだけ読んでも iOS の 70.3% は出てこない。appiconset を測る以外に知る方法が無かった。

もう1つ、同じ「7 割」でも iOS と Android では見える大きさが違う。adaptive icon の 108dp のキャンバスは、ランチャーに表示されるのが中央の 72dp で、さらに安全域は 66dp だ。1.x が iOS 70.3% / Android 37.5% という一見ちぐはぐな組み合わせで出荷していたのはこの差があるからで、両方を測って初めて分かった。

1 ドット = 1px に採り直してから整数倍にする

iOS は簡単で、1.x の原本をそのまま assets/icon.png にコピーした。Expo の prebuild は iOS のアイコンを 1024px 一枚だけ生成し、1024 から 1024 なので実質素通しになる。コピー元の原本は compare -metric AE で 1.x の AppIcon と 0 ピクセル差だった。dev 版は同じファイルの背景色だけを置き換える(ドット絵のパレットに背景の黄色は無い)。

Android は比率の換算が要る。1.x の前景の 37.5% を Expo が要求する 1024px の素材に当てると 384px で、ドット絵は 24 ドットだから 384 = 24 × 16、ちょうど 1 ドット 16px になる。ここが整数で割り切れたので、やることは「ドット絵を 1 ドット = 1px の 24 × 15 に採り直し、それを 16 倍にして透過の 1024px 中央に置く」に決まった。

scripts/make-app-assets.mjs
// 1.x のアイコンのドット絵は 24×15 ドット。
// 1024px では iOS が 30px/ドット、Android の前景が 16px/ドット
const SPRITE_DOTS = { width: 24, height: 15 }
const ANDROID_DOT = 16

// iOS。prod は 1.x の App Store アイコンをそのまま使う
await copyFile(storeIcon, iosIcon)

// Android の adaptive icon の前景。
// ドット絵を 1 ドット = 1px に採り直してから 16 倍にし、透過の 1024px 中央に置く
magick([
  storeIcon,
  '-trim', '+repage',
  '-filter', 'point', '-resize', `${SPRITE_DOTS.width}x${SPRITE_DOTS.height}!`,
  '-filter', 'point', '-resize', `${spriteWidth}x${spriteHeight}!`,
  '-transparent', ICON_BACKGROUND.prod,
  '-background', 'none', '-gravity', 'center', '-extent', '1024x1024',
  adaptiveIcon,
])

捨てた案が1つある。直す前の素材の元になっていた透過のドット絵画像(ドット絵の幅 624px)から 384px へ直接 resize する、というものだ。624 / 384 = 1.625 倍で整数にならず、nearest-neighbor でもドットの幅が不揃いになる。直す前の素材も 635px を 24 ドットで割ると 26.46px/ドットで、同じ理由ですでに不揃いだった。いったん 1 ドット = 1px に落としてから拡大すれば、倍率は常に整数になる。Expo 側が 1024 を 432 へ縮める段階でどうせ非整数になるが、素材の段階では整数倍に保っておく。

検証は素材の矩形だけでなく、Expo が実際に焼く生成物まで見た。

prebuild の生成物を測る
$ APP_ENV=prod npx expo prebuild \
    --platform android \
    --no-install --clean

# xxxhdpi/ic_launcher_foreground.webp
432x432  ドット絵 162x102 +135+165
# 1.x の前景 (432px) と同じ

xxxhdpi の前景は 1.x と同じ 162x102、位置も同じだった。iOS 側の App-Icon-1024x1024@1x.png も 720x450 で 1.x と一致している。 ただし残る非一致が1つある。Expo は density 別の mipmap を @expo/image-utils(sharp)で縮めるので、1.x の point 縮小よりドットの縁がわずかに滑らかになる。legacy の ic_launcher は 1.x の 135x84 に対して 136x85 と、アンチエイリアスが 1px 乗っていた。大きさは一致しているが、ピクセル単位の完全一致は iOS の 1024px だけだ。

アイコンはネイティブ資産なので、ここまで測っても実機には APK / IPA を焼き直すまで反映されない。焼き直した APK から取り出した xxxhdpi の前景も 162x102 @ +135+165 で 1.x と同じだった。最後に iOS / Android それぞれの実機で、1.x のアイコンの隣に並べて見比べ、大きさが一致していることを確認して終えた。

まとめ

「内側 6 割」「安全域」のような比率は、新規アプリなら十分な答えになる。既存アプリのアップデートでは、目標が「ガイドに収まる」から「前と同じに見える」に変わっているので、答えは出荷済みのバイナリの中にしかない。しかもその値は生成スクリプトに書いてある数字とは一致しないことがある。iOS 70.3% / Android 37.5% は、両方の出荷物を測って初めて出てきた。

ドット絵を扱うときは、切り出した画像を目標幅へ直接 resize しない。いったん 1 ドット = 1px に採り直してから整数倍にすれば、どの目標サイズでもドットの幅が揃う。そして検証は素材で止めず、ビルドツールが実際に焼く生成物まで測る。prebuild の出力を測らなければ、sharp の縮小で縁が変わることにも気づかなかった。

ExpoReact NativeCapacitorImageMagickAndroidiOS