fix: Bug - Android app is zoomed in

Scale launcher and splash brand assets to the adaptive-icon safe zone and a smaller splash ratio so circular launcher masks and the launch splash no longer crop the cat face.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-11 21:20:01 +02:00
parent bdea95511d
commit a01abbb1bf
31 changed files with 89 additions and 14 deletions

View File

@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import sharp from 'sharp';
import {
describe,
expect,
@@ -9,13 +10,16 @@ import {
} from 'vitest';
import {
ADAPTIVE_FOREGROUND_ICON_RATIO,
BRAND_LAUNCHER_BACKGROUND_COLOR,
findMissingLauncherResources,
findStockCapacitorResources,
isBrandLauncherBackgroundColor,
readAdaptiveIconBackgroundColor,
REQUIRED_LAUNCHER_ICON_FILES,
REQUIRED_SPLASH_FILES
REQUIRED_SPLASH_FILES,
resolveIconPixelSize,
SPLASH_ICON_RATIO
} from './mobile-android-launcher-icon.rules';
const RES_DIR = resolve(process.cwd(), 'android/app/src/main/res');
@@ -31,6 +35,12 @@ describe('mobile-android-launcher-icon.rules', () => {
const allRequired = [...REQUIRED_LAUNCHER_ICON_FILES, ...REQUIRED_SPLASH_FILES];
const presentFiles = allRequired.filter((file) => existsSync(resolve(RES_DIR, file)));
it('keeps the brand mark inside the adaptive-icon safe zone', () => {
expect(ADAPTIVE_FOREGROUND_ICON_RATIO).toBeCloseTo(66 / 108, 5);
expect(resolveIconPixelSize(432)).toBe(264);
expect(SPLASH_ICON_RATIO).toBeLessThan(0.4);
});
it('ships a launcher icon and splash for every required density', () => {
expect(findMissingLauncherResources(presentFiles)).toEqual([]);
});
@@ -49,4 +59,15 @@ describe('mobile-android-launcher-icon.rules', () => {
expect(isBrandLauncherBackgroundColor(color)).toBe(true);
expect(color?.toLowerCase()).toBe(BRAND_LAUNCHER_BACKGROUND_COLOR.toLowerCase());
});
it('insets the adaptive foreground so launcher masks do not clip the cat face', async () => {
const foregroundPath = resolve(RES_DIR, 'mipmap-xxxhdpi/ic_launcher_foreground.png');
const { data, info } = await sharp(foregroundPath).ensureAlpha()
.raw()
.toBuffer({ resolveWithObject: true });
const topCenterOffset = (0 * info.width + Math.floor(info.width / 2)) * info.channels;
const topCenterAlpha = data[topCenterOffset + 3];
expect(topCenterAlpha).toBeLessThan(32);
});
});

View File

@@ -8,6 +8,26 @@
/** Brand purple sampled from `images/icon-new-rounded.png` (the circle behind the cat). */
export const BRAND_LAUNCHER_BACKGROUND_COLOR = '#4A217A';
/** Adaptive-icon foreground canvas size in dp (Android spec). */
export const ADAPTIVE_ICON_CANVAS_DP = 108;
/** Visible safe-zone diameter inside the adaptive-icon foreground layer (Android spec). */
export const ADAPTIVE_ICON_SAFE_ZONE_DP = 66;
/** Scale the brand mark to fit inside the adaptive-icon safe zone instead of full-bleed. */
export const ADAPTIVE_FOREGROUND_ICON_RATIO = ADAPTIVE_ICON_SAFE_ZONE_DP / ADAPTIVE_ICON_CANVAS_DP;
/** Legacy square/round launcher bitmaps use the same inset so circular masks do not clip the cat. */
export const LEGACY_LAUNCHER_ICON_RATIO = ADAPTIVE_FOREGROUND_ICON_RATIO;
/** Splash art keeps the brand mark smaller than the screen so ears and cheeks stay visible. */
export const SPLASH_ICON_RATIO = 0.32;
/** Return the brand icon pixel size for a square canvas at the given scale ratio. */
export function resolveIconPixelSize(canvasPx: number, ratio: number = ADAPTIVE_FOREGROUND_ICON_RATIO): number {
return Math.round(canvasPx * ratio);
}
/** Density buckets Android resolves launcher icons from. */
export const ANDROID_ICON_DENSITIES = [
'mdpi',