fix: automate graduate timetable read and refine blue glass Dock
This commit is contained in:
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user