feat(splash): 缩短开屏停留并统一开屏图
No files matched your search
@@ -36,6 +36,9 @@ class MainActivity : FlutterActivity() {
|
|||||||
private var imeInset = 0
|
private var imeInset = 0
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
splashScreen.setOnExitAnimationListener { screen -> screen.remove() }
|
||||||
|
}
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:drawable="@color/splash_background" />
|
<item android:drawable="@color/splash_background" />
|
||||||
|
<item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@drawable/splash_screen" />
|
||||||
|
</item>
|
||||||
</layer-list>
|
</layer-list>
|
||||||
@@ -1,4 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:drawable="@color/splash_background" />
|
<item android:drawable="@color/splash_background" />
|
||||||
|
<item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@drawable/splash_screen" />
|
||||||
|
</item>
|
||||||
</layer-list>
|
</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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@color/splash_background" />
|
||||||
<item>
|
<item>
|
||||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@drawable/splash_screen" />
|
||||||
</item>
|
</item>
|
||||||
</layer-list>
|
</layer-list>
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Author: fruit.com.cn -->
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:drawable="@color/splash_background" />
|
<item android:drawable="@color/splash_background" />
|
||||||
|
<item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@drawable/splash_screen" />
|
||||||
|
</item>
|
||||||
</layer-list>
|
</layer-list>
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@color/splash_background" />
|
||||||
<item>
|
<item>
|
||||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@drawable/splash_screen" />
|
||||||
</item>
|
</item>
|
||||||
</layer-list>
|
</layer-list>
|
||||||
@@ -8,6 +8,8 @@
|
|||||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||||
<item name="android:windowSplashScreenBackground">#000000</item>
|
<item name="android:windowSplashScreenBackground">#000000</item>
|
||||||
|
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_brand</item>
|
||||||
|
<item name="android:windowSplashScreenAnimationDuration">0</item>
|
||||||
</style>
|
</style>
|
||||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
<item name="android:windowBackground">@color/splash_background</item>
|
<item name="android:windowBackground">@color/splash_background</item>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||||
<item name="android:windowSplashScreenBackground">#000000</item>
|
<item name="android:windowSplashScreenBackground">#000000</item>
|
||||||
|
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_brand</item>
|
||||||
|
<item name="android:windowSplashScreenAnimationDuration">0</item>
|
||||||
</style>
|
</style>
|
||||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
<item name="android:windowBackground">@color/splash_background</item>
|
<item name="android:windowBackground">@color/splash_background</item>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ final class RunnerFlutterViewController: FlutterViewController {
|
|||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.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">
|
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"/>
|
||||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"></imageView>
|
|
||||||
</subviews>
|
</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>
|
<constraints>
|
||||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="3T2-ad-Qdv"/>
|
<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 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="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"/>
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="xPn-NY-SIU"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
@@ -38,7 +33,6 @@
|
|||||||
</scene>
|
</scene>
|
||||||
</scenes>
|
</scenes>
|
||||||
<resources>
|
<resources>
|
||||||
<image name="LaunchImage" width="168" height="185"/>
|
<image name="LaunchImage" width="390" height="844"/>
|
||||||
<image name="LaunchBackground" width="1" height="1"/>
|
|
||||||
</resources>
|
</resources>
|
||||||
</document>
|
</document>
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
// Author: fruit.com.cn
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
@@ -9,10 +11,10 @@ import 'core/config/server_endpoint.dart';
|
|||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await Future.wait([
|
await ServerEndpoint.load();
|
||||||
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]),
|
|
||||||
ServerEndpoint.load(),
|
|
||||||
]);
|
|
||||||
AppConfig.ready = AppConfig.probeAndApplyBaseUrl();
|
AppConfig.ready = AppConfig.probeAndApplyBaseUrl();
|
||||||
runApp(const LumaDramaApp());
|
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_feed_grid.dart';
|
||||||
import 'widgets/home_grid_header.dart';
|
import 'widgets/home_grid_header.dart';
|
||||||
|
|
||||||
const Duration _startupSplashMinimumDuration = Duration(milliseconds: 400);
|
|
||||||
const Duration _startupSplashFadeDuration = Duration(milliseconds: 200);
|
|
||||||
|
|
||||||
class HomePage extends StatefulWidget {
|
class HomePage extends StatefulWidget {
|
||||||
const HomePage({super.key});
|
const HomePage({super.key});
|
||||||
|
|
||||||
@@ -44,31 +41,15 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
|||||||
bool _resumeRetryInFlight = false;
|
bool _resumeRetryInFlight = false;
|
||||||
HomeFeedController? _homeFeed;
|
HomeFeedController? _homeFeed;
|
||||||
bool _recommendSeeded = false;
|
bool _recommendSeeded = false;
|
||||||
Timer? _startupSplashMinimumTimer;
|
|
||||||
bool _catalogSettled = false;
|
|
||||||
bool _startupSplashVisible = true;
|
|
||||||
bool _startupSplashFadingOut = false;
|
|
||||||
bool _startupSplashMinimumElapsed = false;
|
|
||||||
bool _startupSplashDismissing = false;
|
|
||||||
bool _startupSplashSettledTriggered = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
if (!mounted) return;
|
|
||||||
_startupSplashMinimumTimer = Timer(_startupSplashMinimumDuration, () {
|
|
||||||
if (!mounted) return;
|
|
||||||
_startupSplashMinimumElapsed = true;
|
|
||||||
unawaited(_dismissStartupSplashIfReady());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_startupSplashMinimumTimer?.cancel();
|
|
||||||
_homeFeed?.dispose();
|
_homeFeed?.dispose();
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
@@ -102,43 +83,12 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final catalog = context.watch<CatalogController>();
|
final catalog = context.watch<CatalogController>();
|
||||||
_catalogSettled = catalog.settled;
|
|
||||||
if (_startupSplashVisible &&
|
|
||||||
_catalogSettled &&
|
|
||||||
!_startupSplashSettledTriggered) {
|
|
||||||
_startupSplashSettledTriggered = true;
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
if (mounted) unawaited(_dismissStartupSplashIfReady());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Theme(
|
return Theme(
|
||||||
data: AppTheme.dark(),
|
data: AppTheme.dark(),
|
||||||
child: Stack(
|
child: _buildHome(context, catalog),
|
||||||
fit: StackFit.expand,
|
|
||||||
children: [
|
|
||||||
_buildHome(context, catalog),
|
|
||||||
if (_startupSplashVisible)
|
|
||||||
_StartupSplashOverlay(fadingOut: _startupSplashFadingOut),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
Widget _buildHome(BuildContext context, CatalogController catalog) {
|
||||||
final homeFeed = _ensureHomeFeed(catalog);
|
final homeFeed = _ensureHomeFeed(catalog);
|
||||||
if (!_recommendSeeded && catalog.dramas.isNotEmpty) {
|
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 {
|
class _HomeTabs extends StatefulWidget {
|
||||||
const _HomeTabs({super.key, required this.feedController});
|
const _HomeTabs({super.key, required this.feedController});
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ void main() {
|
|||||||
AppConfig.ready = Future<void>.value();
|
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,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
final repository = _PendingCatalogRepository();
|
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-header')), findsOneWidget);
|
||||||
expect(find.byKey(const Key('home-grid-skeleton')), findsOneWidget);
|
expect(find.byKey(const Key('home-grid-skeleton')), findsOneWidget);
|
||||||
expect(find.byKey(const Key('home-tabs-bar')), 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')), findsNothing);
|
||||||
expect(find.byKey(const Key('startup-splash-logo')), findsOneWidget);
|
|
||||||
expect(find.byType(CircularProgressIndicator), findsNothing);
|
expect(find.byType(CircularProgressIndicator), findsNothing);
|
||||||
|
|
||||||
repository.complete(_feed);
|
repository.complete(_feed);
|
||||||
|
|||||||