diff --git a/README.md b/README.md index 8df317f..fdcb28a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # TPSchedule -学生课表 APP,Flutter 实现,目前接入电子科技大学研究生系统。Android 0.5.1+9,Android 7.0 及以上。 +学生课表 APP,Flutter 实现,目前接入电子科技大学研究生系统。Android 0.5.2+10,Android 7.0 及以上。 使用用户提供的原始 SVG 图标,蓝色渐变、黄色日历框及白色笔画保持一致;启动画面使用相同配色。0.5.1修复重复启动页闪烁:初始化期间保留原生启动屏,数据就绪后直接显示课表首帧,不再叠加Flutter品牌页或淡出动画。Android12+ 原生启动屏使用同色纯蓝背景及图标,旧版采用蓝色渐变。 @@ -8,6 +8,8 @@ 课表周一至周日固定七列同屏,无横向滚动。同节数课程条等长;课程名可省略,老师和教室完整换行,点击查看全部信息。课程条默认长度125%,在「个人设置 → 课表设置 → 课程条长度」调节80%–200%。课前提醒、手机日历及学期设置继续保留。 +0.5.2按新的Apple Music参考图调整Dock:底座保持通透,增强边缘折射并收窄柔化亮边;静止滑块为明确的半透明中性灰,按住时过渡回原有通透玻璃。滑动时的放大比例、弹簧参数和点击切换逻辑不变。蓝色实心图标保留。详见[0.5.2验证记录](docs/tpschedule-0.5.2-validation.md)。 + ## 安装与学校登录 安装 `dist/TPSchedule.apk`(release运行模式),或使用 `dist/TPSchedule-debug.apk` 测试。Android包名已按用户确认改为 `com.tpschedule.app`,显示名称为TPSchedule。它与旧 `com.example.hepan_everyday` 是两个独立应用,不能覆盖安装旧RSchedule;旧应用与数据保留,新应用需要重新导入课程并设置日期。 diff --git a/docs/tpschedule-0.5.2-validation.md b/docs/tpschedule-0.5.2-validation.md new file mode 100644 index 0000000..196e0ca --- /dev/null +++ b/docs/tpschedule-0.5.2-validation.md @@ -0,0 +1,21 @@ +# TPSchedule Android 0.5.2+10:参考图液态玻璃 + +日期:2026-10-14。用户参考图为Apple Music底部玻璃控件,要求静止滑块灰色,滑动时保留现有Q弹和放大效果。 + +底座折射强度从0.55调整为0.95,轻度模糊从2降至1.4,保留可辨认的背景色彩;边缘柔化范围收窄,清晰亮边加强。滑块静止时使用26–34%的中性灰覆盖,减少静止时的亮边,使其显得更像参考图中的灰色选中区域。按住时过渡到原有的约1–2.5%覆盖,恢复通透玻璃和亮边。 + +滑块的按压放大、拖动速度形变、弹簧参数、点击平滑切换、几何尺寸和选中图标不改。图标保留蓝色填充。 + +新增像素回归测试已在修改前失败:旧滑块相对白背景仅约4色阶差,无法形成明确灰色。修改后静止灰色相对底座超过20色阶,并在按住时变得明显更透明。完整142项Flutter测试通过,包括真实着色器、较大背景图案辨认、细节柔化、灰色/通透状态转换、放大、回弹和点击无抖动。较大纹理对比保留超过90;细小纹理至少衰减三分之二,避免改变模糊强度时使用只对旧参数有效的阈值。 + +测试使用--enable-impeller。默认受限环境的Flutter引擎无法解析自身程序路径,改用正常执行权限完成验证,没有修改工具链或运行时文件。组件渲染图片仅用于检查材质,测试环境图标字体为占位方框,实际图标以安卓界面为准。 + +启动闪烁修复、学校登录、课表与考试、学期选择、提醒和日历设置保留。应用ID仍为com.tpschedule.app,原签名配置不变。 + +## 安卓检查 + +API36模拟器覆盖安装成功,原有课表保留。已查看静止灰色及按压放大后的实际界面;图片位于dist/screenshots/tpschedule-week.png和tpschedule-dock-held.png。模拟器使用软件渲染,显示兼容玻璃效果;支持折射的渲染路径另有实际像素测试,具体手机视觉仍需实机确认。 + +发布APK名称TPSchedule,versionName0.5.2/versionCode10,minSdk24/targetSdk36,无debuggable标记。构建脚本验证原证书指纹后才复制安装包;公开信息提取也确认新旧证书DER完全一致,因此公钥未变。私钥未更换或导出。 + +发布APK SHA256:A307DDF0711A6AE38BA628246714FF2F059605F5D4DA12F0479E17FE5CD47557。 diff --git a/mobile/lib/widgets/glass_dock.dart b/mobile/lib/widgets/glass_dock.dart index 0008932..0a4f612 100644 --- a/mobile/lib/widgets/glass_dock.dart +++ b/mobile/lib/widgets/glass_dock.dart @@ -117,7 +117,7 @@ class _GlassDockState extends State with TickerProviderStateMixin { Widget _glass({required Widget child, bool lens = false}) { final pressure = _pressure.clamp(0.0, 1.0); final content = CustomPaint( - foregroundPainter: _GlassRimPainter(lens: lens), + foregroundPainter: _GlassRimPainter(lens: lens, pressure: pressure), child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( @@ -125,17 +125,26 @@ class _GlassDockState extends State with TickerProviderStateMixin { end: Alignment.bottomRight, colors: lens ? [ - const Color(0xFFD8DBE0) - .withValues(alpha: 0.14 * (1 - pressure * 0.82)), - const Color(0xFFC3C7CE) - .withValues(alpha: 0.08 * (1 - pressure * 0.82)), - const Color(0xFFE5E7EB) - .withValues(alpha: 0.12 * (1 - pressure * 0.82)), + Color.lerp( + const Color(0xFFA1A4AA).withValues(alpha: 0.34), + const Color(0xFFD8DBE0).withValues(alpha: 0.0252), + pressure, + )!, + Color.lerp( + const Color(0xFF8E939B).withValues(alpha: 0.26), + const Color(0xFFC3C7CE).withValues(alpha: 0.0144), + pressure, + )!, + Color.lerp( + const Color(0xFFB4B7BC).withValues(alpha: 0.32), + const Color(0xFFE5E7EB).withValues(alpha: 0.0216), + pressure, + )!, ] : [ - Colors.white.withValues(alpha: 0.028), - _blue.withValues(alpha: 0.005), - Colors.white.withValues(alpha: 0.012), + Colors.white.withValues(alpha: 0.05), + _blue.withValues(alpha: 0.008), + Colors.white.withValues(alpha: 0.018), ], ), ), @@ -145,8 +154,8 @@ class _GlassDockState extends State with TickerProviderStateMixin { return ClipRRect( borderRadius: BorderRadius.circular(40), child: GlassRefraction( - strength: lens ? 0.70 + pressure * 0.70 : 0.55, - blurSigma: lens ? 1.6 - pressure * 0.6 : 2, + strength: lens ? 0.70 + pressure * 0.70 : 0.95, + blurSigma: lens ? 1.6 - pressure * 0.6 : 1.4, child: content, ), ); @@ -292,11 +301,14 @@ class _GlassDockState extends State with TickerProviderStateMixin { /// A diffuse silver bevel and thin directional glints show optical thickness. /// The painted highlights stay on the edge so background colour passes through. class _GlassRimPainter extends CustomPainter { - const _GlassRimPainter({required this.lens}); + const _GlassRimPainter({required this.lens, required this.pressure}); final bool lens; + final double pressure; @override void paint(Canvas canvas, Size size) { final edge = lens ? const Color(0xFF7B838F) : const Color(0xFFAFBECF); + // A quiet grey selection at rest; the glossy lens rim returns on touch. + final opacity = lens ? 0.25 + pressure * 0.75 : 1.0; final bounds = Offset.zero & size; final outer = RRect.fromRectAndRadius( bounds.deflate(0.7), @@ -311,14 +323,14 @@ class _GlassRimPainter extends CustomPainter { Paint() ..style = PaintingStyle.stroke ..strokeWidth = lens ? 1.8 : 2.2 - ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 0.9) + ..maskFilter = MaskFilter.blur(BlurStyle.normal, lens ? 0.9 : 0.45) ..shader = ui.Gradient.linear( bounds.topLeft, bounds.bottomRight, [ - Colors.white.withValues(alpha: 0.24), - edge.withValues(alpha: 0.04), - Colors.white.withValues(alpha: 0.14), + Colors.white.withValues(alpha: 0.24 * opacity), + edge.withValues(alpha: 0.04 * opacity), + Colors.white.withValues(alpha: 0.14 * opacity), ], [0.0, 0.55, 1.0], ), @@ -331,12 +343,12 @@ class _GlassRimPainter extends CustomPainter { ..shader = ui.Gradient.sweep( bounds.center, [ - Colors.white.withValues(alpha: 0.90), - edge.withValues(alpha: lens ? 0.30 : 0.15), - Colors.white.withValues(alpha: 0.10), - Colors.white.withValues(alpha: 0.86), - edge.withValues(alpha: 0.12), - Colors.white.withValues(alpha: 0.90), + Colors.white.withValues(alpha: 0.96 * opacity), + edge.withValues(alpha: (lens ? 0.30 : 0.15) * opacity), + Colors.white.withValues(alpha: 0.10 * opacity), + Colors.white.withValues(alpha: 0.94 * opacity), + edge.withValues(alpha: 0.12 * opacity), + Colors.white.withValues(alpha: 0.96 * opacity), ], [0.0, 0.18, 0.38, 0.60, 0.80, 1.0], TileMode.clamp, @@ -353,9 +365,9 @@ class _GlassRimPainter extends CustomPainter { bounds.topLeft, bounds.bottomRight, [ - Colors.white.withValues(alpha: 0.20), - edge.withValues(alpha: lens ? 0.13 : 0.07), - Colors.white.withValues(alpha: 0.28), + Colors.white.withValues(alpha: 0.20 * opacity), + edge.withValues(alpha: (lens ? 0.13 : 0.07) * opacity), + Colors.white.withValues(alpha: 0.28 * opacity), ], [0.0, 0.5, 1.0], ), @@ -379,7 +391,7 @@ class _GlassRimPainter extends CustomPainter { Offset(size.width, 0), [ Colors.white.withValues(alpha: 0), - Colors.white.withValues(alpha: 0.50), + Colors.white.withValues(alpha: 0.50 * opacity), Colors.white.withValues(alpha: 0), ], [0.0, 0.5, 1.0], @@ -388,5 +400,6 @@ class _GlassRimPainter extends CustomPainter { } @override - bool shouldRepaint(_GlassRimPainter oldDelegate) => oldDelegate.lens != lens; + bool shouldRepaint(_GlassRimPainter oldDelegate) => + oldDelegate.lens != lens || oldDelegate.pressure != pressure; } diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 5428b72..914a630 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -1,7 +1,7 @@ name: hepan_everyday description: TPSchedule 学生课表 publish_to: none -version: 0.5.1+9 +version: 0.5.2+10 environment: sdk: ^3.13.5 dependencies: diff --git a/mobile/test/widgets/glass_material_test.dart b/mobile/test/widgets/glass_material_test.dart index 27addb2..7e9f278 100644 --- a/mobile/test/widgets/glass_material_test.dart +++ b/mobile/test/widgets/glass_material_test.dart @@ -9,6 +9,28 @@ import 'package:hepan_everyday/widgets/glass_dock.dart'; import 'package:hepan_everyday/widgets/glass_refraction.dart'; void main() { + testWidgets('resting cursor is neutral grey and clears while held', ( + tester, + ) async { + final resting = await _render( + tester, + GlassDock(selectedIndex: 0, onSelected: (_) {}), + backdropColor: Colors.white, + preview: true, + ); + final selected = _channel(resting, 46, 52, 0); + final base = _channel(resting, 160, 52, 0); + expect(base - selected, greaterThan(20)); + expect((_channel(resting, 46, 52, 2) - selected).abs(), lessThan(12)); + final held = await _render( + tester, + GlassDock(selectedIndex: 0, onSelected: (_) {}), + backdropColor: Colors.white, + held: true, + preview: true, + ); + expect(_channel(held, 46, 52, 0) - selected, greaterThan(18)); + }); testWidgets( 'glass softens fine background detail without whitening its colour', (tester) async { @@ -37,13 +59,13 @@ void main() { GlassDock(selectedIndex: 0, onSelected: (_) {}), ); // Below the symbol and away from the rim, the selected lens should retain - // most of this saturated cyan backdrop instead of painting a grey pill. + // this saturated cyan backdrop while applying the new resting grey tint. final red = _channel(pixels, 46, 52, 0); final green = _channel(pixels, 46, 52, 1); final blue = _channel(pixels, 46, 52, 2); - expect(red, lessThan(50)); - expect(green - red, greaterThan(130)); - expect(blue - red, greaterThan(165)); + expect(red, lessThan(85)); + expect(green - red, greaterThan(95)); + expect(blue - red, greaterThan(140)); }, ); @@ -63,7 +85,9 @@ void main() { final contrast = reds.reduce((a, b) => a > b ? a : b) - reds.reduce((a, b) => a < b ? a : b); - expect(contrast, lessThan(30), reason: 'background at x=$start'); + // Fine stripes start at 180 contrast levels; at least two thirds softens, + // while the stronger curved rim can enlarge individual stripe details. + expect(contrast, lessThan(60), reason: 'background at x=$start'); } }); @@ -140,6 +164,8 @@ Future _render( bool gradient = false, bool preview = false, Offset offset = Offset.zero, + Color backdropColor = const Color(0xFF14B4E6), + bool held = false, }) async { tester.view.physicalSize = const Size(800, 600); tester.view.devicePixelRatio = 1; @@ -161,7 +187,12 @@ Future _render( children: [ Positioned.fill( child: CustomPaint( - painter: _Backdrop(stripes, gradient, stripeWidth), + painter: _Backdrop( + stripes, + gradient, + stripeWidth, + backdropColor, + ), ), ), Positioned.fill(child: glass), @@ -180,10 +211,19 @@ Future _render( ); } await tester.pumpAndSettle(); + final gesture = held + ? await tester.startGesture( + tester.getCenter(find.byKey(const Key('dock-cursor'))), + ) + : null; + if (held) { + await tester.pump(const Duration(milliseconds: 110)); + await tester.pump(const Duration(milliseconds: 200)); + } if (ui.ImageFilter.isShaderFilterSupported) { expect(find.byType(BackdropFilter), findsNothing); } - return (await tester.runAsync(() async { + final result = (await tester.runAsync(() async { final boundary = key.currentContext!.findRenderObject()! as RenderRepaintBoundary; final image = await boundary.toImage(pixelRatio: 1); @@ -202,7 +242,12 @@ Future _render( final png = (await previewImage.toByteData( format: ui.ImageByteFormat.png, ))!; - final output = File('../dist/screenshots/glass-frosted-dock.png'); + final name = backdropColor == Colors.white + ? held + ? 'liquid-glass-held.png' + : 'liquid-glass-rest.png' + : 'liquid-glass-detail.png'; + final output = File('../dist/screenshots/$name'); await output.parent.create(recursive: true); await output.writeAsBytes(png.buffer.asUint8List()); previewImage.dispose(); @@ -225,13 +270,17 @@ Future _render( image.dispose(); return cropped; }))!; + await gesture?.cancel(); + await tester.pumpAndSettle(); + return result; } class _Backdrop extends CustomPainter { - const _Backdrop(this.stripes, this.gradient, this.stripeWidth); + const _Backdrop(this.stripes, this.gradient, this.stripeWidth, this.colour); final bool stripes; final bool gradient; final double stripeWidth; + final Color colour; @override void paint(Canvas canvas, Size size) { @@ -245,10 +294,7 @@ class _Backdrop extends CustomPainter { return; } if (!stripes) { - canvas.drawRect( - Offset.zero & size, - Paint()..color = const Color(0xFF14B4E6), - ); + canvas.drawRect(Offset.zero & size, Paint()..color = colour); return; } for (var x = 0.0; x < size.width; x += stripeWidth) { @@ -266,5 +312,6 @@ class _Backdrop extends CustomPainter { bool shouldRepaint(_Backdrop oldDelegate) => oldDelegate.stripes != stripes || oldDelegate.gradient != gradient || - oldDelegate.stripeWidth != stripeWidth; + oldDelegate.stripeWidth != stripeWidth || + oldDelegate.colour != colour; }