feat(splash): 缩短开屏停留并统一开屏图
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>
|
||||
|
After Width: | Height: | Size: 21 KiB |
|
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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 69 B After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 69 B After Width: | Height: | Size: 14 KiB |
|
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>
|
||||
@@ -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]),
|
||||
);
|
||||
}
|
||||
@@ -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});
|
||||
|
||||
@@ -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);
|
||||
|
||||