feat: 更新启动背景和主题样式,优化加载界面

This commit is contained in:
Booker
2026-05-20 08:50:22 +07:00
parent faeb407e0e
commit f465d5ce45
6 changed files with 190 additions and 26 deletions

View File

@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<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="?android:colorBackground" /> <item>
<shape android:shape="rectangle">
<!-- You can insert your own image assets here --> <gradient
<!-- <item> android:angle="270"
<bitmap android:startColor="#E7F4FF"
android:gravity="center" android:centerColor="#F8FBFF"
android:src="@mipmap/launch_image" /> android:endColor="#FFFFFF" />
</item> --> </shape>
</item>
</layer-list> </layer-list>

View File

@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<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="@android:color/white" /> <item>
<shape android:shape="rectangle">
<!-- You can insert your own image assets here --> <gradient
<!-- <item> android:angle="270"
<bitmap android:startColor="#E7F4FF"
android:gravity="center" android:centerColor="#F8FBFF"
android:src="@mipmap/launch_image" /> android:endColor="#FFFFFF" />
</item> --> </shape>
</item>
</layer-list> </layer-list>

View File

@@ -13,6 +13,6 @@
This Theme is only used starting with V2 of Flutter's Android embedding. --> This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar"> <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item> <item name="android:windowBackground">@drawable/launch_background</item>
</style> </style>
</resources> </resources>

View File

@@ -13,6 +13,6 @@
This Theme is only used starting with V2 of Flutter's Android embedding. --> This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar"> <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item> <item name="android:windowBackground">@drawable/launch_background</item>
</style> </style>
</resources> </resources>

View File

@@ -19,7 +19,7 @@
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" 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> </imageView>
</subviews> </subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="backgroundColor" red="0.97254901960784312" green="0.98431372549019602" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints> <constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/> <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/> <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>

View File

@@ -11,6 +11,10 @@ import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
import 'config/app_config.dart'; import 'config/app_config.dart';
final _homeUrl = AppConfig.homeUrl; final _homeUrl = AppConfig.homeUrl;
const _shellBackground = Color(0xFFF8FBFF);
const _shellAccent = Color(0xFF0089FF);
const _shellSubText = Color(0xFF8E9AB0);
const _resumeCoverDuration = Duration(milliseconds: 700);
const _stopWebMediaScript = r''' const _stopWebMediaScript = r'''
(() => { (() => {
try { try {
@@ -31,7 +35,7 @@ void main() {
const SystemUiOverlayStyle( const SystemUiOverlayStyle(
statusBarColor: Colors.transparent, statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark, statusBarIconBrightness: Brightness.dark,
systemNavigationBarColor: Colors.white, systemNavigationBarColor: _shellBackground,
systemNavigationBarIconBrightness: Brightness.dark, systemNavigationBarIconBrightness: Brightness.dark,
), ),
); );
@@ -48,7 +52,7 @@ class ImWebViewApp extends StatelessWidget {
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData( theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1F6FEB)), colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1F6FEB)),
scaffoldBackgroundColor: Colors.white, scaffoldBackgroundColor: _shellBackground,
useMaterial3: true, useMaterial3: true,
), ),
home: const H5ShellPage(), home: const H5ShellPage(),
@@ -68,6 +72,8 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
int _progress = 0; int _progress = 0;
String? _loadError; String? _loadError;
bool _showShellCover = true;
Timer? _shellCoverTimer;
@override @override
void initState() { void initState() {
@@ -78,6 +84,7 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
@override @override
void dispose() { void dispose() {
_shellCoverTimer?.cancel();
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
super.dispose(); super.dispose();
} }
@@ -89,6 +96,8 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
state == AppLifecycleState.paused || state == AppLifecycleState.paused ||
state == AppLifecycleState.detached) { state == AppLifecycleState.detached) {
unawaited(_stopWebMedia()); unawaited(_stopWebMedia());
} else if (state == AppLifecycleState.resumed) {
_showTransientShellCover();
} }
} }
@@ -113,7 +122,7 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
}, },
) )
..setJavaScriptMode(JavaScriptMode.unrestricted) ..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(Colors.white) ..setBackgroundColor(_shellBackground)
..setNavigationDelegate( ..setNavigationDelegate(
NavigationDelegate( NavigationDelegate(
onProgress: (progress) { onProgress: (progress) {
@@ -126,18 +135,25 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
setState(() { setState(() {
_loadError = null; _loadError = null;
_progress = 0; _progress = 0;
_showShellCover = true;
}); });
} }
}, },
onPageFinished: (_) { onPageFinished: (_) {
if (mounted) { if (mounted) {
setState(() => _progress = 100); setState(() {
_progress = 100;
_showShellCover = false;
});
} }
}, },
onWebResourceError: (error) { onWebResourceError: (error) {
if (error.isForMainFrame ?? true) { if (error.isForMainFrame ?? true) {
if (mounted) { if (mounted) {
setState(() => _loadError = error.description); setState(() {
_loadError = error.description;
_showShellCover = false;
});
} }
} }
}, },
@@ -170,6 +186,18 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
return controller; return controller;
} }
void _showTransientShellCover() {
_shellCoverTimer?.cancel();
if (mounted) {
setState(() => _showShellCover = true);
}
_shellCoverTimer = Timer(_resumeCoverDuration, () {
if (mounted) {
setState(() => _showShellCover = _progress < 100);
}
});
}
Future<void> _runJavaScriptSafely(String source) async { Future<void> _runJavaScriptSafely(String source) async {
try { try {
await _controller.runJavaScript(source); await _controller.runJavaScript(source);
@@ -252,11 +280,17 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
} }
}, },
child: Scaffold( child: Scaffold(
backgroundColor: _shellBackground,
body: SafeArea( body: SafeArea(
bottom: false, bottom: false,
child: Stack( child: Stack(
children: [ children: [
const Positioned.fill(child: _ShellFallback()),
WebViewWidget(controller: _controller), WebViewWidget(controller: _controller),
if (_showShellCover)
const Positioned.fill(
child: IgnorePointer(child: _ShellFallback()),
),
if (_progress < 100) if (_progress < 100)
LinearProgressIndicator( LinearProgressIndicator(
value: _progress == 0 ? null : _progress / 100, value: _progress == 0 ? null : _progress / 100,
@@ -315,3 +349,133 @@ class _ErrorPanel extends StatelessWidget {
); );
} }
} }
class _ShellFallback extends StatelessWidget {
const _ShellFallback();
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFFE7F4FF),
_shellBackground,
Colors.white,
],
stops: [0, 0.44, 1],
),
),
child: Stack(
children: [
const Positioned(
top: 44,
right: -72,
child: _SoftCircle(size: 190, opacity: 0.42),
),
const Positioned(
top: 210,
left: -44,
child: _SoftCircle(size: 160, opacity: 0.34),
),
const Positioned(
right: -54,
bottom: 250,
child: _SoftCircle(size: 220, opacity: 0.38),
),
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: const [
_ShellMark(),
SizedBox(height: 26),
Text(
'心有回响',
style: TextStyle(
color: _shellAccent,
fontSize: 26,
height: 1.2,
fontWeight: FontWeight.w700,
letterSpacing: 0,
),
),
SizedBox(height: 10),
Text(
'正在为你唤醒会话',
style: TextStyle(
color: _shellSubText,
fontSize: 13,
height: 1.2,
letterSpacing: 0,
),
),
SizedBox(height: 32),
SizedBox(
width: 110,
child: LinearProgressIndicator(
minHeight: 3,
backgroundColor: Color(0xFFDCEEFF),
color: _shellAccent,
),
),
],
),
),
],
),
);
}
}
class _ShellMark extends StatelessWidget {
const _ShellMark();
@override
Widget build(BuildContext context) {
return Container(
width: 126,
height: 126,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFFA8DDFF), _shellAccent, Color(0xFF0066E8)],
),
boxShadow: [
BoxShadow(
color: _shellAccent.withValues(alpha: 0.2),
blurRadius: 34,
offset: const Offset(0, 18),
),
],
),
child: const Icon(
Icons.check_rounded,
color: Colors.white,
size: 82,
),
);
}
}
class _SoftCircle extends StatelessWidget {
const _SoftCircle({required this.size, required this.opacity});
final double size;
final double opacity;
@override
Widget build(BuildContext context) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFFBFE4FF).withValues(alpha: opacity),
),
);
}
}