diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml
index f74085f..2995854 100644
--- a/android/app/src/main/res/drawable-v21/launch_background.xml
+++ b/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -1,12 +1,12 @@
-
-
-
-
-
+ -
+
+
+
+
diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml
index 304732f..2995854 100644
--- a/android/app/src/main/res/drawable/launch_background.xml
+++ b/android/app/src/main/res/drawable/launch_background.xml
@@ -1,12 +1,12 @@
-
-
-
-
-
+ -
+
+
+
+
diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml
index 06952be..2bf52b9 100644
--- a/android/app/src/main/res/values-night/styles.xml
+++ b/android/app/src/main/res/values-night/styles.xml
@@ -13,6 +13,6 @@
This Theme is only used starting with V2 of Flutter's Android embedding. -->
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
index cb1ef88..dcbf9a7 100644
--- a/android/app/src/main/res/values/styles.xml
+++ b/android/app/src/main/res/values/styles.xml
@@ -13,6 +13,6 @@
This Theme is only used starting with V2 of Flutter's Android embedding. -->
diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard
index f2e259c..8ad8574 100644
--- a/ios/Runner/Base.lproj/LaunchScreen.storyboard
+++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -19,7 +19,7 @@
-
+
diff --git a/lib/main.dart b/lib/main.dart
index 947ee45..dc936d3 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -11,6 +11,10 @@ import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
import 'config/app_config.dart';
final _homeUrl = AppConfig.homeUrl;
+const _shellBackground = Color(0xFFF8FBFF);
+const _shellAccent = Color(0xFF0089FF);
+const _shellSubText = Color(0xFF8E9AB0);
+const _resumeCoverDuration = Duration(milliseconds: 700);
const _stopWebMediaScript = r'''
(() => {
try {
@@ -31,7 +35,7 @@ void main() {
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
- systemNavigationBarColor: Colors.white,
+ systemNavigationBarColor: _shellBackground,
systemNavigationBarIconBrightness: Brightness.dark,
),
);
@@ -48,7 +52,7 @@ class ImWebViewApp extends StatelessWidget {
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1F6FEB)),
- scaffoldBackgroundColor: Colors.white,
+ scaffoldBackgroundColor: _shellBackground,
useMaterial3: true,
),
home: const H5ShellPage(),
@@ -68,6 +72,8 @@ class _H5ShellPageState extends State with WidgetsBindingObserver {
int _progress = 0;
String? _loadError;
+ bool _showShellCover = true;
+ Timer? _shellCoverTimer;
@override
void initState() {
@@ -78,6 +84,7 @@ class _H5ShellPageState extends State with WidgetsBindingObserver {
@override
void dispose() {
+ _shellCoverTimer?.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@@ -89,6 +96,8 @@ class _H5ShellPageState extends State with WidgetsBindingObserver {
state == AppLifecycleState.paused ||
state == AppLifecycleState.detached) {
unawaited(_stopWebMedia());
+ } else if (state == AppLifecycleState.resumed) {
+ _showTransientShellCover();
}
}
@@ -113,7 +122,7 @@ class _H5ShellPageState extends State with WidgetsBindingObserver {
},
)
..setJavaScriptMode(JavaScriptMode.unrestricted)
- ..setBackgroundColor(Colors.white)
+ ..setBackgroundColor(_shellBackground)
..setNavigationDelegate(
NavigationDelegate(
onProgress: (progress) {
@@ -126,18 +135,25 @@ class _H5ShellPageState extends State with WidgetsBindingObserver {
setState(() {
_loadError = null;
_progress = 0;
+ _showShellCover = true;
});
}
},
onPageFinished: (_) {
if (mounted) {
- setState(() => _progress = 100);
+ setState(() {
+ _progress = 100;
+ _showShellCover = false;
+ });
}
},
onWebResourceError: (error) {
if (error.isForMainFrame ?? true) {
if (mounted) {
- setState(() => _loadError = error.description);
+ setState(() {
+ _loadError = error.description;
+ _showShellCover = false;
+ });
}
}
},
@@ -170,6 +186,18 @@ class _H5ShellPageState extends State with WidgetsBindingObserver {
return controller;
}
+ void _showTransientShellCover() {
+ _shellCoverTimer?.cancel();
+ if (mounted) {
+ setState(() => _showShellCover = true);
+ }
+ _shellCoverTimer = Timer(_resumeCoverDuration, () {
+ if (mounted) {
+ setState(() => _showShellCover = _progress < 100);
+ }
+ });
+ }
+
Future _runJavaScriptSafely(String source) async {
try {
await _controller.runJavaScript(source);
@@ -252,11 +280,17 @@ class _H5ShellPageState extends State with WidgetsBindingObserver {
}
},
child: Scaffold(
+ backgroundColor: _shellBackground,
body: SafeArea(
bottom: false,
child: Stack(
children: [
+ const Positioned.fill(child: _ShellFallback()),
WebViewWidget(controller: _controller),
+ if (_showShellCover)
+ const Positioned.fill(
+ child: IgnorePointer(child: _ShellFallback()),
+ ),
if (_progress < 100)
LinearProgressIndicator(
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),
+ ),
+ );
+ }
+}