fix: keep dock drag springs live and add elastic leading-edge pull

This commit is contained in:
david1465833828
2026-10-28 11:57:44 +08:00
parent 6604f31d98
commit c5cfbced5c
9 changed files with 261 additions and 42 deletions
+2 -2
View File
@@ -1,10 +1,10 @@
# TPSchedule # TPSchedule
当前Android版本0.7.8+30:增强点按挤压回弹、整条Dock随拖动方向弹动、图标到位动画及乳白折射光泽,见[本次记录](docs/tpschedule-0.7.8-dock-spring.md)。构建29提供Dock点按/拖动弹性与Android平板横竖屏、分屏适配,见[本次验证记录](docs/tpschedule-0.7.8-dock-tablet.md)。构建28已提前建立通知类别、区分通知总开关和类别状态,见[通知排查记录](docs/tpschedule-0.7.8-notification-permissions.md)。用户手机已通过开启通知权限并重测确认恢复后台通知。Dock乳白透光层和边缘折射继续保留,见[材质优化记录](docs/tpschedule-0.7.8-dock-refinement.md)。本科/研究生HTML反馈见[0.7.8功能记录](docs/tpschedule-0.7.8-validation.md)及[反馈服务说明](server/feedback/html-diagnostic-update.md),更新管理后台见[部署说明](server/updates/README.md)。 当前Android版本0.7.8+31:滑块移动中持续Q弹、前缘牵拉和弹性跟随,玻璃材质减薄,见[本次记录](docs/tpschedule-0.7.8-dock-drag.md)。构建30提供增强点按挤压回弹、整条Dock随拖动方向弹动、图标到位动画及乳白折射光泽,见[本次记录](docs/tpschedule-0.7.8-dock-spring.md)。构建29提供Dock点按/拖动弹性与Android平板横竖屏、分屏适配,见[本次验证记录](docs/tpschedule-0.7.8-dock-tablet.md)。构建28已提前建立通知类别、区分通知总开关和类别状态,见[通知排查记录](docs/tpschedule-0.7.8-notification-permissions.md)。用户手机已通过开启通知权限并重测确认恢复后台通知。Dock乳白透光层和边缘折射继续保留,见[材质优化记录](docs/tpschedule-0.7.8-dock-refinement.md)。本科/研究生HTML反馈见[0.7.8功能记录](docs/tpschedule-0.7.8-validation.md)及[反馈服务说明](server/feedback/html-diagnostic-update.md),更新管理后台见[部署说明](server/updates/README.md)。
0.7.0补充:考试页查询类型与卡片保持16像素间距;反馈问题和建议可自选附带最近10分钟的登录/读取/提醒诊断日志。默认不发送,不含密码、Cookie或课表内容,单独作为邮件文本附件。服务端需按[日志附件更新说明](server/feedback/diagnostic-log-update.md)手动更新;真实学校登录及SMTP未由本次离线测试代替验证。玻璃取样偏移仍在调查,未纳入本次修复结论。 0.7.0补充:考试页查询类型与卡片保持16像素间距;反馈问题和建议可自选附带最近10分钟的登录/读取/提醒诊断日志。默认不发送,不含密码、Cookie或课表内容,单独作为邮件文本附件。服务端需按[日志附件更新说明](server/feedback/diagnostic-log-update.md)手动更新;真实学校登录及SMTP未由本次离线测试代替验证。玻璃取样偏移仍在调查,未纳入本次修复结论。
学生课表 APP,Flutter 实现,支持电子科技大学研究生和本科生系统。Android 0.7.8+30,Android 7.0 及以上。 学生课表 APP,Flutter 实现,支持电子科技大学研究生和本科生系统。Android 0.7.8+31,Android 7.0 及以上。
使用用户提供的原始 SVG 图标,蓝色渐变、黄色日历框及白色笔画保持一致;启动画面使用相同配色。0.5.1修复重复启动页闪烁:初始化期间保留原生启动屏,数据就绪后直接显示课表首帧,不再叠加Flutter品牌页或淡出动画。Android12+ 原生启动屏使用同色纯蓝背景及图标,旧版采用蓝色渐变。 使用用户提供的原始 SVG 图标,蓝色渐变、黄色日历框及白色笔画保持一致;启动画面使用相同配色。0.5.1修复重复启动页闪烁:初始化期间保留原生启动屏,数据就绪后直接显示课表首帧,不再叠加Flutter品牌页或淡出动画。Android12+ 原生启动屏使用同色纯蓝背景及图标,旧版采用蓝色渐变。
+1 -1
View File
@@ -1,6 +1,6 @@
# Android 首版验证记录 # Android 首版验证记录
当前版本为0.7.8+30,点按/方向弹动及乳白材质见[本次记录](tpschedule-0.7.8-dock-spring.md)。构建29的Dock弹性与Android平板验证见[本次记录](tpschedule-0.7.8-dock-tablet.md)。构建28的通知权限恢复、通道初始化及真实后台触发见[通知记录](tpschedule-0.7.8-notification-permissions.md)。Dock乳白透光材质见[优化记录](tpschedule-0.7.8-dock-refinement.md)。本科/研究生HTML反馈及签名、备案信息见[0.7.8功能记录](tpschedule-0.7.8-validation.md)。应用更新检查与后台说明见[0.7.6记录](tpschedule-0.7.6-updates.md)。本科考试兼容的验证见[0.7.5记录](tpschedule-0.7.5-validation.md)。两例真实课表缺失字段问题待新页面定位。本科接入记录见[0.7.0记录](tpschedule-0.7.0-validation.md)。下文为首版历史记录。 当前版本为0.7.8+31,移动中Q弹、牵拉及减薄见[本次记录](tpschedule-0.7.8-dock-drag.md)。构建30的点按/方向弹动及乳白材质见[本次记录](tpschedule-0.7.8-dock-spring.md)。构建29的Dock弹性与Android平板验证见[本次记录](tpschedule-0.7.8-dock-tablet.md)。构建28的通知权限恢复、通道初始化及真实后台触发见[通知记录](tpschedule-0.7.8-notification-permissions.md)。Dock乳白透光材质见[优化记录](tpschedule-0.7.8-dock-refinement.md)。本科/研究生HTML反馈及签名、备案信息见[0.7.8功能记录](tpschedule-0.7.8-validation.md)。应用更新检查与后台说明见[0.7.6记录](tpschedule-0.7.6-updates.md)。本科考试兼容的验证见[0.7.5记录](tpschedule-0.7.5-validation.md)。两例真实课表缺失字段问题待新页面定位。本科接入记录见[0.7.0记录](tpschedule-0.7.0-validation.md)。下文为首版历史记录。
日期:2026-10-13。测试环境:Flutter 3.47.7 / Dart 3.13.5 / JDK 17;Android 16 API 36,Pixel 7 模拟器,1080×2400。首版版本 0.1.0+1,包 ID com.example.hepan_everyday,最低 Android 7.0(API 24)。 日期:2026-10-13。测试环境:Flutter 3.47.7 / Dart 3.13.5 / JDK 17;Android 16 API 36,Pixel 7 模拟器,1080×2400。首版版本 0.1.0+1,包 ID com.example.hepan_everyday,最低 Android 7.0(API 24)。
+24
View File
@@ -0,0 +1,24 @@
# 0.7.8+31 拖动牵拉与轻薄玻璃
用户要求拖动过程中也有持续的Q弹形变,并减薄玻璃;随后提供飞书20261028-114548.mp4,参考其中滑块、底座前缘被牵动和反向回弹的效果。本轮按已授权的直接执行方式完成。
## 改动
- 滑块以680/28的弹簧轴追随手指目标,有少量弹性滞后;释放时仍按手指最终位置选择标签,避免视觉滞后选错。重新抓取与取消继承可见速度。
- 拖动输入唤醒620ms有限形变波,移动过程中横向伸缩、纵向挤压,后续输入可续发;暂停后自然结束,静止时不无限播放。按压、松手和减少动画状态调制形变强度。
- 整条Dock随方向移动约8逻辑像素,并沿前缘额外伸展约6逻辑像素;反向时沿弹簧回收。图标和文字随底座位置移动,保持清晰,原到位动画继续。
- 乳白层由38%/24%/30%减至29%/17%/23%;静止灰色滑块减轻,外缘柔光描边2.8降至1.8、内缘0.7降至0.45;阴影和折射高光减弱,保留轻模糊与折射。高度仍58,不改变点击区域及平板布局。
## 连续拖动的关键修正
逐帧预览发现,反复对同一控制器调用animateWith会在持续输入时重启计时,可能反复画出t=0,导致跟随几乎不移动。改为单一持续Ticker同时积分两个弹簧轴,触摸只更新目标。积分子步长不超过1/120秒,帧间隔封顶0.25秒;两个轴稳定后停表,减少动画及销毁时停止。无后台常驻动画。
新增匀速连续拖动的中心实际位移断言,修正前失败、修正后通过;不是仅检查有无放大。另有牵拉滞后、前缘不对称伸展、移动中横纵形变和变薄透光层的RED→GREEN验证。
## 验证与交付
320项Flutter测试通过,静态分析无问题。保留点按弹性、快速反向点按连续性、图标悬停及松手衔接、取消归位、减少动画和平板/分屏回归。
已逐帧检查右拖、左拖、反向、释放的渲染预览。独立审查未发现新的重要缺陷;持续Ticker的积分、生命周期与降动态路径另行复核通过。预览为Flutter渲染示意,本轮未作实体平板性能测试。
APK为0.7.8、构建31,原包名、签名、密钥及备案号保留。旧APK、旧源码和0.5.4登记冻结文件校验保持。服务器无需更新代码;更新后台发布版本0.7.8、构建号31和对应HTTPS下载地址。
+1 -1
View File
@@ -1,3 +1,3 @@
const appVersion = '0.7.8'; const appVersion = '0.7.8';
const appBuildNumber = 30; const appBuildNumber = 31;
const appFilingNumber = '蜀ICP备2025169276号-5A'; const appFilingNumber = '蜀ICP备2025169276号-5A';
+155 -31
View File
@@ -3,6 +3,7 @@ import 'dart:ui' as ui;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/physics.dart'; import 'package:flutter/physics.dart';
import 'package:flutter/scheduler.dart';
import 'glass_refraction.dart'; import 'glass_refraction.dart';
@@ -38,19 +39,91 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
late final AnimationController _sway = AnimationController.unbounded( late final AnimationController _sway = AnimationController.unbounded(
vsync: this, vsync: this,
); );
late final AnimationController _ripple = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 620),
);
void _wakeRipple() {
if (!_reduceMotion && !_ripple.isAnimating) _ripple.forward(from: 0);
}
double _swayTarget = 0, _swayVelocity = 0, _dragTarget = 0, _dragVelocity = 0;
Duration? _elasticLastTick;
late final Ticker _elasticTicker = createTicker(_tickElastic);
void _startElastic() {
if (!_elasticTicker.isActive) {
_elasticLastTick = null;
_elasticTicker.start();
}
}
// Keep one clock alive while input changes its targets. Restarting a spring
// on every pointer event repeatedly paints t=0 under continuous dragging.
void _tickElastic(Duration elapsed) {
final seconds = _elasticLastTick == null
? 1 / 60
: ((elapsed - _elasticLastTick!).inMicroseconds / 1000000).clamp(
0.0,
0.25,
);
_elasticLastTick = elapsed;
final sway = _advanceSpring(
_sway.value,
_swayVelocity,
_swayTarget,
220,
18,
seconds,
);
final drag = _advanceSpring(
_dragFollow.value,
_dragVelocity,
_dragTarget,
680,
28,
seconds,
);
_swayVelocity = sway.$2;
_dragVelocity = drag.$2;
_sway.value = sway.$1;
_dragFollow.value = drag.$1;
if ((_sway.value - _swayTarget).abs() < 0.0005 &&
_swayVelocity.abs() < 0.005 &&
(_dragFollow.value - _dragTarget).abs() < 0.0005 &&
_dragVelocity.abs() < 0.005) {
_sway.value = _swayTarget;
_dragFollow.value = _dragTarget;
_swayVelocity = _dragVelocity = 0;
_elasticTicker.stop();
_elasticLastTick = null;
}
}
(double, double) _advanceSpring(
double value,
double velocity,
double target,
double stiffness,
double damping,
double seconds,
) {
final steps = math.max(1, (seconds * 120).ceil());
final step = seconds / steps;
for (var i = 0; i < steps; i++) {
velocity += (stiffness * (target - value) - damping * velocity) * step;
value += velocity * step;
}
return (value, velocity);
}
void _swayTo(double target) { void _swayTo(double target) {
_swayTarget = _reduceMotion ? 0 : target;
if (_reduceMotion) { if (_reduceMotion) {
_sway.value = 0; _sway.value = 0;
_swayVelocity = 0;
return; return;
} }
_sway.animateWith( _startElastic();
SpringSimulation(
const SpringDescription(mass: 1, stiffness: 220, damping: 18),
_sway.value,
target,
_sway.velocity.clamp(-12, 12),
),
);
} }
bool _reduceMotion = false; bool _reduceMotion = false;
@@ -59,6 +132,7 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
void initState() { void initState() {
super.initState(); super.initState();
_targetIndex = widget.selectedIndex; _targetIndex = widget.selectedIndex;
_dragTarget = widget.selectedIndex.toDouble();
} }
double get _pressure => _press.value.clamp(0.0, 1.18); double get _pressure => _press.value.clamp(0.0, 1.18);
@@ -85,13 +159,34 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
_motion.value = widget.selectedIndex.toDouble(); _motion.value = widget.selectedIndex.toDouble();
_press.value = 0; _press.value = 0;
_tap.value = 0; _tap.value = 0;
_ripple.value = 0;
_elasticTicker.stop();
_elasticLastTick = null;
_swayTarget = _swayVelocity = _dragVelocity = 0;
_dragTarget = _drag ?? widget.selectedIndex.toDouble();
_dragFollow.value = _drag ?? widget.selectedIndex.toDouble();
_sway.value = 0; _sway.value = 0;
} }
} }
late final AnimationController _dragFollow = AnimationController.unbounded(
vsync: this,
value: widget.selectedIndex.toDouble(),
);
void _followDragTo(double target) {
_dragTarget = target;
if (_reduceMotion) {
_dragFollow.value = target;
_dragVelocity = 0;
return;
}
_startElastic();
}
double? _drag; double? _drag;
double _dragSpeed = 0; double _dragSpeed = 0;
double get _position => _drag ?? _motion.value.clamp(-0.12, 2.12); double get _position =>
(_drag != null ? _dragFollow.value : _motion.value).clamp(-0.12, 2.12);
void _moveTo(int index, {double velocity = 0, bool springyTap = false}) { void _moveTo(int index, {double velocity = 0, bool springyTap = false}) {
_targetIndex = index; _targetIndex = index;
if (_reduceMotion) { if (_reduceMotion) {
@@ -140,10 +235,13 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
@override @override
void dispose() { void dispose() {
_elasticTicker.dispose();
_motion.dispose(); _motion.dispose();
_press.dispose(); _press.dispose();
_tap.dispose(); _tap.dispose();
_sway.dispose(); _sway.dispose();
_ripple.dispose();
_dragFollow.dispose();
super.dispose(); super.dispose();
} }
@@ -159,25 +257,25 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
colors: lens colors: lens
? [ ? [
Color.lerp( Color.lerp(
const Color(0xFFA1A4AA).withValues(alpha: 0.34), const Color(0xFFA1A4AA).withValues(alpha: 0.28),
const Color(0xFFD8DBE0).withValues(alpha: 0.0252), const Color(0xFFD8DBE0).withValues(alpha: 0.0252),
pressure, pressure,
)!, )!,
Color.lerp( Color.lerp(
const Color(0xFF8E939B).withValues(alpha: 0.26), const Color(0xFF8E939B).withValues(alpha: 0.21),
const Color(0xFFC3C7CE).withValues(alpha: 0.0144), const Color(0xFFC3C7CE).withValues(alpha: 0.0144),
pressure, pressure,
)!, )!,
Color.lerp( Color.lerp(
const Color(0xFFB4B7BC).withValues(alpha: 0.32), const Color(0xFFB4B7BC).withValues(alpha: 0.26),
const Color(0xFFE5E7EB).withValues(alpha: 0.0216), const Color(0xFFE5E7EB).withValues(alpha: 0.0216),
pressure, pressure,
)!, )!,
] ]
: [ : [
Colors.white.withValues(alpha: 0.38), Colors.white.withValues(alpha: 0.29),
const Color(0xFFFDFEFF).withValues(alpha: 0.24), const Color(0xFFFDFEFF).withValues(alpha: 0.17),
Colors.white.withValues(alpha: 0.30), Colors.white.withValues(alpha: 0.23),
], ],
), ),
), ),
@@ -187,7 +285,7 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
return ClipRRect( return ClipRRect(
borderRadius: BorderRadius.circular(40), borderRadius: BorderRadius.circular(40),
child: GlassRefraction( child: GlassRefraction(
strength: lens ? 0.78 + pressure * 0.70 : 1.14, strength: lens ? 0.78 + pressure * 0.70 : 0.98,
blurSigma: lens ? 1.6 - pressure * 0.6 : 1.5, blurSigma: lens ? 1.6 - pressure * 0.6 : 1.5,
child: content, child: content,
), ),
@@ -202,7 +300,14 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
MediaQuery.sizeOf(context).width >= 600 ? 300.0 : 264.0, MediaQuery.sizeOf(context).width >= 600 ? 300.0 : 264.0,
); );
return AnimatedBuilder( return AnimatedBuilder(
animation: Listenable.merge([_motion, _press, _tap, _sway]), animation: Listenable.merge([
_motion,
_press,
_tap,
_sway,
_ripple,
_dragFollow,
]),
builder: (context, _) { builder: (context, _) {
final press = _pressure; final press = _pressure;
final pulse = _reduceMotion ? 0.0 : math.sin(math.pi * _tap.value); final pulse = _reduceMotion ? 0.0 : math.sin(math.pi * _tap.value);
@@ -213,21 +318,34 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
: math.sin(2 * math.pi * _tap.value); : math.sin(2 * math.pi * _tap.value);
final tapStretch = tapWave >= 0 ? tapWave * 0.24 : tapWave * 0.065; final tapStretch = tapWave >= 0 ? tapWave * 0.24 : tapWave * 0.065;
final tapRise = tapWave >= 0 ? -tapWave * 0.045 : -tapWave * 0.15; final tapRise = tapWave >= 0 ? -tapWave * 0.045 : -tapWave * 0.15;
// Pointer movement wakes a finite ripple; pauses do not start idle loops.
final dragWave = _reduceMotion
? 0.0
: math.sin(2 * math.pi * _ripple.value) * press.clamp(0.0, 1.0);
final dragStretch = dragWave * (dragWave >= 0 ? 0.10 : 0.06);
final speed = _reduceMotion final speed = _reduceMotion
? 0.0 ? 0.0
: (_drag != null ? _dragSpeed : _motion.velocity).abs(); : (_drag != null ? _dragSpeed : _motion.velocity).abs();
final stretch = final stretch =
(speed / 80).clamp(0.0, 0.08) + tapStretch + press * 0.30; (speed / 80).clamp(0.0, 0.08) +
tapStretch +
press * 0.30 +
dragStretch;
final lensWidth = _travel * (1 + stretch); final lensWidth = _travel * (1 + stretch);
final lensHeight = final lensHeight =
(_height - _inset * 2) * (_height - _inset * 2) *
(1 + press * 0.31 + tapRise - (speed / 180).clamp(0.0, 0.035)); (1 +
press * 0.31 +
tapRise -
dragWave * 0.08 -
(speed / 180).clamp(0.0, 0.035));
// Resize real glass bounds: sampling stays aligned without scaling text. // Resize real glass bounds: sampling stays aligned without scaling text.
final sway = _reduceMotion ? 0.0 : _sway.value.clamp(-1.2, 1.2); final sway = _reduceMotion ? 0.0 : _sway.value.clamp(-1.2, 1.2);
final bodyGrowth = press * 12 + pulse * 3 + sway.abs() * 4; final bodyGrowth = press * 12 + pulse * 3 + sway.abs() * 4;
final bodyHeight = final bodyHeight =
_height + press * 5 + pulse * 1.5 - sway.abs() * 0.8; _height + press * 5 + pulse * 1.5 - sway.abs() * 0.8;
final dockShift = sway * 8; final dockShift = sway * 8;
final leadingPull = sway * 6;
return SizedBox( return SizedBox(
width: _width, width: _width,
height: _height, height: _height,
@@ -245,9 +363,9 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: const Color(0xFF4C6078) color: const Color(0xFF4C6078)
.withValues(alpha: 0.075), .withValues(alpha: 0.05),
blurRadius: 18, blurRadius: 14,
offset: const Offset(0, 6), offset: const Offset(0, 4),
), ),
], ],
), ),
@@ -255,9 +373,9 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
Positioned( Positioned(
left: -bodyGrowth / 2, left: -bodyGrowth / 2 + math.min(leadingPull, 0),
top: (_height - bodyHeight) / 2, top: (_height - bodyHeight) / 2,
width: _width + bodyGrowth, width: _width + bodyGrowth + leadingPull.abs(),
height: bodyHeight, height: bodyHeight,
child: _glass(child: const SizedBox.expand()), child: _glass(child: const SizedBox.expand()),
), ),
@@ -328,7 +446,11 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
onHorizontalDragStart: index == widget.selectedIndex onHorizontalDragStart: index == widget.selectedIndex
? (_) { ? (_) {
final start = _position.clamp(0.0, 2.0); final start = _position.clamp(0.0, 2.0);
_dragFollow.value = start;
_dragTarget = start;
_dragVelocity = _motion.velocity;
_motion.stop(); _motion.stop();
_wakeRipple();
_pressTo(_reduceMotion ? 0 : 1); _pressTo(_reduceMotion ? 0 : 1);
setState(() { setState(() {
_drag = start; _drag = start;
@@ -339,18 +461,20 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
onHorizontalDragUpdate: index == widget.selectedIndex onHorizontalDragUpdate: index == widget.selectedIndex
? (details) => setState(() { ? (details) => setState(() {
_dragSpeed = details.delta.dx / _travel * 60; _dragSpeed = details.delta.dx / _travel * 60;
_wakeRipple();
_swayTo((_dragSpeed / 8).clamp(-1.0, 1.0)); _swayTo((_dragSpeed / 8).clamp(-1.0, 1.0));
_drag = (_position + details.delta.dx / _travel).clamp( _drag = (_drag! + details.delta.dx / _travel).clamp(
0.0, 0.0,
2.0, 2.0,
); );
_followDragTo(_drag!);
}) })
: null, : null,
onHorizontalDragEnd: index == widget.selectedIndex onHorizontalDragEnd: index == widget.selectedIndex
? (details) => _select( ? (details) => _select(
_position.round().clamp(0, 2), (_drag ?? _position).round().clamp(0, 2),
elastic: true, elastic: true,
velocity: details.velocity.pixelsPerSecond.dx / _travel, velocity: _dragVelocity,
) )
: null, : null,
onHorizontalDragCancel: index == widget.selectedIndex onHorizontalDragCancel: index == widget.selectedIndex
@@ -359,7 +483,7 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
final position = _position; final position = _position;
setState(() => _drag = null); setState(() => _drag = null);
_motion.value = position; _motion.value = position;
_moveTo(widget.selectedIndex); _moveTo(widget.selectedIndex, velocity: _dragVelocity);
_pressTo(0); _pressTo(0);
_swayTo(0); _swayTo(0);
} }
@@ -515,7 +639,7 @@ class _GlassRimPainter extends CustomPainter {
outer, outer,
Paint() Paint()
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = lens ? 1.8 : 2.8 ..strokeWidth = lens ? 1.4 : 1.8
..maskFilter = MaskFilter.blur(BlurStyle.normal, lens ? 0.9 : 0.65) ..maskFilter = MaskFilter.blur(BlurStyle.normal, lens ? 0.9 : 0.65)
..shader = ui.Gradient.linear( ..shader = ui.Gradient.linear(
bounds.topLeft, bounds.topLeft,
@@ -532,7 +656,7 @@ class _GlassRimPainter extends CustomPainter {
outer, outer,
Paint() Paint()
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = lens ? 1.25 : 1.1 ..strokeWidth = lens ? 1.0 : 0.85
..shader = ui.Gradient.sweep( ..shader = ui.Gradient.sweep(
bounds.center, bounds.center,
[ [
@@ -553,7 +677,7 @@ class _GlassRimPainter extends CustomPainter {
inner, inner,
Paint() Paint()
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = 0.7 ..strokeWidth = 0.45
..shader = ui.Gradient.linear( ..shader = ui.Gradient.linear(
bounds.topLeft, bounds.topLeft,
bounds.bottomRight, bounds.bottomRight,
+1 -1
View File
@@ -1,7 +1,7 @@
name: hepan_everyday name: hepan_everyday
description: TPSchedule 学生课表 description: TPSchedule 学生课表
publish_to: none publish_to: none
version: 0.7.8+30 version: 0.7.8+31
environment: environment:
sdk: ^3.13.5 sdk: ^3.13.5
dependencies: dependencies:
+2 -2
View File
@@ -43,8 +43,8 @@ void main() {
colour = mix(colour, reflected, min(edge_light * 0.18, 0.22)); colour = mix(colour, reflected, min(edge_light * 0.18, 0.22));
// A grazing-angle white reflection gives the curved rim visible thickness; // A grazing-angle white reflection gives the curved rim visible thickness;
// the broad centre continues to transmit the live, lightly softened scene. // the broad centre continues to transmit the live, lightly softened scene.
colour.rgb = mix(colour.rgb, vec3(colour.a), min(edge_light * 0.20, 0.22)); colour.rgb = mix(colour.rgb, vec3(colour.a), min(edge_light * 0.15, 0.18));
float reflection = edge_light * 0.10; float reflection = edge_light * 0.075;
colour.rgb = min(colour.rgb + reflection * colour.a, vec3(colour.a)); colour.rgb = min(colour.rgb + reflection * colour.a, vec3(colour.a));
frag_color = colour; frag_color = colour;
} }
+73 -2
View File
@@ -11,6 +11,37 @@ import 'navigation_test.dart' show uiController;
void main() { void main() {
setUpAll(tzdata.initializeTimeZones); setUpAll(tzdata.initializeTimeZones);
testWidgets('drag tugs the leading edge and lens follows with elastic lag', (
tester,
) async {
await tester.pumpWidget(_dock());
await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.assignment_rounded));
await tester.pumpAndSettle();
final lens = find.byType(GlassRefraction).last;
final body = find.byType(GlassRefraction).first;
final gesture = await tester.startGesture(
tester.getCenter(find.byKey(const Key('dock-cursor'))),
);
await tester.pump(const Duration(milliseconds: 110));
await tester.pump(const Duration(milliseconds: 700));
await gesture.moveBy(const Offset(24, 0));
await tester.pump();
final initialX = tester.getCenter(lens).dx;
final initialBody = tester.getRect(body);
await gesture.moveBy(const Offset(60, 0));
await tester.pump();
expect(tester.getCenter(lens).dx - initialX, lessThan(45));
await tester.pump(const Duration(milliseconds: 180));
expect(tester.getCenter(lens).dx, greaterThan(initialX + 35));
final draggedBody = tester.getRect(body);
final leading = draggedBody.right - initialBody.right;
final trailing = draggedBody.left - initialBody.left;
expect(leading - trailing, greaterThan(7));
await gesture.cancel();
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
});
testWidgets('inactive icon press leaves the cursor at its resting size', ( testWidgets('inactive icon press leaves the cursor at its resting size', (
tester, tester,
) async { ) async {
@@ -80,6 +111,42 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(tester.getSize(body), resting.size); expect(tester.getSize(body), resting.size);
}); });
testWidgets(
'lens keeps softly stretching and squeezing during an even drag',
(tester) async {
await tester.pumpWidget(_dock());
await tester.pumpAndSettle();
final lens = find.byType(GlassRefraction).last;
final gesture = await tester.startGesture(
tester.getCenter(find.byKey(const Key('dock-cursor'))),
);
await tester.pump(const Duration(milliseconds: 110));
await tester.pump(const Duration(milliseconds: 700));
await gesture.moveBy(const Offset(24, 0));
await tester.pump();
final widths = <double>[], heights = <double>[], centres = <double>[];
for (var i = 0; i < 22; i++) {
await gesture.moveBy(const Offset(5, 0));
await tester.pump(const Duration(milliseconds: 20));
if (i > 3) {
final size = tester.getSize(lens);
widths.add(size.width);
heights.add(size.height);
centres.add(tester.getCenter(lens).dx);
}
}
expect(widths.reduce(math.max) - widths.reduce(math.min), greaterThan(6));
expect(
heights.reduce(math.max) - heights.reduce(math.min),
greaterThan(2),
);
expect(centres.last - centres.first, greaterThan(50));
await gesture.up();
await tester.pumpAndSettle();
expect(tester.getSize(lens).width, closeTo(86, 0.1));
expect(tester.getSize(lens).height, closeTo(52, 0.1));
},
);
testWidgets( testWidgets(
'rapid reverse taps keep position continuous and finish on last tab', 'rapid reverse taps keep position continuous and finish on last tab',
(tester) async { (tester) async {
@@ -258,7 +325,9 @@ void main() {
await tester.pump(); await tester.pump();
await gesture.moveBy(const Offset(86, 0)); await gesture.moveBy(const Offset(86, 0));
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 180)); for (var frame = 0; frame < 12; frame++) {
await tester.pump(const Duration(milliseconds: 16));
}
expect(tester.getCenter(icon).dy, lessThan(y - 1)); expect(tester.getCenter(icon).dy, lessThan(y - 1));
await tester.pump(const Duration(milliseconds: 600)); await tester.pump(const Duration(milliseconds: 600));
expect(tester.getCenter(icon).dy, closeTo(y, 0.1)); expect(tester.getCenter(icon).dy, closeTo(y, 0.1));
@@ -278,7 +347,9 @@ void main() {
await tester.pump(); await tester.pump();
await gesture.moveBy(const Offset(86, 0)); await gesture.moveBy(const Offset(86, 0));
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 180)); for (var frame = 0; frame < 12; frame++) {
await tester.pump(const Duration(milliseconds: 16));
}
final before = tester.getRect(icon); final before = tester.getRect(icon);
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
+2 -2
View File
@@ -23,8 +23,8 @@ void main() {
// while its original cool colour remains visible through the material. // while its original cool colour remains visible through the material.
final red = _channel(pixels, 160, 43, 0); final red = _channel(pixels, 160, 43, 0);
final green = _channel(pixels, 160, 43, 1); final green = _channel(pixels, 160, 43, 1);
expect(red, greaterThan(75)); expect(red, greaterThan(50));
expect(red, lessThan(115)); expect(red, lessThan(80));
expect(green - red, closeTo(13, 6)); expect(green - red, closeTo(13, 6));
}, },
); );