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
This commit is contained in:
parent
acf102be95
commit
b717963b43
@ -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 <BoxShadow>[
|
||||
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 <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.
|
||||
///
|
||||
/// 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.
|
||||
|
@ -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 ||
|
||||
|
@ -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<NavigatorState>(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<RenderDecoratedBox>());
|
||||
|
||||
// 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<NavigatorState>(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();
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user