feat(splash): 缩短开屏停留并统一开屏图

This commit is contained in:
Zhou Weidong committed 2026-10-23 01:06:07 +08:00
1 parent b513ac0a9c
commit 6318ca24ec
18 files changed
+47 -100

No files matched your search

@@ -36,6 +36,9 @@ class MainActivity : FlutterActivity() {
private var imeInset = 0
override fun onCreate(savedInstanceState: Bundle?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
splashScreen.setOnExitAnimationListener { screen -> screen.remove() }
}
super.onCreate(savedInstanceState)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
@@ -1,4 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Author: fruit.com.cn -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splash_background" />
<item>
<bitmap
android:gravity="center"
android:src="@drawable/splash_screen" />
</item>
</layer-list>
@@ -1,4 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Author: fruit.com.cn -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splash_background" />
<item>
<bitmap
android:gravity="center"
android:src="@drawable/splash_screen" />
</item>
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -1,6 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Author: fruit.com.cn -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splash_background" />
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
<bitmap
android:gravity="center"
android:src="@drawable/splash_screen" />
</item>
</layer-list>
@@ -1,6 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Author: fruit.com.cn -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splash_background" />
<item>
<bitmap
android:gravity="center"
android:src="@drawable/splash_screen" />
</item>
</layer-list>
@@ -1,6 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Author: fruit.com.cn -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splash_background" />
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
<bitmap
android:gravity="center"
android:src="@drawable/splash_screen" />
</item>
</layer-list>
@@ -8,6 +8,8 @@
<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:windowSplashScreenAnimationDuration">0</item>
</style>
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@color/splash_background</item>
@@ -8,6 +8,8 @@
<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:windowSplashScreenAnimationDuration">0</item>
</style>
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@color/splash_background</item>
+1 -1
View File
@@ -29,7 +29,7 @@ final class RunnerFlutterViewController: FlutterViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(red: 16.0 / 255.0, green: 12.0 / 255.0, blue: 10.0 / 255.0, alpha: 1)
view.backgroundColor = .black
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 B

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 B

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 B

After

Width:  |  Height:  |  Size: 24 KiB

@@ -16,18 +16,13 @@
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"></imageView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"/>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="backgroundColor" red="0" green="0" blue="0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="3T2-ad-Qdv"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="RPx-PI-7Xg"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="SdS-ul-q2q"/>
<constraint firstAttribute="trailing" secondItem="tWc-Dq-wcI" secondAttribute="trailing" id="Swv-Gf-Rwn"/>
<constraint firstAttribute="trailing" secondItem="YRO-k0-Ey4" secondAttribute="trailing" id="TQA-XW-tRk"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="duK-uY-Gun"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="kV7-tw-vXt"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="xPn-NY-SIU"/>
</constraints>
</view>
@@ -38,7 +33,6 @@
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
<image name="LaunchBackground" width="1" height="1"/>
<image name="LaunchImage" width="390" height="844"/>
</resources>
</document>
+6 -4
View File
@@ -1,5 +1,7 @@
// Author: fruit.com.cn
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -9,10 +11,10 @@ import 'core/config/server_endpoint.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Future.wait([
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]),
ServerEndpoint.load(),
]);
await ServerEndpoint.load();
AppConfig.ready = AppConfig.probeAndApplyBaseUrl();
runApp(const LumaDramaApp());
unawaited(
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]),
);
}
+1 -80
View File
@@ -30,9 +30,6 @@ import 'home_logic.dart';
import 'widgets/home_feed_grid.dart';
import 'widgets/home_grid_header.dart';
const Duration _startupSplashMinimumDuration = Duration(milliseconds: 400);
const Duration _startupSplashFadeDuration = Duration(milliseconds: 200);
class HomePage extends StatefulWidget {
const HomePage({super.key});
@@ -44,31 +41,15 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
bool _resumeRetryInFlight = false;
HomeFeedController? _homeFeed;
bool _recommendSeeded = false;
Timer? _startupSplashMinimumTimer;
bool _catalogSettled = false;
bool _startupSplashVisible = true;
bool _startupSplashFadingOut = false;
bool _startupSplashMinimumElapsed = false;
bool _startupSplashDismissing = false;
bool _startupSplashSettledTriggered = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_startupSplashMinimumTimer = Timer(_startupSplashMinimumDuration, () {
if (!mounted) return;
_startupSplashMinimumElapsed = true;
unawaited(_dismissStartupSplashIfReady());
});
});
}
@override
void dispose() {
_startupSplashMinimumTimer?.cancel();
_homeFeed?.dispose();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
@@ -102,43 +83,12 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
@override
Widget build(BuildContext context) {
final catalog = context.watch<CatalogController>();
_catalogSettled = catalog.settled;
if (_startupSplashVisible &&
_catalogSettled &&
!_startupSplashSettledTriggered) {
_startupSplashSettledTriggered = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) unawaited(_dismissStartupSplashIfReady());
});
}
return Theme(
data: AppTheme.dark(),
child: Stack(
fit: StackFit.expand,
children: [
_buildHome(context, catalog),
if (_startupSplashVisible)
_StartupSplashOverlay(fadingOut: _startupSplashFadingOut),
],
),
child: _buildHome(context, catalog),
);
}
Future<void> _dismissStartupSplashIfReady() async {
if (!mounted ||
!_startupSplashVisible ||
_startupSplashDismissing ||
!_startupSplashMinimumElapsed ||
!_catalogSettled) {
return;
}
_startupSplashDismissing = true;
setState(() => _startupSplashFadingOut = true);
await Future<void>.delayed(_startupSplashFadeDuration);
if (!mounted) return;
setState(() => _startupSplashVisible = false);
}
Widget _buildHome(BuildContext context, CatalogController catalog) {
final homeFeed = _ensureHomeFeed(catalog);
if (!_recommendSeeded && catalog.dramas.isNotEmpty) {
@@ -162,35 +112,6 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
}
}
class _StartupSplashOverlay extends StatelessWidget {
const _StartupSplashOverlay({required this.fadingOut});
final bool fadingOut;
@override
Widget build(BuildContext context) {
return IgnorePointer(
child: AnimatedOpacity(
key: const Key('startup-splash'),
opacity: fadingOut ? 0 : 1,
duration: _startupSplashFadeDuration,
curve: Curves.easeOut,
child: ColoredBox(
color: AppColors.appBackground,
child: Center(
child: Image.asset(
'assets/icon/splash_logo.png',
key: const Key('startup-splash-logo'),
width: 240,
fit: BoxFit.contain,
),
),
),
),
);
}
}
/// 四个根页面共享同一路由并保持各自状态。
class _HomeTabs extends StatefulWidget {
const _HomeTabs({super.key, required this.feedController});
+2 -3
View File
@@ -36,7 +36,7 @@ void main() {
AppConfig.ready = Future<void>.value();
});
testWidgets('should_show_brand_splash_and_grid_skeleton_while_loading', (
testWidgets('should_show_grid_skeleton_without_an_in_app_splash', (
tester,
) async {
final repository = _PendingCatalogRepository();
@@ -47,8 +47,7 @@ void main() {
expect(find.byKey(const Key('home-grid-header')), findsOneWidget);
expect(find.byKey(const Key('home-grid-skeleton')), findsOneWidget);
expect(find.byKey(const Key('home-tabs-bar')), findsOneWidget);
expect(find.byKey(const Key('startup-splash')), findsOneWidget);
expect(find.byKey(const Key('startup-splash-logo')), findsOneWidget);
expect(find.byKey(const Key('startup-splash')), findsNothing);
expect(find.byType(CircularProgressIndicator), findsNothing);
repository.complete(_feed);