feat: add elastic glass dock motion and Android tablet layouts
This commit is contained in:
@@ -25,7 +25,7 @@ void main() {
|
||||
await gesture.cancel();
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
testWidgets('tap cursor travels monotonically with no size jitter', (
|
||||
testWidgets('tap has bounded elastic stretch and settles on the new icon', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(_dock());
|
||||
@@ -35,17 +35,123 @@ void main() {
|
||||
final target = tester.getCenter(find.byIcon(Icons.assignment_rounded)).dx;
|
||||
await tester.tap(find.byIcon(Icons.assignment_rounded));
|
||||
await tester.pump();
|
||||
var previous = resting.center.dx;
|
||||
for (var frame = 0; frame < 50; frame++) {
|
||||
var widest = resting.width;
|
||||
var furthest = resting.center.dx;
|
||||
for (var frame = 0; frame < 70; frame++) {
|
||||
await tester.pump(const Duration(milliseconds: 16));
|
||||
final rect = tester.getRect(lens);
|
||||
expect(rect.width, closeTo(resting.width, 0.05));
|
||||
expect(rect.height, closeTo(resting.height, 0.05));
|
||||
expect(rect.center.dx, greaterThanOrEqualTo(previous - 0.05));
|
||||
expect(rect.center.dx, lessThanOrEqualTo(target + 0.05));
|
||||
previous = rect.center.dx;
|
||||
widest = widest > rect.width ? widest : rect.width;
|
||||
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(previous, closeTo(target, 0.05));
|
||||
expect(widest, greaterThan(resting.width * 1.06));
|
||||
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));
|
||||
});
|
||||
testWidgets('drag deforms the dock body while labels remain readable', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(_dock());
|
||||
await tester.pumpAndSettle();
|
||||
final body = find.byType(GlassRefraction).first;
|
||||
final resting = tester.getRect(body);
|
||||
final labelSize = tester.getSize(find.text('考试'));
|
||||
final gesture = await tester.startGesture(
|
||||
tester.getCenter(find.byKey(const Key('dock-cursor'))),
|
||||
);
|
||||
await tester.pump(const Duration(milliseconds: 180));
|
||||
await gesture.moveBy(const Offset(75, 0));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
expect(tester.getSize(body).width, greaterThan(resting.width + 5));
|
||||
expect(tester.getSize(body).height, greaterThan(resting.height + 2));
|
||||
expect(tester.getSize(find.text('考试')), labelSize);
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getSize(body), resting.size);
|
||||
});
|
||||
testWidgets(
|
||||
'rapid reverse taps keep position continuous and finish on last tab',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(_dock());
|
||||
await tester.pumpAndSettle();
|
||||
final lens = find.byType(GlassRefraction).last;
|
||||
await tester.tap(find.byIcon(Icons.person_rounded));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 90));
|
||||
final before = tester.getCenter(lens);
|
||||
final beforeSize = tester.getSize(lens);
|
||||
await tester.tap(find.byIcon(Icons.calendar_month_rounded));
|
||||
await tester.pump();
|
||||
expect((tester.getCenter(lens) - before).distance, lessThan(1));
|
||||
expect(tester.getSize(lens).width, closeTo(beforeSize.width, 0.1));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester.getCenter(lens).dx,
|
||||
closeTo(
|
||||
tester.getCenter(find.byIcon(Icons.calendar_month_rounded)).dx,
|
||||
0.1,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
testWidgets('reversing a tap at the edge preserves glass size and position', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(_dock());
|
||||
await tester.pumpAndSettle();
|
||||
final lens = find.byType(GlassRefraction).last;
|
||||
final body = find.byType(GlassRefraction).first;
|
||||
await tester.tap(find.byIcon(Icons.person_rounded));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 230));
|
||||
final before = tester.getRect(lens);
|
||||
final bodyBefore = tester.getRect(body);
|
||||
await tester.tap(find.byIcon(Icons.calendar_month_rounded));
|
||||
await tester.pump();
|
||||
final after = tester.getRect(lens);
|
||||
expect(after.width, closeTo(before.width, 0.1));
|
||||
expect(after.height, closeTo(before.height, 0.1));
|
||||
expect(after.center.dx, closeTo(before.center.dx, 0.1));
|
||||
expect(tester.getSize(body).width, closeTo(bodyBefore.width, 0.1));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester.getCenter(lens).dx,
|
||||
closeTo(
|
||||
tester.getCenter(find.byIcon(Icons.calendar_month_rounded)).dx,
|
||||
0.1,
|
||||
),
|
||||
);
|
||||
});
|
||||
testWidgets('dock body follows a moving drag and springs back on cancel', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(_dock());
|
||||
await tester.pumpAndSettle();
|
||||
final body = find.byType(GlassRefraction).first;
|
||||
final resting = tester.getRect(body);
|
||||
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: 350));
|
||||
await gesture.moveBy(const Offset(24, 0));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 350));
|
||||
final held = tester.getRect(body);
|
||||
await gesture.moveBy(const Offset(60, 0));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
expect(tester.getSize(body).width, greaterThan(held.width + 0.5));
|
||||
await gesture.cancel();
|
||||
await tester.pumpAndSettle();
|
||||
final settled = tester.getRect(body);
|
||||
expect(settled.left, closeTo(resting.left, 0.05));
|
||||
expect(settled.top, closeTo(resting.top, 0.05));
|
||||
expect(settled.width, closeTo(resting.width, 0.05));
|
||||
expect(settled.height, closeTo(resting.height, 0.05));
|
||||
});
|
||||
testWidgets('base and selected cursor both refract the live backdrop', (
|
||||
tester,
|
||||
@@ -75,6 +181,44 @@ void main() {
|
||||
expect(tester.getCenter(lens).dx, closeTo(target, 0.05));
|
||||
expect(tester.getSize(lens), resting);
|
||||
});
|
||||
testWidgets(
|
||||
'changing reduced motion during a tap does not leave frozen deformation',
|
||||
(tester) async {
|
||||
final reduced = ValueNotifier(false);
|
||||
var selected = 0;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: StatefulBuilder(
|
||||
builder: (context, setState) => ValueListenableBuilder<bool>(
|
||||
valueListenable: reduced,
|
||||
builder: (context, value, _) => MediaQuery(
|
||||
data: MediaQueryData(disableAnimations: value),
|
||||
child: Center(
|
||||
child: GlassDock(
|
||||
selectedIndex: selected,
|
||||
onSelected: (index) => setState(() => selected = index),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
final lens = find.byType(GlassRefraction).last;
|
||||
final size = tester.getSize(lens);
|
||||
await tester.tap(find.byIcon(Icons.assignment_rounded));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 120));
|
||||
reduced.value = true;
|
||||
await tester.pump();
|
||||
reduced.value = false;
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getSize(lens).width, closeTo(size.width, 0.05));
|
||||
expect(tester.getSize(lens).height, closeTo(size.height, 0.05));
|
||||
reduced.dispose();
|
||||
},
|
||||
);
|
||||
testWidgets('held cursor enlarges before dragging and returns on release', (
|
||||
tester,
|
||||
) async {
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:timezone/data/latest.dart' as tzdata;
|
||||
import 'package:hepan_everyday/app/hepan_app.dart';
|
||||
import 'package:hepan_everyday/features/timetable/week_grid.dart';
|
||||
import 'package:hepan_everyday/features/exams/exams_page.dart';
|
||||
import 'package:hepan_everyday/features/profile/profile_page.dart';
|
||||
import 'package:hepan_everyday/widgets/glass_dock.dart';
|
||||
import 'package:hepan_everyday/features/profile/settings_page.dart';
|
||||
import 'package:hepan_everyday/features/feedback/feedback_page.dart';
|
||||
|
||||
import 'navigation_test.dart' show uiController;
|
||||
|
||||
void main() {
|
||||
setUpAll(tzdata.initializeTimeZones);
|
||||
for (final size in [const Size(800, 1280), const Size(1280, 800)]) {
|
||||
testWidgets('tablet $size fits seven days and limits reading width', (
|
||||
tester,
|
||||
) async {
|
||||
tester.view.devicePixelRatio = 1;
|
||||
tester.view.physicalSize = size;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
final controller = uiController();
|
||||
await controller.initialize();
|
||||
await controller.showDemo();
|
||||
await tester.pumpWidget(HepanApp(controller: controller));
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(tester.getRect(find.text('周一')).left, greaterThan(0));
|
||||
expect(tester.getRect(find.text('周日')).right, lessThan(size.width));
|
||||
final monday = tester.widget<Text>(find.text('周一'));
|
||||
expect(monday.style!.fontSize, greaterThanOrEqualTo(12));
|
||||
expect(tester.getSize(find.byType(GlassDock)).width, 300);
|
||||
expect(tester.getSize(find.byType(GlassDock)).height, 58);
|
||||
await tester.drag(find.text('周三'), const Offset(-160, 0));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('第 2 周'), findsOneWidget);
|
||||
await tester.tap(find.byIcon(Icons.assignment_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester.getSize(find.byType(ExamsPage)).width,
|
||||
lessThanOrEqualTo(840),
|
||||
);
|
||||
await tester.tap(find.byIcon(Icons.person_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester.getSize(find.byType(ProfilePage)).width,
|
||||
lessThanOrEqualTo(840),
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
await tester.tap(find.byIcon(Icons.calendar_month_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('第 2 周'), findsOneWidget);
|
||||
tester.view.physicalSize = const Size(480, 800);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getSize(find.byType(GlassDock)).width, 264);
|
||||
expect(find.text('第 2 周'), findsOneWidget);
|
||||
expect(
|
||||
tester.getRect(find.byType(WeekGrid)).right,
|
||||
lessThanOrEqualTo(480),
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
controller.dispose();
|
||||
});
|
||||
}
|
||||
testWidgets(
|
||||
'wide settings and feedback remain centered and usable with large text',
|
||||
(tester) async {
|
||||
tester.view.devicePixelRatio = 1;
|
||||
tester.view.physicalSize = const Size(1280, 800);
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
final controller = uiController();
|
||||
await controller.initialize();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(
|
||||
size: Size(1280, 800),
|
||||
textScaler: TextScaler.linear(1.5),
|
||||
),
|
||||
child: SettingsPage(controller: controller),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getSize(find.byType(ListView)).width, 760);
|
||||
expect(tester.getRect(find.byType(ListView)).center.dx, 640);
|
||||
expect(tester.takeException(), isNull);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(
|
||||
size: Size(1280, 800),
|
||||
textScaler: TextScaler.linear(1.5),
|
||||
),
|
||||
child: const FeedbackPage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getSize(find.byType(ListView)).width, 760);
|
||||
expect(tester.takeException(), isNull);
|
||||
await tester.enterText(find.byKey(const Key('feedback-message')), '平板反馈');
|
||||
expect(find.text('平板反馈'), findsOneWidget);
|
||||
controller.dispose();
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -45,7 +45,8 @@ void main() {
|
||||
final a = tester.getRect(find.bySemanticsLabel('A,T,R'));
|
||||
final b = tester.getRect(find.bySemanticsLabel('B,T,R'));
|
||||
final cRect = tester.getRect(find.bySemanticsLabel('C,T,R'));
|
||||
expect(a.overlaps(b), isFalse);
|
||||
expect(b.overlaps(cRect), isFalse);
|
||||
// Fractional Expanded heights can differ by floating-point epsilon.
|
||||
expect(b.top, greaterThanOrEqualTo(a.bottom - 0.001));
|
||||
expect(cRect.top, greaterThanOrEqualTo(b.bottom - 0.001));
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user