feat(brand): 应用图标、开屏资源与品牌字体切换为 ymb

This commit is contained in:
Vicky authored and trae-solo-agent committed 2026-10-30 17:14:27 +08:00
1 parent 26321e3348
commit 22cc669bbe
71 files changed
+489 -114

No files matched your search

+1 -1
View File
@@ -6,7 +6,7 @@
<uses-permission
android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application
android:label="FruitShorts"
android:label="ymb"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

@@ -2,6 +2,18 @@
<!-- Author: fruit.com.cn -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splash_background" />
<item>
<shape android:shape="rectangle">
<gradient
android:type="radial"
android:centerX="0"
android:centerY="0"
android:gradientRadius="350dp"
android:startColor="#FF6B011A"
android:centerColor="#616B011A"
android:endColor="#006B011A" />
</shape>
</item>
<item>
<bitmap
android:gravity="center"
@@ -2,6 +2,18 @@
<!-- Author: fruit.com.cn -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splash_background" />
<item>
<shape android:shape="rectangle">
<gradient
android:type="radial"
android:centerX="0"
android:centerY="0"
android:gradientRadius="350dp"
android:startColor="#FF6B011A"
android:centerColor="#616B011A"
android:endColor="#006B011A" />
</shape>
</item>
<item>
<bitmap
android:gravity="center"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

@@ -2,6 +2,18 @@
<!-- Author: fruit.com.cn -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splash_background" />
<item>
<shape android:shape="rectangle">
<gradient
android:type="radial"
android:centerX="0"
android:centerY="0"
android:gradientRadius="350dp"
android:startColor="#FF6B011A"
android:centerColor="#616B011A"
android:endColor="#006B011A" />
</shape>
</item>
<item>
<bitmap
android:gravity="center"
@@ -2,6 +2,18 @@
<!-- Author: fruit.com.cn -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splash_background" />
<item>
<shape android:shape="rectangle">
<gradient
android:type="radial"
android:centerX="0"
android:centerY="0"
android:gradientRadius="350dp"
android:startColor="#FF6B011A"
android:centerColor="#616B011A"
android:endColor="#006B011A" />
</shape>
</item>
<item>
<bitmap
android:gravity="center"
Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

@@ -2,6 +2,18 @@
<!-- Author: fruit.com.cn -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splash_background" />
<item>
<shape android:shape="rectangle">
<gradient
android:type="radial"
android:centerX="0"
android:centerY="0"
android:gradientRadius="350dp"
android:startColor="#FF6B011A"
android:centerColor="#616B011A"
android:endColor="#006B011A" />
</shape>
</item>
<item>
<bitmap
android:gravity="center"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 469 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -8,7 +8,7 @@
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground">#000000</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_brand</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
<item name="android:windowSplashScreenAnimationDuration">0</item>
</style>
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
@@ -8,7 +8,7 @@
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground">#000000</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_brand</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
<item name="android:windowSplashScreenAnimationDuration">0</item>
</style>
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
+2 -2
View File
@@ -3,5 +3,5 @@
<resources>
<color name="splash_background">#000000</color>
<color name="ic_launcher_background">#FF6B2C</color>
</resources>
<color name="ic_launcher_background">#E9142E</color>
</resources>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 734 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 412 B

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 907 B

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 593 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 907 B

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 655 KiB

+103 -103
View File
@@ -1,115 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>ymb</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>ymb</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>FruitShorts</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>FruitShorts</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<key>NSAllowsLocalNetworking</key>
<true/>
<key>NSAppTransportSecurity</key>
<key>NSExceptionDomains</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSAllowsLocalNetworking</key>
<true/>
<key>NSExceptionDomains</key>
<key>127.0.0.1</key>
<dict>
<key>127.0.0.1</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>192.168.31.243</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>39.162.50.99</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>192.168.31.243</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>39.162.50.99</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
<key>NSLocalNetworkUsageDescription</key>
<string>开发阶段需要访问同一局域网内的短剧内容服务。</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIStatusBarHidden</key>
<false/>
</dict>
<key>NSLocalNetworkUsageDescription</key>
<string>开发阶段需要访问同一局域网内的短剧内容服务。</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
+28 -6
View File
@@ -87,11 +87,30 @@ flutter:
assets:
- assets/icon/
- assets/icon/splash_logo.png
- assets/icon/splash_logo_android12.png
- assets/icon/rail/
# 只打包代码实际引用的那张登录背景(其余 3 张留在仓库但不进包)。
- assets/auth/bg_3.jpg
fonts:
- family: PlusJakartaSans
fonts:
- asset: assets/fonts/PlusJakartaSans-Regular.ttf
weight: 400
- asset: assets/fonts/PlusJakartaSans-Medium.ttf
weight: 500
- asset: assets/fonts/PlusJakartaSans-SemiBold.ttf
weight: 600
- asset: assets/fonts/PlusJakartaSans-Bold.ttf
weight: 700
- family: Outfit
fonts:
- asset: assets/fonts/Outfit-Regular.ttf
weight: 400
- asset: assets/fonts/Outfit-SemiBold.ttf
weight: 600
- asset: assets/fonts/Outfit-Bold.ttf
weight: 700
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
@@ -126,13 +145,15 @@ flutter:
flutter_launcher_icons:
android: true
ios: true
image_path: assets/icon/fruitshorts.png
adaptive_icon_background: "#FF6B2C"
adaptive_icon_foreground: assets/icon/fruitshorts.png
image_path: assets/icon/ymb.png
adaptive_icon_background: "#E9142E"
adaptive_icon_foreground: assets/icon/ymb_foreground.png
remove_alpha_ios: true
# 修改以下开屏配置后,必须重新执行:
# dart run flutter_native_splash:create
# 开屏页资源目前是手工维护的:Android 走 res/drawable*/launch_background.xml
# (黑底暗红渐变 + 居中标识),iOS 走 LaunchImage.imageset 的完整参考图。
# 不要再执行 `dart run flutter_native_splash:create`,它只会按 color 重新生成,
# 会覆盖固定尺寸布局。使用 python3 tools/generate_brand_assets.py 生成资源。
flutter_native_splash:
color: "#000000"
android: true
@@ -140,3 +161,4 @@ flutter_native_splash:
web: false
android_12:
color: "#000000"
image: assets/icon/ymb_android12.png
+136
View File
@@ -0,0 +1,136 @@
// Author: fruit.com.cn
// 验证品牌名称、原生启动布局与平台图标资源。
import 'dart:convert';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/l10n/app_localizations.dart';
const _iconAsset = 'assets/icon/ymb.png';
const _iconSize = 1024;
const _launchWidth = 390;
const _launchHeight = 844;
const _androidRoot = 'android/app/src/main/res';
const _iosRoot = 'ios/Runner/Assets.xcassets';
const _red = [245, 48, 59, 255];
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test('should_use_ymb_when_resolving_each_app_locale', () async {
for (final locale in AppLocalizations.supportedLocales) {
final l10n = await AppLocalizations.delegate.load(locale);
expect(l10n.appName, 'ymb');
}
});
test('should_keep_reference_colors_when_decoding_master_icon', () async {
final image = await _decode(_iconAsset);
addTearDown(image.dispose);
expect(image.width, _iconSize);
expect(image.height, _iconSize);
final bytes = await image.toByteData();
final pixel = bytes!.buffer.asUint8List().sublist(0, _red.length);
for (var channel = 0; channel < _red.length; channel++) {
expect(pixel[channel], closeTo(_red[channel], 2));
}
});
test('should_match_declared_sizes_when_loading_ios_app_icons', () async {
final manifest =
jsonDecode(
File(
'$_iosRoot/AppIcon.appiconset/Contents.json',
).readAsStringSync(),
)
as Map<String, dynamic>;
for (final entry in manifest['images'] as List) {
final size = double.parse((entry['size'] as String).split('x').first);
final scale = int.parse((entry['scale'] as String).replaceAll('x', ''));
final image = await _decode(
'$_iosRoot/AppIcon.appiconset/${entry['filename']}',
);
expect(image.width, (size * scale).round());
expect(image.height, image.width);
image.dispose();
}
});
test('should_fit_full_reference_when_loading_ios_launch_screen', () {
final storyboard = File(
'ios/Runner/Base.lproj/LaunchScreen.storyboard',
).readAsStringSync();
expect(storyboard, contains('contentMode="scaleAspectFit"'));
expect(storyboard, contains('firstAttribute="leading"'));
expect(storyboard, contains('firstAttribute="trailing"'));
});
test('should_keep_reference_ratio_when_loading_ios_launch_images', () async {
for (var scale = 1; scale <= 3; scale++) {
final suffix = scale == 1 ? '' : '@${scale}x';
final image = await _decode(
'$_iosRoot/LaunchImage.imageset/LaunchImage$suffix.png',
);
expect(image.width, _launchWidth * scale);
expect(image.height, _launchHeight * scale);
image.dispose();
}
});
test('should_keep_reference_tint_when_loading_android_launch_layers', () {
for (final qualifier in ['', '-v21', '-v31', '-night', '-night-v21']) {
final layers = File(
'$_androidRoot/drawable$qualifier/launch_background.xml',
).readAsStringSync();
expect(layers, contains('<gradient'));
expect(layers, contains('android:gravity="center"'));
expect(layers, contains('@drawable/splash_screen'));
}
});
test('should_preserve_safe_area_when_loading_android12_mark', () async {
const root = '$_androidRoot/drawable-xxxhdpi';
final image = await _decode('$root/android12splash.png');
addTearDown(image.dispose);
const canvasSize = 1152;
const markInset = 416;
const center = canvasSize ~/ 2;
expect(image.width, canvasSize);
expect(image.height, canvasSize);
final data = (await image.toByteData())!.buffer.asUint8List();
int alphaAt(int x, int y) => data[(y * canvasSize + x) * 4 + 3];
expect(alphaAt(0, 0), 0);
expect(alphaAt(markInset - 1, center), 0);
expect(alphaAt(markInset + 1, center), 255);
expect(alphaAt(center, center), 255);
for (final qualifier in ['', '-night']) {
final styles = File(
'$_androidRoot/values$qualifier-v31/styles.xml',
).readAsStringSync();
expect(styles, contains('@drawable/android12splash'));
}
});
test('should_use_ymb_when_resolving_platform_display_names', () {
final manifest = File(
'android/app/src/main/AndroidManifest.xml',
).readAsStringSync();
expect(manifest, contains('android:label="ymb"'));
final plist = File('ios/Runner/Info.plist').readAsStringSync();
expect(
RegExp(
r'<key>CFBundleDisplayName</key>\s*<string>ymb</string>',
).hasMatch(plist),
isTrue,
);
});
}
Future<ui.Image> _decode(String path) async {
final codec = await ui.instantiateImageCodec(File(path).readAsBytesSync());
final frame = await codec.getNextFrame();
codec.dispose();
return frame.image;
}
+15
View File
@@ -1,6 +1,7 @@
// Author: fruit.com.cn
import 'dart:math';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -107,4 +108,18 @@ void main() {
expect(AppTheme.cardShadow.blurRadius, 0);
expect(AppTheme.cardShadow.offset, Offset.zero);
});
test('should_register_brand_font_assets_in_pubspec', () {
final pubspec = File('pubspec.yaml').readAsStringSync();
expect(pubspec, contains('family: PlusJakartaSans'));
expect(pubspec, contains('PlusJakartaSans-Regular.ttf'));
expect(pubspec, contains('PlusJakartaSans-Medium.ttf'));
expect(pubspec, contains('PlusJakartaSans-SemiBold.ttf'));
expect(pubspec, contains('PlusJakartaSans-Bold.ttf'));
expect(pubspec, contains('family: Outfit'));
expect(pubspec, contains('Outfit-Regular.ttf'));
expect(pubspec, contains('Outfit-SemiBold.ttf'));
expect(pubspec, contains('Outfit-Bold.ttf'));
});
}
+142
View File
@@ -0,0 +1,142 @@
# Author: fruit.com.cn
"""使用确认的 ymb 原图生成图标、启动标识及原生平台尺寸。"""
import json
from pathlib import Path
from PIL import Image, ImageChops, ImageDraw
ROOT = Path(__file__).resolve().parents[1]
ASSETS = ROOT / "assets/icon"
IOS = ROOT / "ios/Runner/Assets.xcassets"
ANDROID = ROOT / "android/app/src/main/res"
REFERENCE = ROOT / "assets/branding/ymb-reference.png"
LAUNCH_REFERENCE = ROOT / "assets/branding/ymb-launch-reference.png"
BRAND_RED = "#E9142E"
WHITE = "#FFFFFF"
CLEAR = (0, 0, 0, 0)
MASTER_SIZE = 1024
SPLASH_SIZE = 80
SPLASH_RADIUS = 18
ANDROID12_SIZE = 288
FOREGROUND_SIZE = 108
LEGACY_SIZE = 48
PLACEHOLDER_SIZE = (600, 360)
LAUNCH_SIZE = (390, 844)
WHITE_THRESHOLD = 100
WHITE_RANGE = 145
DENSITIES = {"mdpi": 1, "hdpi": 1.5, "xhdpi": 2, "xxhdpi": 3, "xxxhdpi": 4}
RESAMPLE = Image.Resampling.LANCZOS
def wordmark(reference):
"""从原图白色字标提取透明前景,不重新绘制或改变字形。"""
channels = reference.convert("RGB").split()
lightness = ImageChops.darker(ImageChops.darker(*channels[:2]), channels[2])
alpha = lightness.point(
lambda value: max(0, min(255, round(
(value - WHITE_THRESHOLD) * 255 / WHITE_RANGE
)))
)
image = Image.new("RGBA", reference.size, WHITE)
image.putalpha(alpha)
return image
def centered(canvas, mark):
"""将标识按可见边界居中,不包含字体基线额外留白。"""
origin = ((canvas.width - mark.width) // 2, (canvas.height - mark.height) // 2)
canvas.alpha_composite(mark, origin)
return canvas
def scaled_wordmark(mark, width):
"""等比缩放字标,所有输出共用同一字形。"""
height = round(mark.height * width / mark.width)
return mark.resize((width, height), RESAMPLE)
def splash_icon(master, size):
"""仅启动图烘焙圆角;桌面图标保留系统负责的裁切。"""
image = master.resize((size, size), RESAMPLE).convert("RGBA")
mask = Image.new("L", (MASTER_SIZE, MASTER_SIZE))
radius = MASTER_SIZE * SPLASH_RADIUS / SPLASH_SIZE
ImageDraw.Draw(mask).rounded_rectangle(
(0, 0, MASTER_SIZE - 1, MASTER_SIZE - 1), radius=radius, fill=255
)
image.putalpha(mask.resize((size, size), RESAMPLE))
return image
def save(image, target):
"""写入明确路径的生成资源,不清理或删除目录。"""
target.parent.mkdir(parents=True, exist_ok=True)
image.save(target, optimize=True)
def ios_assets(master):
"""严格按资源目录声明导出图标;开屏保留确认的完整参考图。"""
app_icons = IOS / "AppIcon.appiconset"
manifest = json.loads((app_icons / "Contents.json").read_text())
for entry in manifest["images"]:
points = float(entry["size"].split("x")[0])
scale = int(entry["scale"].removesuffix("x"))
pixels = round(points * scale)
save(master.resize((pixels, pixels), RESAMPLE), app_icons / entry["filename"])
for scale in (1, 2, 3):
suffix = "" if scale == 1 else f"@{scale}x"
size = tuple(value * scale for value in LAUNCH_SIZE)
image = Image.open(LAUNCH_REFERENCE).convert("RGB").resize(size, RESAMPLE)
save(image, IOS / f"LaunchImage.imageset/LaunchImage{suffix}.png")
def android_assets(master, foreground):
"""覆盖现有密度资源,并为 Android 12 提供独立安全区标识。"""
for density, scale in DENSITIES.items():
pixels = round(LEGACY_SIZE * scale)
save(
master.resize((pixels, pixels), RESAMPLE),
ANDROID / f"mipmap-{density}/ic_launcher.png",
)
pixels = round(FOREGROUND_SIZE * scale)
save(
foreground.resize((pixels, pixels), RESAMPLE),
ANDROID / f"drawable-{density}/ic_launcher_foreground.png",
)
save(
Image.new("RGB", (pixels, pixels), BRAND_RED),
ANDROID / f"mipmap-{density}/ic_launcher_background.png",
)
mark = splash_icon(master, round(SPLASH_SIZE * scale))
save(mark, ANDROID / f"drawable-{density}/splash_screen.png")
size = round(ANDROID12_SIZE * scale)
image = centered(Image.new("RGBA", (size, size), CLEAR), mark)
save(image, ANDROID / f"drawable-{density}/android12splash.png")
def main():
"""直接复用确认的原始图标和开屏,输出平台所需尺寸。"""
reference = Image.open(REFERENCE).convert("RGB")
master = reference.resize((MASTER_SIZE, MASTER_SIZE), RESAMPLE)
mark = wordmark(reference)
foreground = mark.resize((MASTER_SIZE, MASTER_SIZE), RESAMPLE)
save(master, ASSETS / "ymb.png")
save(foreground, ASSETS / "ymb_foreground.png")
placeholder = centered(
Image.new("RGBA", PLACEHOLDER_SIZE, CLEAR),
scaled_wordmark(mark.crop(mark.getbbox()), PLACEHOLDER_SIZE[0]),
)
save(placeholder, ASSETS / "splash_logo.png")
icon = splash_icon(master, SPLASH_SIZE * 4)
size = ANDROID12_SIZE * 4
save(
centered(Image.new("RGBA", (size, size), CLEAR), icon),
ASSETS / "ymb_android12.png",
)
ios_assets(master)
android_assets(master, foreground)
print("Generated ymb assets: master 1024, iOS catalog, Android 5 densities.")
if __name__ == "__main__":
main()