feat: strengthen dock spring motion, directional follow and milky glass

This commit is contained in:
david1465833828
2026-10-28 11:28:51 +08:00
parent 9da5864d29
commit 6604f31d98
9 changed files with 321 additions and 109 deletions
+109 -2
View File
@@ -1,3 +1,5 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:timezone/data/latest.dart' as tzdata;
@@ -36,16 +38,22 @@ void main() {
await tester.tap(find.byIcon(Icons.assignment_rounded));
await tester.pump();
var widest = resting.width;
var narrowest = resting.width;
var tallest = resting.height;
var furthest = resting.center.dx;
for (var frame = 0; frame < 70; frame++) {
await tester.pump(const Duration(milliseconds: 16));
final rect = tester.getRect(lens);
widest = widest > rect.width ? widest : rect.width;
narrowest = narrowest < rect.width ? narrowest : rect.width;
tallest = tallest > rect.height ? tallest : rect.height;
furthest = furthest > rect.center.dx ? furthest : rect.center.dx;
expect(rect.width, lessThan(resting.width * 1.4));
expect(rect.center.dx, lessThan(target + 8));
expect(rect.center.dx, lessThan(target + 14));
}
expect(widest, greaterThan(resting.width * 1.06));
expect(widest, greaterThan(resting.width * 1.20));
expect(narrowest, lessThan(resting.width * 0.97));
expect(tallest, greaterThan(resting.height * 1.10));
expect(furthest, greaterThan(target + 0.1));
expect(tester.getCenter(lens).dx, closeTo(target, 0.1));
expect(tester.getSize(lens).width, closeTo(resting.width, 0.1));
@@ -166,6 +174,59 @@ void main() {
greaterThan(0),
);
});
testWidgets(
'drag moves the whole dock in its direction and returns on release',
(tester) async {
await tester.pumpWidget(_dock());
await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.assignment_rounded));
await tester.pumpAndSettle();
final icon = find.byIcon(Icons.calendar_month_rounded);
final glass = find.byType(GlassRefraction).first;
final x = tester.getCenter(icon).dx;
final glassX = tester.getCenter(glass).dx;
final gesture = await tester.startGesture(
tester.getCenter(find.byKey(const Key('dock-cursor'))),
);
await gesture.moveBy(const Offset(24, 0));
await tester.pump();
await gesture.moveBy(const Offset(45, 0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 140));
expect(tester.getCenter(icon).dx, greaterThan(x + 4));
expect(tester.getCenter(glass).dx, greaterThan(glassX + 4));
await gesture.moveBy(const Offset(-70, 0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 220));
expect(tester.getCenter(icon).dx, lessThan(x - 4));
await gesture.up();
await tester.pumpAndSettle();
expect(tester.getCenter(icon).dx, closeTo(x, 0.05));
},
);
testWidgets('icon bounces once when the glass arrives and then rests', (
tester,
) async {
await tester.pumpWidget(_dock());
await tester.pumpAndSettle();
final icon = find.byIcon(Icons.assignment_rounded);
final rest = tester.getRect(icon);
await tester.tap(icon);
await tester.pump();
var highest = rest.top;
var widest = rest.width;
for (var i = 0; i < 65; i++) {
await tester.pump(const Duration(milliseconds: 16));
final r = tester.getRect(icon);
highest = math.min(highest, r.top);
widest = math.max(widest, r.width);
}
expect(highest, lessThan(rest.top - 1));
expect(widest, greaterThan(rest.width * 1.1));
await tester.pumpAndSettle();
expect(tester.getRect(icon).top, closeTo(rest.top, 0.05));
expect(tester.getSize(icon).width, closeTo(rest.width, 0.05));
});
testWidgets('reduced motion snaps the cursor without oscillation', (
tester,
) async {
@@ -173,6 +234,7 @@ void main() {
await tester.pumpAndSettle();
final lens = find.byType(GlassRefraction).last;
final resting = tester.getSize(lens);
final iconBefore = tester.getRect(find.byIcon(Icons.person_rounded));
await tester.tap(find.byIcon(Icons.person_rounded));
await tester.pump();
final target = tester.getCenter(find.byIcon(Icons.person_rounded)).dx;
@@ -180,6 +242,51 @@ void main() {
await tester.pump(const Duration(milliseconds: 200));
expect(tester.getCenter(lens).dx, closeTo(target, 0.05));
expect(tester.getSize(lens), resting);
expect(tester.getRect(find.byIcon(Icons.person_rounded)), iconBefore);
});
testWidgets('hovering a dragged lens animates its icon before releasing', (
tester,
) async {
await tester.pumpWidget(_dock());
await tester.pumpAndSettle();
final icon = find.byIcon(Icons.assignment_rounded);
final y = tester.getCenter(icon).dy;
final gesture = await tester.startGesture(
tester.getCenter(find.byKey(const Key('dock-cursor'))),
);
await gesture.moveBy(const Offset(24, 0));
await tester.pump();
await gesture.moveBy(const Offset(86, 0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 180));
expect(tester.getCenter(icon).dy, lessThan(y - 1));
await tester.pump(const Duration(milliseconds: 600));
expect(tester.getCenter(icon).dy, closeTo(y, 0.1));
await gesture.cancel();
await tester.pumpAndSettle();
});
testWidgets('releasing over an animating icon preserves its bounce', (
tester,
) async {
await tester.pumpWidget(_dock());
await tester.pumpAndSettle();
final icon = find.byIcon(Icons.assignment_rounded);
final gesture = await tester.startGesture(
tester.getCenter(find.byKey(const Key('dock-cursor'))),
);
await gesture.moveBy(const Offset(24, 0));
await tester.pump();
await gesture.moveBy(const Offset(86, 0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 180));
final before = tester.getRect(icon);
await gesture.up();
await tester.pump();
final after = tester.getRect(icon);
expect(after.top, closeTo(before.top, 0.1));
expect(after.width, closeTo(before.width, 0.1));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
});
testWidgets(
'changing reduced motion during a tap does not leave frozen deformation',
+8 -9
View File
@@ -23,8 +23,8 @@ void main() {
// while its original cool colour remains visible through the material.
final red = _channel(pixels, 160, 43, 0);
final green = _channel(pixels, 160, 43, 1);
expect(red, greaterThan(45));
expect(red, lessThan(90));
expect(red, greaterThan(75));
expect(red, lessThan(115));
expect(green - red, closeTo(13, 6));
},
);
@@ -85,11 +85,10 @@ void main() {
final red = _channel(pixels, 16, 40, 0);
final green = _channel(pixels, 16, 40, 1);
final blue = _channel(pixels, 16, 40, 2);
expect(red, lessThan(115));
expect(green - red, greaterThanOrEqualTo(80));
// The milky base and resting grey lens still transmit over half of the
// cyan source's 210-level blue/red contrast rather than becoming opaque.
expect(blue - red, greaterThan(105));
expect(red, lessThan(125));
expect(green - red, greaterThanOrEqualTo(65));
// The thicker milky base still transmits a clear cyan colour difference.
expect(blue - red, greaterThan(90));
},
);
@@ -133,8 +132,8 @@ void main() {
reds.reduce((a, b) => a > b ? a : b) -
reds.reduce((a, b) => a < b ? a : b);
// The original stripes have 180 levels of contrast. Both the base and
// selected lens retain over half, keeping these larger shapes readable.
expect(contrast, greaterThan(90), reason: 'background at x=$start');
// selected lens retain at least 40%, keeping larger shapes readable.
expect(contrast, greaterThan(72), reason: 'background at x=$start');
}
},
);