refactor: simplify icon assets and improve keystore resolution

- Remove separate icon_foreground.png, reuse icon.png for adaptive icon foreground
- Enhance resolveKeystoreFile() with root-relative → module-relative fallback
- Update documentation to reflect icon consolidation
- Update test bootstrap IP to 192.168.2.57
This commit is contained in:
Max 2026-03-23 17:53:53 +08:00
parent 0a17e1c4f7
commit 318c829ced
27 changed files with 93 additions and 36 deletions

View File

@ -33,6 +33,10 @@ web_android_shell/
└── architecture.md # 架构说明 └── architecture.md # 架构说明
``` ```
## 执行指令
```text
dart run tool/generate_app.dart aixue
```
## 核心思路 ## 核心思路
- 应用启动优先读取本地默认启动配置 `assets/config/bootstrap.json` - 应用启动优先读取本地默认启动配置 `assets/config/bootstrap.json`
@ -130,7 +134,6 @@ theme:
branding: branding:
icon: "icon.png" icon: "icon.png"
icon_background: "#FFFFFF" icon_background: "#FFFFFF"
icon_foreground: "icon_foreground.png"
splash: "splash.png" splash: "splash.png"
splash_color: "#FFFFFF" splash_color: "#FFFFFF"
``` ```
@ -145,6 +148,7 @@ branding:
- `portraitDown` - `portraitDown`
- `landscapeLeft` - `landscapeLeft`
- `landscapeRight` - `landscapeRight`
- `branding.icon`:同时用于普通应用图标和 Android 自适应图标前景
## 生成后的入口形态 ## 生成后的入口形态
@ -187,8 +191,7 @@ runShellApp(
| 资源 | 文件名 | 最小尺寸 | 格式 | 说明 | | 资源 | 文件名 | 最小尺寸 | 格式 | 说明 |
|---|---|---|---|---| |---|---|---|---|---|
| 应用图标 | `icon.png` | 1024×1024 | PNG | 用于生成各尺寸 launcher icon | | 应用图标 | `icon.png` | 1024×1024 | PNG | 同时用于生成各尺寸 launcher icon 与 Android 自适应图标前景 |
| 自适应图标前景 | `icon_foreground.png` | 1024×1024 | PNG | 建议透明背景,主体留安全区 |
| 启动页图片 | `splash.png` | 1152×1152 | PNG | 用于生成 Android 12+ 与旧版启动页 | | 启动页图片 | `splash.png` | 1152×1152 | PNG | 用于生成 Android 12+ 与旧版启动页 |
品牌资源源文件放在 `flavors/<品牌名>/` 下,生成后会复制到 `apps/<品牌名>/assets/branding/` 品牌资源源文件放在 `flavors/<品牌名>/` 下,生成后会复制到 `apps/<品牌名>/assets/branding/`

View File

@ -30,7 +30,21 @@ fun resolveKeystoreFile(rawPath: String?): File {
val normalized = expandedHome.replace('\\', File.separatorChar).replace('/', File.separatorChar) val normalized = expandedHome.replace('\\', File.separatorChar).replace('/', File.separatorChar)
val storeFile = File(normalized) val storeFile = File(normalized)
return if (storeFile.isAbsolute) storeFile else rootProject.file(normalized) if (storeFile.isAbsolute) {
return storeFile
}
val rootRelativeFile = rootProject.file(normalized)
if (rootRelativeFile.exists()) {
return rootRelativeFile
}
val moduleRelativeFile = project.file(normalized)
if (moduleRelativeFile.exists()) {
return moduleRelativeFile
}
return rootRelativeFile
} }
val releaseStoreFile = val releaseStoreFile =

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 490 KiB

View File

@ -2,4 +2,4 @@ flutter_launcher_icons:
android: true android: true
image_path: "assets/branding/icon.png" image_path: "assets/branding/icon.png"
adaptive_icon_background: "#FFFFFF" adaptive_icon_background: "#FFFFFF"
adaptive_icon_foreground: "assets/branding/icon_foreground.png" adaptive_icon_foreground: "assets/branding/icon.png"

View File

@ -30,7 +30,21 @@ fun resolveKeystoreFile(rawPath: String?): File {
val normalized = expandedHome.replace('\\', File.separatorChar).replace('/', File.separatorChar) val normalized = expandedHome.replace('\\', File.separatorChar).replace('/', File.separatorChar)
val storeFile = File(normalized) val storeFile = File(normalized)
return if (storeFile.isAbsolute) storeFile else rootProject.file(normalized) if (storeFile.isAbsolute) {
return storeFile
}
val rootRelativeFile = rootProject.file(normalized)
if (rootRelativeFile.exists()) {
return rootRelativeFile
}
val moduleRelativeFile = project.file(normalized)
if (moduleRelativeFile.exists()) {
return moduleRelativeFile
}
return rootRelativeFile
} }
val releaseStoreFile = val releaseStoreFile =

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View File

@ -1,5 +1,5 @@
{ {
"initialUrl": "http://192.168.2.54:8080/test_bridge.html", "initialUrl": "http://192.168.2.57:8080/test_bridge.html",
"preferredOrientations": [ "preferredOrientations": [
"portraitUp", "portraitUp",
"portraitDown" "portraitDown"

View File

@ -2,4 +2,4 @@ flutter_launcher_icons:
android: true android: true
image_path: "assets/branding/icon.png" image_path: "assets/branding/icon.png"
adaptive_icon_background: "#1F2937" adaptive_icon_background: "#1F2937"
adaptive_icon_foreground: "assets/branding/icon_foreground.png" adaptive_icon_foreground: "assets/branding/icon.png"

View File

@ -30,7 +30,21 @@ fun resolveKeystoreFile(rawPath: String?): File {
val normalized = expandedHome.replace('\\', File.separatorChar).replace('/', File.separatorChar) val normalized = expandedHome.replace('\\', File.separatorChar).replace('/', File.separatorChar)
val storeFile = File(normalized) val storeFile = File(normalized)
return if (storeFile.isAbsolute) storeFile else rootProject.file(normalized) if (storeFile.isAbsolute) {
return storeFile
}
val rootRelativeFile = rootProject.file(normalized)
if (rootRelativeFile.exists()) {
return rootRelativeFile
}
val moduleRelativeFile = project.file(normalized)
if (moduleRelativeFile.exists()) {
return moduleRelativeFile
}
return rootRelativeFile
} }
val releaseStoreFile = val releaseStoreFile =

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@ -2,4 +2,4 @@ flutter_launcher_icons:
android: true android: true
image_path: "assets/branding/icon.png" image_path: "assets/branding/icon.png"
adaptive_icon_background: "#FFFFFF" adaptive_icon_background: "#FFFFFF"
adaptive_icon_foreground: "assets/branding/icon_foreground.png" adaptive_icon_foreground: "assets/branding/icon.png"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

View File

@ -10,8 +10,7 @@
| 资源名称 | 文件名 | 格式要求 | 尺寸 (px) | 核心要求 | | 资源名称 | 文件名 | 格式要求 | 尺寸 (px) | 核心要求 |
|---|---|---|---|---| |---|---|---|---|---|
| **应用图标** | `icon.png` | PNG *(不透明)* | 1024 × 1024 | 正方形铺满(用于老版本系统图标)。 | | **应用图标** | `icon.png` | PNG *(不透明)* | 1024 × 1024 | 同时用于老版本系统图标与 Android 自适应图标前景。 |
| **自适应前景** | `icon_foreground.png` | **PNG (背景透明)** | 1024 × 1024 | 主体图形居中。四周必须保留 **33% 的透明安全区**。 |
| **启动页图像** | `splash.png` | PNG *(含透明或实色)* | 1152 × 1152 | 核心 Logo 必须完全置于正中心的 **768×768 直径圆形**范围内。 | | **启动页图像** | `splash.png` | PNG *(含透明或实色)* | 1152 × 1152 | 核心 Logo 必须完全置于正中心的 **768×768 直径圆形**范围内。 |
| **单色前景** *(可选)* | `icon_monochrome.png` | **PNG (背景透明)** | 1024 × 1024 | 专为 Android 13+ 提供。必须是纯黑色或纯白色(无渐变),靠透明度展现轮廓。 | | **单色前景** *(可选)* | `icon_monochrome.png` | **PNG (背景透明)** | 1024 × 1024 | 专为 Android 13+ 提供。必须是纯黑色或纯白色(无渐变),靠透明度展现轮廓。 |
@ -21,30 +20,19 @@
### 2.1 应用图标 (`icon.png`) ### 2.1 应用图标 (`icon.png`)
这是最基础的图标,用于向后兼容较老版本的 Android 系统。 这是最基础的图标,同时用于向后兼容较老版本的 Android 系统,以及生成 Android 自适应图标前景
* **尺寸**1024 × 1024 像素(也可提供最低 512 × 512 的设计底线)。 * **尺寸**1024 × 1024 像素(也可提供最低 512 × 512 的设计底线)。
* **要求**:不需要圆角或圆形裁切(系统会自动裁切),直接提交带有背景颜色的**纯正方向**图形。 * **要求**:不需要圆角或圆形裁切(系统会自动裁切),直接提交带有背景颜色的**纯正方向**图形。
* **自适应图标兼容建议**
* 主体内容尽量集中在中央安全区,避免过分贴边。
* 如图标本身包含文字或复杂形状,建议四周保留适当留白,避免被系统遮罩裁切。
![应用图标规范](images/icon_spec.png) ![应用图标规范](images/icon_spec.png)
--- ---
### 2.2 自适应图标前景 (`icon_foreground.png`) ### 2.2 启动页图片 (`splash.png`)
自 Android 8.0 起,系统采用了由「背景层」+「前景层」自由组合实现动态效果的自适应图标Adaptive Icon。该图即为此时使用的「前景层」。
* **格式****必须是带透明背景的 PNG (PNG-24/32)**,不能使用 JPEG否则其白色底会遮盖住背景颜色。
* **安全区设计**
* 画布总尺寸保持 1024 × 1024。
* **主体内容必须集中在中央的 682 × 682 的圆形安全区内**。
* 外围的透明留白(约占据画布边长的 33%)将被系统底层用于视差动画和异形遮罩裁切。超出中央安全圈的内容**将被无情裁掉**。
![自适应前景图规范](images/foreground_spec.png)
---
### 2.3 启动页图片 (`splash.png`)
自 Android 12 开始系统强制接管开屏动画SplashScreen API对核心图像的位置和大小有极其严苛的要求否则在 2K/4K 等高清分辨率平板上会造成拉伸模糊或主体被截断。 自 Android 12 开始系统强制接管开屏动画SplashScreen API对核心图像的位置和大小有极其严苛的要求否则在 2K/4K 等高清分辨率平板上会造成拉伸模糊或主体被截断。

View File

@ -15,6 +15,5 @@ theme:
branding: branding:
icon: "icon.png" icon: "icon.png"
icon_background: "#FFFFFF" icon_background: "#FFFFFF"
icon_foreground: "icon_foreground.png"
splash: "splash.png" splash: "splash.png"
splash_color: "#FFFFFF" splash_color: "#FFFFFF"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 490 KiB

View File

@ -15,6 +15,5 @@ theme:
branding: branding:
icon: "icon.png" icon: "icon.png"
icon_background: "#1F2937" icon_background: "#1F2937"
icon_foreground: "icon_foreground.png"
splash: "splash.png" splash: "splash.png"
splash_color: "#1F2937" splash_color: "#1F2937"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View File

@ -15,6 +15,5 @@ theme:
branding: branding:
icon: "icon.png" icon: "icon.png"
icon_background: "#FFFFFF" icon_background: "#FFFFFF"
icon_foreground: "icon_foreground.png"
splash: "splash.png" splash: "splash.png"
splash_color: "#FFFFFF" splash_color: "#FFFFFF"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@ -30,7 +30,21 @@ fun resolveKeystoreFile(rawPath: String?): File {
val normalized = expandedHome.replace('\\', File.separatorChar).replace('/', File.separatorChar) val normalized = expandedHome.replace('\\', File.separatorChar).replace('/', File.separatorChar)
val storeFile = File(normalized) val storeFile = File(normalized)
return if (storeFile.isAbsolute) storeFile else rootProject.file(normalized) if (storeFile.isAbsolute) {
return storeFile
}
val rootRelativeFile = rootProject.file(normalized)
if (rootRelativeFile.exists()) {
return rootRelativeFile
}
val moduleRelativeFile = project.file(normalized)
if (moduleRelativeFile.exists()) {
return moduleRelativeFile
}
return rootRelativeFile
} }
val releaseStoreFile = resolveKeystoreFile(keystoreProperties["storeFile"] as String?) val releaseStoreFile = resolveKeystoreFile(keystoreProperties["storeFile"] as String?)

View File

@ -515,7 +515,6 @@ Future<void> _generateBrandingAssets(
], workingDirectory: appDir); ], workingDirectory: appDir);
final iconPath = 'assets/branding/${branding['icon']}'; final iconPath = 'assets/branding/${branding['icon']}';
final iconForeground = 'assets/branding/${branding['icon_foreground']}';
final iconBackground = branding['icon_background'] as String; final iconBackground = branding['icon_background'] as String;
final iconsYaml = final iconsYaml =
''' '''
@ -523,7 +522,7 @@ flutter_launcher_icons:
android: true android: true
image_path: "$iconPath" image_path: "$iconPath"
adaptive_icon_background: "$iconBackground" adaptive_icon_background: "$iconBackground"
adaptive_icon_foreground: "$iconForeground" adaptive_icon_foreground: "$iconPath"
'''; ''';
await File('$appDir/flutter_launcher_icons.yaml').writeAsString(iconsYaml); await File('$appDir/flutter_launcher_icons.yaml').writeAsString(iconsYaml);
@ -652,9 +651,23 @@ fun resolveKeystoreFile(rawPath: String?): File {
candidate candidate
} }
val normalized = expandedHome.replace('\\', File.separatorChar).replace('/', File.separatorChar) val normalized = expandedHome.replace('\\\\', File.separatorChar).replace('/', File.separatorChar)
val storeFile = File(normalized) val storeFile = File(normalized)
return if (storeFile.isAbsolute) storeFile else rootProject.file(normalized) if (storeFile.isAbsolute) {
return storeFile
}
val rootRelativeFile = rootProject.file(normalized)
if (rootRelativeFile.exists()) {
return rootRelativeFile
}
val moduleRelativeFile = project.file(normalized)
if (moduleRelativeFile.exists()) {
return moduleRelativeFile
}
return rootRelativeFile
} }
val releaseStoreFile = val releaseStoreFile =