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), + ), + ); + } +}