fix: automate graduate timetable read and refine blue glass Dock

This commit is contained in:
Codex
2026-10-13 17:50:04 +08:00
parent 8f20a341c4
commit 476ef097ee
23 changed files with 1072 additions and 176 deletions
+110 -76
View File
@@ -4,6 +4,8 @@ import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
import 'glass_refraction.dart';
class GlassDock extends StatefulWidget {
const GlassDock({
super.key,
@@ -17,21 +19,45 @@ class GlassDock extends StatefulWidget {
}
class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
static const _width = 280.0, _travel = 88.0;
static const _width = 264.0, _height = 68.0, _inset = 3.0, _travel = 86.0;
static const _blue = Color(0xFF007AFF);
static const _selectedInk = Color(0xFF62666C);
late final AnimationController _motion = AnimationController.unbounded(
vsync: this,
value: widget.selectedIndex.toDouble(),
);
late final AnimationController _press = AnimationController(
late final AnimationController _press = AnimationController.unbounded(
vsync: this,
duration: const Duration(milliseconds: 140),
value: 0,
);
bool _reduceMotion = false;
double get _pressure => _press.value.clamp(0.0, 1.18);
void _pressTo(double target) {
if (_reduceMotion) {
_press.value = target;
return;
}
_press.animateWith(
SpringSimulation(
const SpringDescription(mass: 1, stiffness: 440, damping: 20),
_press.value,
target,
_press.velocity,
),
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_reduceMotion = MediaQuery.disableAnimationsOf(context);
}
double? _drag;
double _dragSpeed = 0;
double get _position => _drag ?? _motion.value.clamp(-0.12, 2.12);
void _spring(int index, [double velocity = 0]) {
if (MediaQuery.disableAnimationsOf(context)) {
if (_reduceMotion) {
_motion.value = index.toDouble();
return;
}
@@ -50,7 +76,7 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
setState(() => _drag = null);
_motion.value = position;
_spring(index, velocity);
_press.reverse();
_pressTo(0);
widget.onSelected(index);
}
@@ -69,53 +95,59 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
super.dispose();
}
Widget _glass({required Widget child, bool lens = false}) => ClipRRect(
borderRadius: BorderRadius.circular(lens ? 36 : 40),
child: BackdropFilter(
// Keep the underlying timetable visible. Optical rims provide the glass
// thickness instead of a frosted, heavily tinted fill.
filter: ui.ImageFilter.blur(
sigmaX: lens ? 0.6 : 1.5,
sigmaY: lens ? 0.6 : 1.5,
),
child: CustomPaint(
foregroundPainter: _GlassRimPainter(lens: lens),
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: lens
? [
Colors.white.withValues(alpha: 0.045),
_blue.withValues(alpha: 0.018),
Colors.white.withValues(alpha: 0.012),
]
: [
Colors.white.withValues(alpha: 0.028),
_blue.withValues(alpha: 0.005),
Colors.white.withValues(alpha: 0.012),
],
),
Widget _glass({required Widget child, bool lens = false}) {
final pressure = _pressure.clamp(0.0, 1.0);
final content = CustomPaint(
foregroundPainter: _GlassRimPainter(lens: lens),
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: lens
? [
const Color(0xFFD8DBE0)
.withValues(alpha: 0.72 * (1 - pressure * 0.91)),
const Color(0xFFC3C7CE)
.withValues(alpha: 0.60 * (1 - pressure * 0.93)),
const Color(0xFFE5E7EB)
.withValues(alpha: 0.72 * (1 - pressure * 0.90)),
]
: [
Colors.white.withValues(alpha: 0.028),
_blue.withValues(alpha: 0.005),
Colors.white.withValues(alpha: 0.012),
],
),
child: child,
),
child: child,
),
),
);
);
return ClipRRect(
borderRadius: BorderRadius.circular(40),
child: lens
? GlassRefraction(strength: 0.25 + pressure * 0.95, child: content)
: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: 1.0, sigmaY: 1.0),
child: content,
),
);
}
@override
Widget build(BuildContext context) => AnimatedBuilder(
animation: Listenable.merge([_motion, _press]),
builder: (context, _) {
final press = Curves.easeOut.transform(_press.value);
final press = _pressure;
final speed = (_drag != null ? _dragSpeed : _motion.velocity).abs();
final stretch = (speed / 16).clamp(0.0, 0.14) + press * 0.04;
final lensWidth = 88 * (1 + stretch);
final lensHeight = 56 + press * 5 - stretch * 9;
final stretch = (speed / 16).clamp(0.0, 0.12) + press * 0.18;
final lensWidth = _travel * (1 + stretch);
final lensHeight =
(_height - _inset * 2) *
(1 + press * 0.19 - (speed / 16).clamp(0.0, 0.07));
return SizedBox(
width: _width,
height: 70,
height: _height,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(40),
@@ -127,34 +159,34 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
),
],
),
child: _glass(
child: Stack(
clipBehavior: Clip.none,
children: [
Positioned(
left: 8 + _position * _travel - (lensWidth - 88) / 2,
top: (70 - lensHeight) / 2,
width: lensWidth,
height: lensHeight,
child: IgnorePointer(
child: _glass(lens: true, child: const SizedBox.expand()),
),
child: Stack(
clipBehavior: Clip.none,
children: [
Positioned.fill(child: _glass(child: const SizedBox.expand())),
Positioned(
left: (_inset + _position * _travel - (lensWidth - _travel) / 2)
.clamp(1.0, _width - lensWidth - 1),
top: (_height - lensHeight) / 2,
width: lensWidth,
height: lensHeight,
child: IgnorePointer(
child: _glass(lens: true, child: const SizedBox.expand()),
),
Positioned(
left: 8,
right: 8,
top: 7,
bottom: 7,
child: Row(
children: [
_item(0, Icons.calendar_month_rounded, '课表'),
_item(1, Icons.assignment_outlined, '考试'),
_item(2, Icons.person_rounded, '个人设置'),
],
),
),
Positioned(
left: _inset,
right: _inset,
top: _inset,
bottom: _inset,
child: Row(
children: [
_item(0, Icons.calendar_month_rounded, '课表'),
_item(1, Icons.assignment_outlined, '考试'),
_item(2, Icons.person_rounded, '个人设置'),
],
),
],
),
),
],
),
),
);
@@ -170,16 +202,16 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
child: GestureDetector(
key: index == widget.selectedIndex ? const Key('dock-cursor') : null,
behavior: HitTestBehavior.opaque,
onTapDown: (_) => _press.forward(),
onTapDown: (_) => _pressTo(1),
onTapCancel: () {
if (_drag == null) _press.reverse();
if (_drag == null) _pressTo(0);
},
onTap: () => _select(index),
onHorizontalDragStart: index == widget.selectedIndex
? (_) {
final start = _position.clamp(0.0, 2.0);
_motion.stop();
_press.forward();
_pressTo(1);
setState(() {
_drag = start;
_dragSpeed = 0;
@@ -203,24 +235,25 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
: null,
onHorizontalDragCancel: index == widget.selectedIndex
? () {
if (!mounted) return;
final position = _position;
setState(() => _drag = null);
_motion.value = position;
_spring(widget.selectedIndex);
_press.reverse();
_pressTo(0);
}
: null,
child: Center(
child: AnimatedScale(
scale: index == _position.round().clamp(0, 2)
? 1 + _press.value * 0.08
? 1 + _pressure * 0.08
: 1,
duration: const Duration(milliseconds: 130),
child: Icon(
icon,
size: 27,
color: index == _position.round().clamp(0, 2)
? _blue
? _selectedInk
: _blue.withValues(alpha: 0.48),
),
),
@@ -238,6 +271,7 @@ class _GlassRimPainter extends CustomPainter {
static const _blue = Color(0xFF007AFF);
@override
void paint(Canvas canvas, Size size) {
final edge = lens ? const Color(0xFF7B838F) : _blue;
final bounds = Offset.zero & size;
final outer = RRect.fromRectAndRadius(
bounds.deflate(0.7),
@@ -256,10 +290,10 @@ class _GlassRimPainter extends CustomPainter {
bounds.center,
[
Colors.white.withValues(alpha: 0.90),
_blue.withValues(alpha: lens ? 0.30 : 0.15),
edge.withValues(alpha: lens ? 0.30 : 0.15),
Colors.white.withValues(alpha: 0.10),
Colors.white.withValues(alpha: 0.86),
_blue.withValues(alpha: 0.12),
edge.withValues(alpha: 0.12),
Colors.white.withValues(alpha: 0.90),
],
[0.0, 0.18, 0.38, 0.60, 0.80, 1.0],
@@ -278,7 +312,7 @@ class _GlassRimPainter extends CustomPainter {
bounds.bottomRight,
[
Colors.white.withValues(alpha: 0.30),
_blue.withValues(alpha: lens ? 0.13 : 0.07),
edge.withValues(alpha: lens ? 0.13 : 0.07),
Colors.white.withValues(alpha: 0.40),
],
[0.0, 0.5, 1.0],