feat: select school terms and smooth refractive Dock motion

This commit is contained in:
Codex
2026-10-13 19:08:36 +08:00
parent 3e2d8864af
commit 50fcc70e8e
33 changed files with 1245 additions and 94 deletions
+51 -25
View File
@@ -30,6 +30,14 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
value: 0,
);
bool _reduceMotion = false;
late int _targetIndex;
bool _elasticMotion = false;
@override
void initState() {
super.initState();
_targetIndex = widget.selectedIndex;
}
double get _pressure => _press.value.clamp(0.0, 1.18);
void _pressTo(double target) {
if (_reduceMotion) {
@@ -38,7 +46,7 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
}
_press.animateWith(
SpringSimulation(
const SpringDescription(mass: 1, stiffness: 440, damping: 20),
const SpringDescription(mass: 1, stiffness: 440, damping: 17),
_press.value,
target,
_press.velocity,
@@ -55,26 +63,36 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
double? _drag;
double _dragSpeed = 0;
double get _position => _drag ?? _motion.value.clamp(-0.12, 2.12);
void _spring(int index, [double velocity = 0]) {
void _moveTo(int index, {bool elastic = false, double velocity = 0}) {
_targetIndex = index;
_elasticMotion = elastic && !_reduceMotion;
if (_reduceMotion) {
_motion.value = index.toDouble();
return;
}
_motion.animateWith(
SpringSimulation(
const SpringDescription(mass: 1, stiffness: 380, damping: 23),
_position,
if (elastic) {
_motion.animateWith(
SpringSimulation(
const SpringDescription(mass: 1, stiffness: 380, damping: 19),
_position,
index.toDouble(),
velocity.clamp(-8, 8),
),
);
} else {
_motion.animateTo(
index.toDouble(),
velocity.clamp(-8, 8),
),
);
duration: const Duration(milliseconds: 320),
curve: Curves.easeOutCubic,
);
}
}
void _select(int index, [double velocity = 0]) {
void _select(int index, {bool elastic = false, double velocity = 0}) {
final position = _position;
setState(() => _drag = null);
_motion.value = position;
_spring(index, velocity);
_moveTo(index, elastic: elastic, velocity: velocity);
_pressTo(0);
widget.onSelected(index);
}
@@ -82,8 +100,10 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
@override
void didUpdateWidget(GlassDock oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.selectedIndex != widget.selectedIndex && _drag == null) {
_spring(widget.selectedIndex);
// The parent acknowledges onSelected with a rebuild. Restarting here used
// to erase the release velocity and start a second spring for the same tab.
if (widget.selectedIndex != _targetIndex && _drag == null) {
_moveTo(widget.selectedIndex);
}
}
@@ -124,12 +144,10 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
);
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,
),
child: GlassRefraction(
strength: lens ? 0.25 + pressure * 0.95 : 0.32,
child: content,
),
);
}
@@ -138,12 +156,19 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
animation: Listenable.merge([_motion, _press]),
builder: (context, _) {
final press = _pressure;
final speed = (_drag != null ? _dragSpeed : _motion.velocity).abs();
final stretch = (speed / 16).clamp(0.0, 0.12) + press * 0.18;
final speed = _reduceMotion
? 0.0
: (_drag != null
? _dragSpeed
: _elasticMotion
? _motion.velocity
: 0.0)
.abs();
final stretch = (speed / 16).clamp(0.0, 0.14) + press * 0.30;
final lensWidth = _travel * (1 + stretch);
final lensHeight =
(_height - _inset * 2) *
(1 + press * 0.19 - (speed / 16).clamp(0.0, 0.07));
(1 + press * 0.31 - (speed / 16).clamp(0.0, 0.07));
return SizedBox(
width: _width,
height: _height,
@@ -201,7 +226,7 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
child: GestureDetector(
key: index == widget.selectedIndex ? const Key('dock-cursor') : null,
behavior: HitTestBehavior.opaque,
onTapDown: (_) => _pressTo(1),
onTapDown: index == widget.selectedIndex ? (_) => _pressTo(1) : null,
onTapCancel: () {
if (_drag == null) _pressTo(0);
},
@@ -229,7 +254,8 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
onHorizontalDragEnd: index == widget.selectedIndex
? (details) => _select(
_position.round().clamp(0, 2),
details.velocity.pixelsPerSecond.dx / _travel,
elastic: true,
velocity: details.velocity.pixelsPerSecond.dx / _travel,
)
: null,
onHorizontalDragCancel: index == widget.selectedIndex
@@ -238,7 +264,7 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
final position = _position;
setState(() => _drag = null);
_motion.value = position;
_spring(widget.selectedIndex);
_moveTo(widget.selectedIndex, elastic: true);
_pressTo(0);
}
: null,