Enable physical model shadows (with animation) and change elevation to a double (#9756)

Fixes https://github.com/flutter/flutter/issues/9342
This commit is contained in:
Jason Simmons 2017-05-03 12:11:01 -07:00 committed by GitHub
parent 2051669ac7
commit b586a97ad2
27 changed files with 192 additions and 192 deletions

View file

@ -66,7 +66,7 @@ class TileScrollLayout extends StatelessWidget {
return new Padding(
padding:const EdgeInsets.all(5.0),
child: new Material(
elevation: index % 5 + 1,
elevation: (index % 5 + 1).toDouble(),
color: Colors.white,
child: new IconBar(),
),

View file

@ -116,7 +116,7 @@ class _CalculatorState extends State<Calculator> {
return new Scaffold(
appBar: new AppBar(
backgroundColor: Theme.of(context).canvasColor,
elevation: 0
elevation: 0.0
),
body: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,

View file

@ -127,7 +127,7 @@ class ColorsDemo extends StatelessWidget {
length: allPalettes.length,
child: new Scaffold(
appBar: new AppBar(
elevation: 0,
elevation: 0.0,
title: const Text('Colors'),
bottom: new TabBar(
isScrollable: true,

View file

@ -162,7 +162,7 @@ class _Heading extends StatelessWidget {
height: (screenSize.height - kToolbarHeight) * 1.35,
child: new Material(
type: MaterialType.card,
elevation: 0,
elevation: 0.0,
child: new Padding(
padding: const EdgeInsets.only(left: 16.0, top: 18.0, right: 16.0, bottom: 24.0),
child: new CustomMultiChildLayout(
@ -302,7 +302,7 @@ class _OrderPageState extends State<OrderPage> {
.where((Product product) => product != widget.order.product)
.map((Product product) {
return new Card(
elevation: 1,
elevation: 1.0,
child: new Image.asset(
product.imageAsset,
fit: BoxFit.contain,

View file

@ -39,10 +39,10 @@ class ShrinePage extends StatefulWidget {
/// Defines the Scaffold, AppBar, etc that the demo pages have in common.
class ShrinePageState extends State<ShrinePage> {
int _appBarElevation = 0;
double _appBarElevation = 0.0;
bool _handleScrollNotification(ScrollNotification notification) {
final int elevation = notification.metrics.extentBefore <= 0.0 ? 0 : 1;
final double elevation = notification.metrics.extentBefore <= 0.0 ? 0.0 : 1.0;
if (elevation != _appBarElevation) {
setState(() {
_appBarElevation = elevation;

View file

@ -193,7 +193,7 @@ class StockHomeState extends State<StockHome> {
Widget buildAppBar() {
return new AppBar(
elevation: 0,
elevation: 0.0,
title: new Text(StockStrings.of(context).title()),
actions: <Widget>[
new IconButton(

View file

@ -157,7 +157,7 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
this.actions,
this.flexibleSpace,
this.bottom,
this.elevation: 4,
this.elevation: 4.0,
this.backgroundColor,
this.brightness,
this.iconTheme,
@ -236,10 +236,8 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
/// The z-coordinate at which to place this app bar.
///
/// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12, 16, 24
///
/// Defaults to 4, the appropriate elevation for app bars.
final int elevation;
final double elevation;
/// The color to use for the app bar's material. Typically this should be set
/// along with [brightness], [iconTheme], [textTheme].
@ -566,7 +564,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
final List<Widget> actions;
final Widget flexibleSpace;
final PreferredSizeWidget bottom;
final int elevation;
final double elevation;
final bool forceElevated;
final Color backgroundColor;
final Brightness brightness;
@ -607,7 +605,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
actions: actions,
flexibleSpace: flexibleSpace,
bottom: bottom,
elevation: forceElevated || overlapsContent || (pinned && shrinkOffset > maxExtent - minExtent) ? elevation ?? 4 : 0,
elevation: forceElevated || overlapsContent || (pinned && shrinkOffset > maxExtent - minExtent) ? elevation ?? 4.0 : 0.0,
backgroundColor: backgroundColor,
brightness: brightness,
iconTheme: iconTheme,
@ -774,15 +772,13 @@ class SliverAppBar extends StatefulWidget {
/// The z-coordinate at which to place this app bar when it is above other
/// content.
///
/// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12, 16, 24
///
/// Defaults to 4, the appropriate elevation for app bars.
///
/// If [forceElevated] is false, the elevation is ignored when the app bar has
/// no content underneath it. For example, if the app bar is [pinned] but no
/// content is scrolled under it, or if it scrolls with the content, then no
/// shadow is drawn, regardless of the value of [elevation].
final int elevation;
final double elevation;
/// Whether to show the shadow appropriate for the [elevation] even if the
/// content is not scrolled under the [AppBar].

View file

@ -448,7 +448,7 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr
children: <Widget>[
new Positioned.fill(
child: new Material( // Casts shadow.
elevation: 8,
elevation: 8.0,
color: widget.type == BottomNavigationBarType.shifting ? _backgroundColor : null
)
),

View file

@ -192,17 +192,13 @@ class MaterialButton extends StatefulWidget {
/// The z-coordinate at which to place this button.
///
/// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12, 16, 24
///
/// Defaults to 0.
final int elevation;
final double elevation;
/// The z-coordinate at which to place this button when highlighted.
///
/// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12, 16, 24
///
/// Defaults to 0.
final int highlightElevation;
final double highlightElevation;
/// The smallest horizontal extent that the button will occupy.
///
@ -290,7 +286,7 @@ class _MaterialButtonState extends State<MaterialButton> {
final TextStyle style = theme.textTheme.button.copyWith(color: textColor);
final ButtonTheme buttonTheme = ButtonTheme.of(context);
final double height = widget.height ?? buttonTheme.height;
final int elevation = (_highlight ? widget.highlightElevation : widget.elevation) ?? 0;
final double elevation = (_highlight ? widget.highlightElevation : widget.elevation) ?? 0.0;
final bool hasColorOrElevation = (widget.color != null || elevation > 0);
Widget contents = IconTheme.merge(
data: new IconThemeData(

View file

@ -23,7 +23,7 @@ class Card extends StatelessWidget {
const Card({
Key key,
this.color,
this.elevation: 2,
this.elevation: 2.0,
this.child
}) : super(key: key);
@ -35,10 +35,8 @@ class Card extends StatelessWidget {
/// The z-coordinate at which to place this card.
///
/// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12, 16, 24
///
/// Defaults to 2, the appropriate elevation for cards.
final int elevation;
final double elevation;
@override
Widget build(BuildContext context) {

View file

@ -51,7 +51,7 @@ class Dialog extends StatelessWidget {
child: new ConstrainedBox(
constraints: const BoxConstraints(minWidth: 280.0),
child: new Material(
elevation: 24,
elevation: 24.0,
color: _getColor(context),
type: MaterialType.card,
child: child

View file

@ -68,16 +68,14 @@ class Drawer extends StatelessWidget {
/// Typically used in the [Scaffold.drawer] property.
const Drawer({
Key key,
this.elevation: 16,
this.elevation: 16.0,
this.child
}) : super(key: key);
/// The z-coordinate at which to place this drawer.
///
/// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12, 16, 24
///
/// Defaults to 16, the appropriate elevation for drawers.
final int elevation;
final double elevation;
/// The widget below this widget in the tree.
///

View file

@ -48,8 +48,8 @@ class FloatingActionButton extends StatefulWidget {
this.tooltip,
this.backgroundColor,
this.heroTag,
this.elevation: 6,
this.highlightElevation: 12,
this.elevation: 6.0,
this.highlightElevation: 12.0,
@required this.onPressed,
this.mini: false
}) : super(key: key);
@ -80,18 +80,14 @@ class FloatingActionButton extends StatefulWidget {
/// The z-coordinate at which to place this button.
///
/// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12, 16, 24
///
/// Defaults to 6, the appropriate elevation for floating action buttons.
final int elevation;
final double elevation;
/// The z-coordinate at which to place this button when the user is touching the button.
///
/// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12, 16, 24
///
/// Defaults to 12, the appropriate elevation for floating action buttons
/// while they are being touched.
final int highlightElevation;
final double highlightElevation;
/// Controls the size of this button.
///

View file

@ -7,7 +7,6 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'constants.dart';
import 'shadows.dart';
import 'theme.dart';
/// Signature for the callback used by ink effects to obtain the rectangle for the effect.
@ -113,7 +112,7 @@ class Material extends StatefulWidget {
const Material({
Key key,
this.type: MaterialType.canvas,
this.elevation: 0,
this.elevation: 0.0,
this.color,
this.textStyle,
this.borderRadius,
@ -133,10 +132,8 @@ class Material extends StatefulWidget {
/// The z-coordinate at which to place this material.
///
/// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12, 16, 24
///
/// Defaults to 0.
final int elevation;
final double elevation;
/// The color to paint the material.
///
@ -189,9 +186,6 @@ class Material extends StatefulWidget {
/// The default radius of an ink splash in logical pixels.
static const double defaultSplashRadius = 35.0;
/// Temporary flag used to enable the PhysicalModel shadow implementation.
static bool debugEnablePhysicalModel = false;
}
class _MaterialState extends State<Material> with TickerProviderStateMixin {
@ -237,63 +231,38 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin {
)
);
if (Material.debugEnablePhysicalModel) {
if (widget.type == MaterialType.circle) {
contents = new PhysicalModel(
shape: BoxShape.circle,
elevation: widget.elevation,
color: backgroundColor,
child: contents,
);
} else if (widget.type == MaterialType.transparency) {
if (radius == null) {
contents = new ClipRect(child: contents);
} else {
contents = new ClipRRect(
borderRadius: radius,
child: contents
);
}
if (widget.type == MaterialType.circle) {
contents = new AnimatedPhysicalModel(
curve: Curves.fastOutSlowIn,
duration: kThemeChangeDuration,
shape: BoxShape.circle,
elevation: widget.elevation,
color: backgroundColor,
animateColor: false,
child: contents,
);
} else if (widget.type == MaterialType.transparency) {
if (radius == null) {
contents = new ClipRect(child: contents);
} else {
contents = new PhysicalModel(
shape: BoxShape.rectangle,
borderRadius: radius ?? BorderRadius.zero,
elevation: widget.elevation,
color: backgroundColor,
child: contents,
);
}
} else {
if (widget.type == MaterialType.circle) {
contents = new ClipOval(child: contents);
} else if (kMaterialEdges[widget.type] != null) {
contents = new ClipRRect(
borderRadius: radius,
child: contents
);
}
}
if (widget.type != MaterialType.transparency) {
contents = new AnimatedContainer(
} else {
contents = new AnimatedPhysicalModel(
curve: Curves.fastOutSlowIn,
duration: kThemeChangeDuration,
decoration: new BoxDecoration(
borderRadius: radius,
boxShadow: widget.elevation == 0 || Material.debugEnablePhysicalModel ?
null : kElevationToShadow[widget.elevation],
shape: widget.type == MaterialType.circle ? BoxShape.circle : BoxShape.rectangle
),
child: new Container(
decoration: new BoxDecoration(
borderRadius: radius,
color: backgroundColor,
shape: widget.type == MaterialType.circle ? BoxShape.circle : BoxShape.rectangle
),
child: contents
)
shape: BoxShape.rectangle,
borderRadius: radius ?? BorderRadius.zero,
elevation: widget.elevation,
color: backgroundColor,
animateColor: false,
child: contents,
);
}
return contents;
}
}

View file

@ -379,7 +379,7 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
final RelativeRect position;
final List<PopupMenuEntry<T>> items;
final dynamic initialValue;
final int elevation;
final double elevation;
final ThemeData theme;
@override
@ -433,15 +433,14 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
/// a [Theme] to use for the menu.
///
/// The `elevation` argument specifies the z-coordinate at which to place the
/// menu. The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9,
/// 12, 16, 24. The elevation defaults to 8, the appropriate elevation for popup
/// menu. The elevation defaults to 8, the appropriate elevation for popup
/// menus.
Future<T> showMenu<T>({
@required BuildContext context,
RelativeRect position,
@required List<PopupMenuEntry<T>> items,
T initialValue,
int elevation: 8
double elevation: 8.0
}) {
assert(context != null);
assert(items != null && items.isNotEmpty);
@ -481,7 +480,7 @@ class PopupMenuButton<T> extends StatefulWidget {
this.initialValue,
this.onSelected,
this.tooltip: 'Show menu',
this.elevation: 8,
this.elevation: 8.0,
this.padding: const EdgeInsets.all(8.0),
this.child
}) : assert(itemBuilder != null),
@ -503,9 +502,7 @@ class PopupMenuButton<T> extends StatefulWidget {
final String tooltip;
/// The z-coordinate at which to place the menu when open.
///
/// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12, 16, 24
final int elevation;
final double elevation;
/// Matches IconButton's 8 dps padding by default. In some cases, notably where
/// this button appears as the trailing element of a list item, it's useful to be able

View file

@ -480,7 +480,7 @@ class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState {
child: new Material(
type: MaterialType.circle,
color: widget.backgroundColor ?? Theme.of(context).canvasColor,
elevation: 2,
elevation: 2.0,
child: new Padding(
padding: const EdgeInsets.all(12.0),
child: new CustomPaint(

View file

@ -43,9 +43,9 @@ class RaisedButton extends StatelessWidget {
this.highlightColor,
this.splashColor,
this.disabledColor,
this.elevation: 2,
this.highlightElevation: 8,
this.disabledElevation: 0,
this.elevation: 2.0,
this.highlightElevation: 8.0,
this.disabledElevation: 0.0,
this.colorBrightness,
this.child
}) : super(key: key);
@ -97,25 +97,19 @@ class RaisedButton extends StatelessWidget {
/// The z-coordinate at which to place this button.
///
/// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12, 16, 24
///
/// Defaults to 2, the appropriate elevation for raised buttons.
final int elevation;
final double elevation;
/// The z-coordinate at which to place this button when highlighted.
///
/// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12, 16, 24
///
/// Defaults to 8, the appropriate elevation for raised buttons while they are
/// being touched.
final int highlightElevation;
final double highlightElevation;
/// The z-coordinate at which to place this button when disabled.
///
/// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12, 16, 24
///
/// Defaults to 0, the appropriate elevation for disabled raised buttons.
final int disabledElevation;
final double disabledElevation;
/// The theme brightness to use for this button.
///

View file

@ -237,7 +237,7 @@ class SnackBar extends StatelessWidget {
Scaffold.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.swipe);
},
child: new Material(
elevation: 6,
elevation: 6.0,
color: backgroundColor ?? _kSnackBackground,
child: new Theme(
data: darkTheme,

View file

@ -591,7 +591,7 @@ class _StepperState extends State<Stepper> with TickerProviderStateMixin {
return new Column(
children: <Widget>[
new Material(
elevation: 2,
elevation: 2.0,
child: new Container(
margin: const EdgeInsets.symmetric(horizontal: 24.0),
child: new Row(

View file

@ -41,7 +41,7 @@ class _TextSelectionToolbar extends StatelessWidget {
}
return new Material(
elevation: 1,
elevation: 1.0,
child: new Container(
height: 44.0,
child: new Row(mainAxisSize: MainAxisSize.min, children: items)

View file

@ -645,7 +645,7 @@ class PhysicalModelLayer extends ContainerLayer {
///
/// The scene must be explicitly recomposited after this property is changed
/// (as described at [Layer]).
int elevation;
double elevation;
/// The background color.
///
@ -657,7 +657,7 @@ class PhysicalModelLayer extends ContainerLayer {
void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
builder.pushPhysicalModel(
rrect: clipRRect.shift(layerOffset),
elevation: elevation.toDouble(),
elevation: elevation,
color: color,
);
addChildrenToScene(builder, layerOffset);

View file

@ -456,7 +456,7 @@ class PaintingContext {
/// * `color` is the background color.
/// * `painter` is a callback that will paint with the `clipRRect` applied. This
/// function calls the `painter` synchronously.
void pushPhysicalModel(bool needsCompositing, Offset offset, Rect bounds, RRect clipRRect, int elevation, Color color, PaintingContextCallback painter) {
void pushPhysicalModel(bool needsCompositing, Offset offset, Rect bounds, RRect clipRRect, double elevation, Color color, PaintingContextCallback painter) {
final Rect offsetBounds = bounds.shift(offset);
final RRect offsetClipRRect = clipRRect.shift(offset);
if (needsCompositing) {

View file

@ -1198,7 +1198,7 @@ class RenderPhysicalModel extends _RenderCustomClip<RRect> {
RenderBox child,
BoxShape shape,
BorderRadius borderRadius: BorderRadius.zero,
int elevation,
double elevation,
Color color,
}) : _shape = shape,
_borderRadius = borderRadius,
@ -1235,9 +1235,9 @@ class RenderPhysicalModel extends _RenderCustomClip<RRect> {
}
/// The z-coordinate at which to place this material.
int get elevation => _elevation;
int _elevation;
set elevation(int value) {
double get elevation => _elevation;
double _elevation;
set elevation(double value) {
assert(value != null);
if (_elevation == value)
return;

View file

@ -472,7 +472,7 @@ class PhysicalModel extends SingleChildRenderObjectWidget {
final BorderRadius borderRadius;
/// The z-coordinate at which to place this physical object.
final int elevation;
final double elevation;
/// The background color.
final Color color;

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart';
@ -44,6 +45,17 @@ class EdgeInsetsTween extends Tween<EdgeInsets> {
EdgeInsets lerp(double t) => EdgeInsets.lerp(begin, end, t);
}
/// An interpolation between two [BorderRadius]s.
class BorderRadiusTween extends Tween<BorderRadius> {
/// Creates a border radius tween.
///
/// The [begin] and [end] arguments must not be null.
BorderRadiusTween({ BorderRadius begin, BorderRadius end }) : super(begin: begin, end: end);
@override
BorderRadius lerp(double t) => BorderRadius.lerp(begin, end, t);
}
/// An interpolation between two [Matrix4]s.
///
/// Currently this class works only for translations.
@ -643,3 +655,86 @@ class _AnimatedDefaultTextStyleState extends AnimatedWidgetBaseState<AnimatedDef
);
}
}
/// Animated version of [PhysicalModel].
class AnimatedPhysicalModel extends ImplicitlyAnimatedWidget {
/// Creates a widget that animates the properties of a [PhysicalModel].
///
/// The [child], [shape], [borderRadius], [elevation], [color], [curve], and
/// [duration] arguments must not be null.
///
/// Animating [color] is optional and is controlled by the [animateColor] flag.
const AnimatedPhysicalModel({
Key key,
@required this.child,
@required this.shape,
this.borderRadius: BorderRadius.zero,
@required this.elevation,
@required this.color,
this.animateColor: true,
Curve curve: Curves.linear,
@required Duration duration,
}) : assert(child != null),
assert(shape != null),
assert(borderRadius != null),
assert(elevation != null),
assert(color != null),
super(key: key, curve: curve, duration: duration);
/// The widget below this widget in the tree.
final Widget child;
/// The type of shape.
///
/// This property is not animated.
final BoxShape shape;
/// The target border radius of the rounded corners for a rectangle shape.
final BorderRadius borderRadius;
/// The target z-coordinate at which to place this physical object.
final double elevation;
/// The target background color.
final Color color;
/// Whether the color should be animated.
final bool animateColor;
@override
_AnimatedPhysicalModelState createState() => new _AnimatedPhysicalModelState();
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('shape: $shape');
description.add('borderRadius: $borderRadius');
description.add('elevation: $elevation');
description.add('color: $color');
description.add('animateColor: $animateColor');
}
}
class _AnimatedPhysicalModelState extends AnimatedWidgetBaseState<AnimatedPhysicalModel> {
BorderRadiusTween _borderRadius;
Tween<double> _elevation;
ColorTween _color;
@override
void forEachTween(TweenVisitor<dynamic> visitor) {
_borderRadius = visitor(_borderRadius, widget.borderRadius, (dynamic value) => new BorderRadiusTween(begin: value));
_elevation = visitor(_elevation, widget.elevation, (dynamic value) => new Tween<double>(begin: value));
_color = visitor(_color, widget.color, (dynamic value) => new ColorTween(begin: value));
}
@override
Widget build(BuildContext context) {
return new PhysicalModel(
child: widget.child,
shape: widget.shape,
borderRadius: _borderRadius.evaluate(animation),
elevation: _elevation.evaluate(animation),
color: widget.animateColor ? _color.evaluate(animation) : widget.color,
);
}
}

View file

@ -14,7 +14,7 @@ class NotifyMaterial extends StatelessWidget {
}
}
Widget buildMaterial(int elevation) {
Widget buildMaterial(double elevation) {
return new Center(
child: new SizedBox(
height: 100.0,
@ -27,10 +27,8 @@ Widget buildMaterial(int elevation) {
);
}
List<BoxShadow> getShadow(WidgetTester tester) {
final RenderDecoratedBox box = tester.renderObject(find.byType(DecoratedBox).first);
final BoxDecoration decoration = box.decoration;
return decoration.boxShadow;
RenderPhysicalModel getShadow(WidgetTester tester) {
return tester.renderObject(find.byType(PhysicalModel));
}
class PaintRecorder extends CustomPainter {
@ -115,67 +113,27 @@ void main() {
});
testWidgets('Shadows animate smoothly', (WidgetTester tester) async {
await tester.pumpWidget(buildMaterial(0));
final List<BoxShadow> shadowA = getShadow(tester);
// This code verifies that the PhysicalModel's elevation animates over
// a kThemeChangeDuration time interval.
await tester.pumpWidget(buildMaterial(9));
final List<BoxShadow> shadowB = getShadow(tester);
await tester.pumpWidget(buildMaterial(0.0));
final RenderPhysicalModel modelA = getShadow(tester);
expect(modelA.elevation, equals(0.0));
await tester.pumpWidget(buildMaterial(9.0));
final RenderPhysicalModel modelB = getShadow(tester);
expect(modelB.elevation, equals(0.0));
await tester.pump(const Duration(milliseconds: 1));
final List<BoxShadow> shadowC = getShadow(tester);
final RenderPhysicalModel modelC = getShadow(tester);
expect(modelC.elevation, closeTo(0.0, 0.001));
await tester.pump(kThemeChangeDuration ~/ 2);
final List<BoxShadow> shadowD = getShadow(tester);
final RenderPhysicalModel modelD = getShadow(tester);
expect(modelD.elevation, isNot(closeTo(0.0, 0.001)));
await tester.pump(kThemeChangeDuration);
final List<BoxShadow> shadowE = getShadow(tester);
// This code verifies the following:
// 1. When the elevation is zero, there's no shadow.
// 2. When the elevation isn't zero, there's three shadows.
// 3. When the elevation changes from 0 to 9, one millisecond later, the
// shadows are still more or less indistinguishable from zero.
// 4. Have a kThemeChangeDuration later, they are distinguishable form
// zero.
// 5. ...but still distinguishable from the actual 9 elevation.
// 6. After kThemeChangeDuration, the shadows are exactly the elevation 9
// shadows.
// The point being to verify that the shadows animate, and do so
// continually, not in discrete increments (e.g. not jumping from elevation
// 0 to 1 to 2 to 3 and so forth).
// TODO(ianh): Port this test when we turn the physical model back on.
// 1
expect(shadowA, isNull);
expect(shadowB, isNull);
// 2
expect(shadowC, hasLength(3));
// 3
expect(shadowC[0].offset.dy, closeTo(0.0, 0.001));
expect(shadowC[1].offset.dy, closeTo(0.0, 0.001));
expect(shadowC[2].offset.dy, closeTo(0.0, 0.001));
expect(shadowC[0].blurRadius, closeTo(0.0, 0.001));
expect(shadowC[1].blurRadius, closeTo(0.0, 0.001));
expect(shadowC[2].blurRadius, closeTo(0.0, 0.001));
expect(shadowC[0].spreadRadius, closeTo(0.0, 0.001));
expect(shadowC[1].spreadRadius, closeTo(0.0, 0.001));
expect(shadowC[2].spreadRadius, closeTo(0.0, 0.001));
// 4
expect(shadowD[0].offset.dy, isNot(closeTo(0.0, 0.001)));
expect(shadowD[1].offset.dy, isNot(closeTo(0.0, 0.001)));
expect(shadowD[2].offset.dy, isNot(closeTo(0.0, 0.001)));
expect(shadowD[0].blurRadius, isNot(closeTo(0.0, 0.001)));
expect(shadowD[1].blurRadius, isNot(closeTo(0.0, 0.001)));
expect(shadowD[2].blurRadius, isNot(closeTo(0.0, 0.001)));
expect(shadowD[0].spreadRadius, isNot(closeTo(0.0, 0.001)));
expect(shadowD[1].spreadRadius, isNot(closeTo(0.0, 0.001)));
expect(shadowD[2].spreadRadius, isNot(closeTo(0.0, 0.001)));
// 5
expect(shadowD[0], isNot(shadowE[0]));
expect(shadowD[1], isNot(shadowE[1]));
expect(shadowD[2], isNot(shadowE[2]));
// 6
expect(shadowE, kElevationToShadow[9]);
final RenderPhysicalModel modelE = getShadow(tester);
expect(modelE.elevation, equals(9.0));
});
}

View file

@ -51,11 +51,14 @@ void matches(BorderRadius borderRadius, RadiusType top, RadiusType bottom) {
}
}
// Returns the border radius decoration of an item within a MergeableMaterial.
// This depends on the exact structure of objects built by the Material and
// MergeableMaterial widgets.
BorderRadius getBorderRadius(WidgetTester tester, int index) {
final List<Element> containers = tester.elementList(find.byType(Container))
.toList();
final Container container = containers[index + 2].widget;
final Container container = containers[index].widget;
final BoxDecoration boxDecoration = container.decoration;
return boxDecoration.borderRadius;
@ -1095,7 +1098,7 @@ void main() {
);
List<Widget> boxes = tester.widgetList(find.byType(DecoratedBox)).toList();
int offset = 3;
int offset = 1;
expect(isDivider(boxes[offset], false, true), isTrue);
expect(isDivider(boxes[offset + 1], true, true), isTrue);
@ -1151,7 +1154,7 @@ void main() {
await tester.pump(const Duration(milliseconds: 200));
boxes = tester.widgetList(find.byType(DecoratedBox)).toList();
offset = 3;
offset = 1;
expect(isDivider(boxes[offset], false, true), isTrue);
expect(isDivider(boxes[offset + 1], true, false), isTrue);