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
|
|
@ -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/`。
|
||||||
|
|
|
||||||
|
|
@ -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 =
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 283 KiB |
|
Before Width: | Height: | Size: 490 KiB |
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 =
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 29 KiB |
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 =
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 30 KiB |
|
|
@ -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"
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 48 KiB |
|
|
@ -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 的设计底线)。
|
||||||
* **要求**:不需要圆角或圆形裁切(系统会自动裁切),直接提交带有背景颜色的**纯正方向**图形。
|
* **要求**:不需要圆角或圆形裁切(系统会自动裁切),直接提交带有背景颜色的**纯正方向**图形。
|
||||||
|
* **自适应图标兼容建议**:
|
||||||
|
* 主体内容尽量集中在中央安全区,避免过分贴边。
|
||||||
|
* 如图标本身包含文字或复杂形状,建议四周保留适当留白,避免被系统遮罩裁切。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2.2 自适应图标前景 (`icon_foreground.png`)
|
### 2.2 启动页图片 (`splash.png`)
|
||||||
|
|
||||||
自 Android 8.0 起,系统采用了由「背景层」+「前景层」自由组合实现动态效果的自适应图标(Adaptive Icon)。该图即为此时使用的「前景层」。
|
|
||||||
|
|
||||||
* **格式**:**必须是带透明背景的 PNG (PNG-24/32)**,不能使用 JPEG,否则其白色底会遮盖住背景颜色。
|
|
||||||
* **安全区设计**:
|
|
||||||
* 画布总尺寸保持 1024 × 1024。
|
|
||||||
* **主体内容必须集中在中央的 682 × 682 的圆形安全区内**。
|
|
||||||
* 外围的透明留白(约占据画布边长的 33%)将被系统底层用于视差动画和异形遮罩裁切。超出中央安全圈的内容**将被无情裁掉**。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2.3 启动页图片 (`splash.png`)
|
|
||||||
|
|
||||||
自 Android 12 开始,系统强制接管开屏动画(SplashScreen API),对核心图像的位置和大小有极其严苛的要求,否则在 2K/4K 等高清分辨率平板上会造成拉伸模糊或主体被截断。
|
自 Android 12 开始,系统强制接管开屏动画(SplashScreen API),对核心图像的位置和大小有极其严苛的要求,否则在 2K/4K 等高清分辨率平板上会造成拉伸模糊或主体被截断。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 490 KiB |
|
|
@ -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"
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 29 KiB |
|
|
@ -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"
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 30 KiB |
|
|
@ -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?)
|
||||||
|
|
|
||||||
|
|
@ -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 =
|
||||||
|
|
|
||||||