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,
|
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 {
|
|||||||
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 {
|
|||||||
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 @@
|
|||||||
|
|
||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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();
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user