diff --git a/android/app/src/main/res/drawable-nodpi/splash_android12_icon.png b/android/app/src/main/res/drawable-nodpi/splash_android12_icon.png index 0ba6084..0b9336c 100644 Binary files a/android/app/src/main/res/drawable-nodpi/splash_android12_icon.png and b/android/app/src/main/res/drawable-nodpi/splash_android12_icon.png differ diff --git a/android/app/src/main/res/drawable-nodpi/splash_content.png b/android/app/src/main/res/drawable-nodpi/splash_content.png index 40530dc..66e7749 100644 Binary files a/android/app/src/main/res/drawable-nodpi/splash_content.png and b/android/app/src/main/res/drawable-nodpi/splash_content.png differ diff --git a/lib/main.dart b/lib/main.dart index 720702b..f10b2ed 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -319,13 +319,15 @@ class _H5ShellPageState extends State { bottom: false, child: Stack( children: [ - const Positioned.fill(child: _ShellFallback()), + Positioned.fill(child: _ShellFallback(progress: _progress)), WebViewWidget(controller: _controller), if (_showShellCover) - const Positioned.fill( - child: IgnorePointer(child: _ShellFallback()), + Positioned.fill( + child: IgnorePointer( + child: _ShellFallback(progress: _progress), + ), ), - if (_progress < 100) + if (!_showShellCover && _progress < 100) LinearProgressIndicator( value: _progress == 0 ? null : _progress / 100, minHeight: 2, @@ -385,131 +387,266 @@ class _ErrorPanel extends StatelessWidget { } class _ShellFallback extends StatelessWidget { - const _ShellFallback(); + const _ShellFallback({required this.progress}); + + final int progress; @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, + final progressValue = + progress <= 0 ? 0.42 : (0.18 + progress.clamp(0, 100) * 0.0074); + + return CustomPaint( + painter: const _H5LoadingBackgroundPainter(), + child: LayoutBuilder( + builder: (context, constraints) { + final markSize = (constraints.maxWidth * 0.37).clamp(118.0, 152.0); + final contentTop = constraints.maxHeight * 0.29; + final progressWidth = + (constraints.maxWidth * 0.28).clamp(104.0, 132.0); + + return Align( + alignment: Alignment.topCenter, + child: Padding( + padding: EdgeInsets.only(top: contentTop), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _ShellMark(size: markSize), + SizedBox(height: markSize * 0.16), + const Text( + '心有回响', + style: TextStyle( + color: _shellAccent, + fontSize: 24, + height: 1.2, + fontWeight: FontWeight.w700, + letterSpacing: 0, + ), ), - ), - SizedBox(height: 10), - Text( - '正在为你唤醒会话', - style: TextStyle( - color: _shellSubText, - fontSize: 13, - height: 1.2, - letterSpacing: 0, + const SizedBox(height: 8), + const Text( + '正在为你唤醒会话', + style: TextStyle( + color: _shellSubText, + fontSize: 12, + height: 1.2, + letterSpacing: 0, + ), ), - ), - SizedBox(height: 32), - SizedBox( - width: 110, - child: LinearProgressIndicator( - minHeight: 3, - backgroundColor: Color(0xFFDCEEFF), - color: _shellAccent, + const SizedBox(height: 30), + _H5LoadingProgress( + width: progressWidth, + value: progressValue.clamp(0.18, 0.96), ), - ), - ], + ], + ), ), - ), - ], + ); + }, ), ); } } 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}); + const _ShellMark({required this.size}); 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), + return CustomPaint( + size: Size(size, size * 0.92), + painter: const _BubbleCheckPainter(), + ); + } +} + +class _H5LoadingProgress extends StatelessWidget { + const _H5LoadingProgress({required this.width, required this.value}); + + final double width; + final double value; + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(99), + child: SizedBox( + width: width, + height: 4, + child: Stack( + fit: StackFit.expand, + children: [ + const ColoredBox(color: Color(0xFFDCEEFF)), + TweenAnimationBuilder( + tween: Tween(end: value), + duration: const Duration(milliseconds: 260), + curve: Curves.easeOutCubic, + builder: (context, animatedValue, _) { + return FractionallySizedBox( + alignment: Alignment.centerLeft, + widthFactor: animatedValue, + child: const ColoredBox(color: _shellAccent), + ); + }, + ), + ], + ), ), ); } } + +class _H5LoadingBackgroundPainter extends CustomPainter { + const _H5LoadingBackgroundPainter(); + + static const _sourceSize = Size(1080, 2400); + + @override + void paint(Canvas canvas, Size size) { + final scale = + (size.width / _sourceSize.width) > (size.height / _sourceSize.height) + ? size.width / _sourceSize.width + : size.height / _sourceSize.height; + final dx = (size.width - _sourceSize.width * scale) / 2; + final dy = (size.height - _sourceSize.height * scale) / 2; + + canvas.save(); + canvas.translate(dx, dy); + canvas.scale(scale); + + final pageRect = Offset.zero & _sourceSize; + final pagePaint = Paint() + ..shader = const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFFE7F4FF), + _shellBackground, + Colors.white, + ], + stops: [0, 0.46, 1], + ).createShader(pageRect); + canvas.drawRect(pageRect, pagePaint); + + final topWave = Path() + ..moveTo(0, 0) + ..lineTo(1080, 0) + ..lineTo(1080, 318) + ..cubicTo(920, 246, 780, 246, 660, 318) + ..cubicTo(512, 407, 387, 410, 284, 348) + ..cubicTo(174, 282, 79, 281, 0, 344) + ..close(); + canvas.drawPath( + topWave, + Paint()..color = const Color(0xFFDDF1FF).withValues(alpha: 0.68), + ); + + final bottomWave = Path() + ..moveTo(1080, 855) + ..lineTo(1080, 2400) + ..lineTo(0, 2400) + ..lineTo(0, 1942) + ..cubicTo(124, 1854, 265, 1834, 423, 1883) + ..cubicTo(598, 1937, 744, 1905, 862, 1788) + ..cubicTo(934, 1717, 1007, 1678, 1080, 1672) + ..close(); + canvas.drawPath( + bottomWave, + Paint()..color = const Color(0xFFF4FAFF).withValues(alpha: 0.86), + ); + + canvas.restore(); + } + + @override + bool shouldRepaint(covariant _H5LoadingBackgroundPainter oldDelegate) { + return false; + } +} + +class _BubbleCheckPainter extends CustomPainter { + const _BubbleCheckPainter(); + + static const _viewBox = Size(400, 363.33); + + @override + void paint(Canvas canvas, Size size) { + final scale = + (size.width / _viewBox.width) < (size.height / _viewBox.height) + ? size.width / _viewBox.width + : size.height / _viewBox.height; + final dx = (size.width - _viewBox.width * scale) / 2; + final dy = (size.height - _viewBox.height * scale) / 2; + + canvas.save(); + canvas.translate(dx, dy); + canvas.scale(scale); + + final bubble = Path() + ..moveTo(200, 0) + ..cubicTo(89.54, 0, 0, 81.33, 0, 181.67) + ..cubicTo(0, 282, 89.54, 363.33, 200, 363.33) + ..cubicTo(237.89, 363.33, 273.32, 353.77, 303.52, 337.16) + ..lineTo(386.67, 363.33) + ..lineTo(355.95, 290.69) + ..cubicTo(383.61, 260.37, 400, 222.36, 400, 181.67) + ..cubicTo(400, 81.33, 310.46, 0, 200, 0) + ..close(); + + final shadowPaint = Paint() + ..color = const Color(0xFF026CD5).withValues(alpha: 0.18) + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 34); + canvas.save(); + canvas.translate(0, 30); + canvas.drawPath(bubble, shadowPaint); + canvas.restore(); + + final bubblePaint = Paint() + ..shader = const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFFA8DDFF), + _shellAccent, + Color(0xFF0066E8), + ], + stops: [0, 0.48, 1], + ).createShader(Offset.zero & _viewBox); + canvas.drawPath(bubble, bubblePaint); + + final highlightPaint = Paint() + ..shader = LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white.withValues(alpha: 0.38), + Colors.white.withValues(alpha: 0), + ], + ).createShader(Offset.zero & _viewBox); + canvas.drawPath(bubble, highlightPaint); + + final check = Path() + ..moveTo(101, 163.5) + ..lineTo(170.5, 233) + ..lineTo(306, 97.5); + canvas.drawPath( + check, + Paint() + ..color = Colors.white + ..style = PaintingStyle.stroke + ..strokeWidth = 58 + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round, + ); + + canvas.restore(); + } + + @override + bool shouldRepaint(covariant _BubbleCheckPainter oldDelegate) { + return false; + } +}