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>
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 237 KiB After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 233 KiB After Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 12 KiB |
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||