Commit b717963b authored by xster's avatar xster Committed by GitHub

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
parent acf102be
...@@ -25,20 +25,118 @@ final FractionalOffsetTween _kBottomUpTween = new FractionalOffsetTween( ...@@ -25,20 +25,118 @@ final FractionalOffsetTween _kBottomUpTween = new FractionalOffsetTween(
end: FractionalOffset.topLeft, end: FractionalOffset.topLeft,
); );
// BoxDecoration from no shadow to page shadow mimicking iOS page transitions. // Custom decoration from no shadow to page shadow mimicking iOS page
final DecorationTween _kShadowTween = new DecorationTween( // transitions using gradients.
begin: BoxDecoration.none, // No shadow initially. final DecorationTween _kGradientShadowTween = new DecorationTween(
end: const BoxDecoration( begin: _CupertinoEdgeShadowDecoration.none, // No decoration initially.
boxShadow: const <BoxShadow>[ end: const _CupertinoEdgeShadowDecoration(
const BoxShadow( edgeGradient: const LinearGradient(
blurRadius: 10.0, // Spans 5% of the page.
spreadRadius: 4.0, begin: const FractionalOffset(0.95, 0.0),
color: const Color(0x38000000), end: FractionalOffset.topRight,
), // Eyeballed gradient used to mimic a drop shadow on the left side only.
], colors: const <Color>[
const Color(0x00000000),
const Color(0x04000000),
const Color(0x12000000),
const Color(0x38000000)
],
stops: const <double>[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. /// 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 /// 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 { ...@@ -71,7 +169,12 @@ class CupertinoPageTransition extends StatelessWidget {
reverseCurve: Curves.easeIn, reverseCurve: Curves.easeIn,
) )
), ),
_primaryShadowAnimation = _kShadowTween.animate(primaryRouteAnimation), _primaryShadowAnimation = _kGradientShadowTween.animate(
new CurvedAnimation(
parent: primaryRouteAnimation,
curve: Curves.easeOut,
)
),
super(key: key); super(key: key);
// When this page is coming in to cover another page. // When this page is coming in to cover another page.
......
...@@ -1129,9 +1129,6 @@ class BoxDecoration extends Decoration { ...@@ -1129,9 +1129,6 @@ class BoxDecoration extends Decoration {
this.shape: BoxShape.rectangle this.shape: BoxShape.rectangle
}); });
/// A [BoxDecoration] with no decorating properties.
static const BoxDecoration none = const BoxDecoration();
@override @override
bool debugAssertIsValid() { bool debugAssertIsValid() {
assert(shape != BoxShape.circle || assert(shape != BoxShape.circle ||
......
...@@ -4,7 +4,9 @@ ...@@ -4,7 +4,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.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() { void main() {
testWidgets('test Android page transition', (WidgetTester tester) async { testWidgets('test Android page transition', (WidgetTester tester) async {
...@@ -77,14 +79,14 @@ void main() { ...@@ -77,14 +79,14 @@ void main() {
final Offset widget1InitialTopLeft = tester.getTopLeft(find.text('Page 1')); final Offset widget1InitialTopLeft = tester.getTopLeft(find.text('Page 1'));
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next'); tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 150)); await tester.pump(const Duration(milliseconds: 150));
Offset widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1')); Offset widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2')); Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
DecoratedBox box = tester.element(find.byKey(page2Key)).ancestorWidgetOfExactType(DecoratedBox); final RenderDecoratedBox box = tester.element(find.byKey(page2Key))
BoxDecoration decoration = box.decoration; .ancestorRenderObjectOfType(const TypeMatcher<RenderDecoratedBox>());
BoxShadow shadow = decoration.boxShadow[0];
// Page 1 is moving to the left. // Page 1 is moving to the left.
expect(widget1TransientTopLeft.dx < widget1InitialTopLeft.dx, true); expect(widget1TransientTopLeft.dx < widget1InitialTopLeft.dx, true);
...@@ -94,9 +96,14 @@ void main() { ...@@ -94,9 +96,14 @@ void main() {
expect(widget1InitialTopLeft.dy == widget2TopLeft.dy, true); expect(widget1InitialTopLeft.dy == widget2TopLeft.dy, true);
// Page 2 is coming in from the right. // Page 2 is coming in from the right.
expect(widget2TopLeft.dx > widget1InitialTopLeft.dx, true); expect(widget2TopLeft.dx > widget1InitialTopLeft.dx, true);
// The shadow should be exactly half its maximum extent. // The shadow should be drawn to one screen width to the left of where
expect(shadow.blurRadius, 5.0); // the page 2 box is. `paints` tests relative to the painter's given canvas
expect(shadow.spreadRadius, 2.0); // 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(); await tester.pumpAndSettle();
...@@ -107,9 +114,6 @@ void main() { ...@@ -107,9 +114,6 @@ void main() {
tester.state<NavigatorState>(find.byType(Navigator)).pop(); tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 100)); 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')); widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
widget2TopLeft = tester.getTopLeft(find.text('Page 2')); widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
...@@ -122,9 +126,6 @@ void main() { ...@@ -122,9 +126,6 @@ void main() {
expect(widget1InitialTopLeft.dy == widget2TopLeft.dy, true); expect(widget1InitialTopLeft.dy == widget2TopLeft.dy, true);
// Page 2 is leaving towards the right. // Page 2 is leaving towards the right.
expect(widget2TopLeft.dx > widget1InitialTopLeft.dx, true); 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(); await tester.pumpAndSettle();
......
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