From b717963b43a1d61482dabaecbc78d442bff449ea Mon Sep 17 00:00:00 2001 From: xster Date: Fri, 28 Apr 2017 12:50:20 -0700 Subject: [PATCH] Change Cupertino page transition box shadow to a simple custom gradient (#9673) Creates another Decoration for drawing outside the decorated box with a gradient to emulate the shadow. Lets the cupertino transition page's background be transparent. Fixes #9321 --- packages/flutter/lib/src/cupertino/page.dart | 127 ++++++++++++++++-- .../flutter/lib/src/painting/box_painter.dart | 3 - packages/flutter/test/material/page_test.dart | 27 ++-- 3 files changed, 129 insertions(+), 28 deletions(-) diff --git a/packages/flutter/lib/src/cupertino/page.dart b/packages/flutter/lib/src/cupertino/page.dart index 6392a66fb3..e77d11c3e7 100644 --- a/packages/flutter/lib/src/cupertino/page.dart +++ b/packages/flutter/lib/src/cupertino/page.dart @@ -25,20 +25,118 @@ final FractionalOffsetTween _kBottomUpTween = new FractionalOffsetTween( end: FractionalOffset.topLeft, ); -// BoxDecoration from no shadow to page shadow mimicking iOS page transitions. -final DecorationTween _kShadowTween = new DecorationTween( - begin: BoxDecoration.none, // No shadow initially. - end: const BoxDecoration( - boxShadow: const [ - const BoxShadow( - blurRadius: 10.0, - spreadRadius: 4.0, - color: const Color(0x38000000), - ), - ], +// Custom decoration from no shadow to page shadow mimicking iOS page +// transitions using gradients. +final DecorationTween _kGradientShadowTween = new DecorationTween( + begin: _CupertinoEdgeShadowDecoration.none, // No decoration initially. + end: const _CupertinoEdgeShadowDecoration( + edgeGradient: const LinearGradient( + // Spans 5% of the page. + begin: const FractionalOffset(0.95, 0.0), + end: FractionalOffset.topRight, + // Eyeballed gradient used to mimic a drop shadow on the left side only. + colors: const [ + const Color(0x00000000), + const Color(0x04000000), + const Color(0x12000000), + const Color(0x38000000) + ], + stops: const [0.0, 0.3, 0.6, 1.0], + ), ), ); +/// A custom [Decoration] used to paint an extra shadow on the left edge of the +/// box it's decorating. It's like a [BoxDecoration] with only a gradient except +/// it paints to the left of the box instead of behind the box. +class _CupertinoEdgeShadowDecoration extends Decoration { + const _CupertinoEdgeShadowDecoration({ this.edgeGradient }); + + /// A Decoration with no decorating properties. + static const _CupertinoEdgeShadowDecoration none = + const _CupertinoEdgeShadowDecoration(); + + /// A gradient to draw to the left of the box being decorated. + /// FractionalOffsets are relative to the original box translated one box + /// width to the left. + final LinearGradient edgeGradient; + + /// Linearly interpolate between two edge shadow decorations decorations. + /// + /// See also [Decoration.lerp]. + static _CupertinoEdgeShadowDecoration lerp( + _CupertinoEdgeShadowDecoration a, + _CupertinoEdgeShadowDecoration b, + double t + ) { + if (a == null && b == null) + return null; + return new _CupertinoEdgeShadowDecoration( + edgeGradient: LinearGradient.lerp(a?.edgeGradient, b?.edgeGradient, t), + ); + } + + @override + _CupertinoEdgeShadowDecoration lerpFrom(Decoration a, double t) { + if (a is! _CupertinoEdgeShadowDecoration) + return _CupertinoEdgeShadowDecoration.lerp(null, this, t); + return _CupertinoEdgeShadowDecoration.lerp(a, this, t); + } + + @override + _CupertinoEdgeShadowDecoration lerpTo(Decoration b, double t) { + if (b is! _CupertinoEdgeShadowDecoration) + return _CupertinoEdgeShadowDecoration.lerp(this, null, t); + return _CupertinoEdgeShadowDecoration.lerp(this, b, t); + } + + @override + _CupertinoEdgeShadowPainter createBoxPainter([VoidCallback onChanged]) { + return new _CupertinoEdgeShadowPainter(this, onChanged); + } + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) + return true; + if (other.runtimeType != _CupertinoEdgeShadowDecoration) + return false; + final _CupertinoEdgeShadowDecoration typedOther = other; + return edgeGradient == typedOther.edgeGradient; + } + + @override + int get hashCode { + return edgeGradient.hashCode; + } +} + +/// A [BoxPainter] used to draw the page transition shadow using gradients. +class _CupertinoEdgeShadowPainter extends BoxPainter { + _CupertinoEdgeShadowPainter( + @required this._decoration, + VoidCallback onChange + ) : assert(_decoration != null), + super(onChange); + + final _CupertinoEdgeShadowDecoration _decoration; + + @override + void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { + final LinearGradient gradient = _decoration.edgeGradient; + if (gradient == null) + return; + // The drawable space for the gradient is a rect with the same size as + // its parent box one box width to the left of the box. + final Rect rect = + (offset & configuration.size).translate(-configuration.size.width, 0.0); + final Paint paint = new Paint() + ..shader = gradient.createShader(rect); + + canvas.drawRect(rect, paint); + } +} + /// Provides the native iOS page transition animation. /// /// The page slides in from the right and exits in reverse. It also shifts to the left in @@ -71,7 +169,12 @@ class CupertinoPageTransition extends StatelessWidget { reverseCurve: Curves.easeIn, ) ), - _primaryShadowAnimation = _kShadowTween.animate(primaryRouteAnimation), + _primaryShadowAnimation = _kGradientShadowTween.animate( + new CurvedAnimation( + parent: primaryRouteAnimation, + curve: Curves.easeOut, + ) + ), super(key: key); // When this page is coming in to cover another page. diff --git a/packages/flutter/lib/src/painting/box_painter.dart b/packages/flutter/lib/src/painting/box_painter.dart index 8ceb81a4e2..5f7c3ea2e7 100644 --- a/packages/flutter/lib/src/painting/box_painter.dart +++ b/packages/flutter/lib/src/painting/box_painter.dart @@ -1129,9 +1129,6 @@ class BoxDecoration extends Decoration { this.shape: BoxShape.rectangle }); - /// A [BoxDecoration] with no decorating properties. - static const BoxDecoration none = const BoxDecoration(); - @override bool debugAssertIsValid() { assert(shape != BoxShape.circle || diff --git a/packages/flutter/test/material/page_test.dart b/packages/flutter/test/material/page_test.dart index b622c39dc2..9dbb93ac87 100644 --- a/packages/flutter/test/material/page_test.dart +++ b/packages/flutter/test/material/page_test.dart @@ -4,7 +4,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_test/flutter_test.dart' hide TypeMatcher; + +import '../rendering/mock_canvas.dart'; void main() { testWidgets('test Android page transition', (WidgetTester tester) async { @@ -77,14 +79,14 @@ void main() { final Offset widget1InitialTopLeft = tester.getTopLeft(find.text('Page 1')); tester.state(find.byType(Navigator)).pushNamed('/next'); + await tester.pump(); await tester.pump(const Duration(milliseconds: 150)); Offset widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1')); Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2')); - DecoratedBox box = tester.element(find.byKey(page2Key)).ancestorWidgetOfExactType(DecoratedBox); - BoxDecoration decoration = box.decoration; - BoxShadow shadow = decoration.boxShadow[0]; + final RenderDecoratedBox box = tester.element(find.byKey(page2Key)) + .ancestorRenderObjectOfType(const TypeMatcher()); // Page 1 is moving to the left. expect(widget1TransientTopLeft.dx < widget1InitialTopLeft.dx, true); @@ -94,9 +96,14 @@ void main() { expect(widget1InitialTopLeft.dy == widget2TopLeft.dy, true); // Page 2 is coming in from the right. expect(widget2TopLeft.dx > widget1InitialTopLeft.dx, true); - // The shadow should be exactly half its maximum extent. - expect(shadow.blurRadius, 5.0); - expect(shadow.spreadRadius, 2.0); + // The shadow should be drawn to one screen width to the left of where + // the page 2 box is. `paints` tests relative to the painter's given canvas + // rather than relative to the screen so assert that it's one screen + // width to the left of 0 offset box rect and nothing is drawn inside the + // box's rect. + expect(box, paints..rect( + rect: new Rect.fromLTWH(-800.0, 0.0, 800.0, 600.0) + )); await tester.pumpAndSettle(); @@ -107,9 +114,6 @@ void main() { tester.state(find.byType(Navigator)).pop(); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); - box = tester.element(find.byKey(page2Key)).ancestorWidgetOfExactType(DecoratedBox); - decoration = box.decoration; - shadow = decoration.boxShadow[0]; widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1')); widget2TopLeft = tester.getTopLeft(find.text('Page 2')); @@ -122,9 +126,6 @@ void main() { expect(widget1InitialTopLeft.dy == widget2TopLeft.dy, true); // Page 2 is leaving towards the right. expect(widget2TopLeft.dx > widget1InitialTopLeft.dx, true); - // The shadow should be exactly 2/3 of its maximum extent. - expect(shadow.blurRadius, closeTo(6.6, 0.1)); - expect(shadow.spreadRadius, closeTo(2.6, 0.1)); await tester.pumpAndSettle();