Unverified Commit 1ab73b15 authored by Will Lockwood's avatar Will Lockwood Committed by GitHub

Improve iOS fidelity of `barrierColor`s and edge decorations for full-screen...

Improve iOS fidelity of `barrierColor`s and edge decorations for full-screen Cupertino page transitions (#95537)
parent 06f08e4a
......@@ -25,6 +25,19 @@ const int _kMaxDroppedSwipePageForwardAnimationTime = 800; // Milliseconds.
// user releases a page mid swipe.
const int _kMaxPageBackAnimationTime = 300; // Milliseconds.
/// Barrier color used for a barrier visible during transitions for Cupertino
/// page routes.
///
/// This barrier color is only used for full-screen page routes with
/// `fullscreenDialog: false`.
///
/// By default, `fullscreenDialog` Cupertino route transitions have no
/// `barrierColor`, and [CupertinoDialogRoute]s and [CupertinoModalPopupRoute]s
/// have a `barrierColor` defined by [kCupertinoModalBarrierColor].
///
/// A relatively rigorous eyeball estimation.
const Color _kCupertinoPageTransitionBarrierColor = Color(0x18000000);
/// Barrier color for a Cupertino modal barrier.
///
/// Extracted from https://developer.apple.com/design/resources/.
......@@ -126,7 +139,7 @@ mixin CupertinoRouteTransitionMixin<T> on PageRoute<T> {
Duration get transitionDuration => const Duration(milliseconds: 400);
@override
Color? get barrierColor => null;
Color? get barrierColor => fullscreenDialog ? null : _kCupertinoPageTransitionBarrierColor;
@override
String? get barrierLabel => null;
......@@ -791,8 +804,6 @@ class _CupertinoEdgeShadowDecoration extends Decoration {
end: const _CupertinoEdgeShadowDecoration._(
// Eyeballed gradient used to mimic a drop shadow on the start side only.
<Color>[
Color(0x38000000),
Color(0x12000000),
Color(0x04000000),
Color(0x00000000),
],
......
......@@ -9,6 +9,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart';
void main() {
......@@ -937,6 +938,170 @@ void main() {
);
});
group('Cupertino page transitions', () {
CupertinoPageRoute<void> buildRoute({required bool fullscreenDialog}) {
return CupertinoPageRoute<void>(
fullscreenDialog: fullscreenDialog,
builder: (_) => const SizedBox(),
);
}
testWidgets('when route is not fullscreenDialog, it has a barrierColor', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: SizedBox.expand(),
),
);
tester.state<NavigatorState>(find.byType(Navigator)).push(
buildRoute(fullscreenDialog: false),
);
await tester.pumpAndSettle();
expect(tester.widget<ModalBarrier>(find.byType(ModalBarrier).last).color, const Color(0x18000000));
});
testWidgets('when route is a fullscreenDialog, it has no barrierColor', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: SizedBox.expand(),
),
);
tester.state<NavigatorState>(find.byType(Navigator)).push(
buildRoute(fullscreenDialog: true),
);
await tester.pumpAndSettle();
expect(tester.widget<ModalBarrier>(find.byType(ModalBarrier).last).color, isNull);
});
testWidgets('when route is not fullscreenDialog, it has a _CupertinoEdgeShadowDecoration', (WidgetTester tester) async {
PaintPattern paintsShadowRect({required double dx, required Color color}) {
return paints..everything((Symbol methodName, List<dynamic> arguments) {
if (methodName != #drawRect)
return true;
final Rect rect = arguments[0] as Rect;
final Color paintColor = (arguments[1] as Paint).color;
if (rect.top != 0 || rect.width != 1.0 || rect.height != 600)
// _CupertinoEdgeShadowDecoration draws the shadows with a series of
// differently colored 1px-wide rects. Skip rects that aren't being
// drawn by the _CupertinoEdgeShadowDecoration.
return true;
if ((rect.left - dx).abs() >= 1)
// Skip calls for rects until the one with the given position offset
return true;
if (paintColor.value == color.value)
return true;
throw '''
For a rect with an expected left-side position: $dx (drawn at ${rect.left}):
Expected a rect with color: $color,
And drew a rect with color: $paintColor.
''';
});
}
await tester.pumpWidget(
const MaterialApp(
home: SizedBox.expand(),
),
);
tester.state<NavigatorState>(find.byType(Navigator)).push(
buildRoute(fullscreenDialog: false),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 1));
final RenderBox box = tester.firstRenderObject<RenderBox>(find.byType(CustomPaint));
// Animation starts with effectively no shadow
expect(box, paintsShadowRect(dx: 795, color: const Color(0x00000000)));
expect(box, paintsShadowRect(dx: 785, color: const Color(0x00000000)));
expect(box, paintsShadowRect(dx: 775, color: const Color(0x00000000)));
expect(box, paintsShadowRect(dx: 765, color: const Color(0x00000000)));
expect(box, paintsShadowRect(dx: 755, color: const Color(0x00000000)));
await tester.pump(const Duration(milliseconds: 100));
// Part-way through the transition, the shadow is approaching the full gradient
expect(box, paintsShadowRect(dx: 296, color: const Color(0x03000000)));
expect(box, paintsShadowRect(dx: 286, color: const Color(0x02000000)));
expect(box, paintsShadowRect(dx: 276, color: const Color(0x01000000)));
expect(box, paintsShadowRect(dx: 266, color: const Color(0x00000000)));
expect(box, paintsShadowRect(dx: 266, color: const Color(0x00000000)));
await tester.pumpAndSettle();
// At the end of the transition, the shadow is a gradient between
// 0x04000000 and 0x00000000 and is now offscreen
expect(box, paintsShadowRect(dx: -1, color: const Color(0x04000000)));
expect(box, paintsShadowRect(dx: -10, color: const Color(0x03000000)));
expect(box, paintsShadowRect(dx: -20, color: const Color(0x02000000)));
expect(box, paintsShadowRect(dx: -30, color: const Color(0x01000000)));
expect(box, paintsShadowRect(dx: -40, color: const Color(0x00000000)));
// Start animation in reverse
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(box, paintsShadowRect(dx: 498, color: const Color(0x04000000)));
expect(box, paintsShadowRect(dx: 488, color: const Color(0x03000000)));
expect(box, paintsShadowRect(dx: 478, color: const Color(0x02000000)));
expect(box, paintsShadowRect(dx: 468, color: const Color(0x01000000)));
expect(box, paintsShadowRect(dx: 458, color: const Color(0x00000000)));
await tester.pump(const Duration(milliseconds: 250));
// At the end of the animation, the shadow approaches full transparency
expect(box, paintsShadowRect(dx: 794, color: const Color(0x01000000)));
expect(box, paintsShadowRect(dx: 784, color: const Color(0x00000000)));
expect(box, paintsShadowRect(dx: 774, color: const Color(0x00000000)));
expect(box, paintsShadowRect(dx: 764, color: const Color(0x00000000)));
expect(box, paintsShadowRect(dx: 754, color: const Color(0x00000000)));
});
testWidgets('when route is fullscreenDialog, it has no visible _CupertinoEdgeShadowDecoration', (WidgetTester tester) async {
PaintPattern paintsNoShadows() {
return paints..everything((Symbol methodName, List<dynamic> arguments) {
if (methodName != #drawRect)
return true;
final Rect rect = arguments[0] as Rect;
// _CupertinoEdgeShadowDecoration draws the shadows with a series of
// differently colored 1px rects. Skip all rects not drawn by a
// _CupertinoEdgeShadowDecoration.
if (rect.width != 1.0)
return true;
throw '''
Expected: no rects with a width of 1px.
Found: $rect.
''';
});
}
await tester.pumpWidget(
const MaterialApp(
home: SizedBox.expand(),
),
);
final RenderBox box = tester.firstRenderObject<RenderBox>(find.byType(CustomPaint));
tester.state<NavigatorState>(find.byType(Navigator)).push(
buildRoute(fullscreenDialog: true),
);
await tester.pumpAndSettle();
expect(box, paintsNoShadows());
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pumpAndSettle();
expect(box, paintsNoShadows());
});
});
testWidgets('ModalPopup overlay dark mode', (WidgetTester tester) async {
late StateSetter stateSetter;
Brightness brightness = Brightness.light;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment