Commit a91bc0ba authored by Ian Hickson's avatar Ian Hickson

Material Data Tables (#3337)

+ Add new demo to gallery to show data tables. (This currently doesn't
use a Card; I'll create a Card version in a subsequent patch.)

+ Fix checkbox alignment. It now centers in its box regardless.

+ Add Colors.black54.

+ Some minor fixes to dartdocs.

+ DataTable, DataColumn, DataRow, DataCell

+ RowInkWell

+ Augment dartdocs of materia/debug.dart.

+ DropDownButtonHideUnderline to hide the underline in a drop-down when
  used in a DataTable.

+ Add new capabilities to InkResponse to support RowInkWell.

+ Augment dartdocs of materia/material.dart.

+ Add an assert to catch nested Blocks.

+ Fix a crash in RenderBox when you remove an object and an ancestor
  used its baseline. (https://github.com/flutter/flutter/issues/2874)

+ Fix (and redocument) RenderBaseline/Baseline.

+ Add flex support to IntrinsicColumnWidth.

+ Document more stuff on the RenderTable side.

+ Fix a bug with parentData handling on RenderTable children.

+ Completely rewrite the column width computations. The old logic made
  no sense at all.

+ Add dartdocs to widgets/debug.dart.

+ Add a toString for TableRow.
parent db2f66aa
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart';
class Desert {
Desert(this.name, this.calories, this.fat, this.carbs, this.protein, this.sodium, this.calcium, this.iron);
final String name;
final int calories;
final double fat;
final int carbs;
final double protein;
final int sodium;
final int calcium;
final int iron;
bool selected = false;
}
class DataTableDemo extends StatefulWidget {
@override
_DataTableDemoState createState() => new _DataTableDemoState();
}
class _DataTableDemoState extends State<DataTableDemo> {
int _sortColumnIndex;
bool _sortAscending = true;
final List<Desert> _deserts = [
new Desert('Frozen yogurt', 159, 6.0, 24, 4.0, 87, 14, 1),
new Desert('Ice cream sandwich', 237, 9.0, 37, 4.3, 129, 8, 1),
new Desert('Eclair', 262, 16.0, 24, 6.0, 337, 6, 7),
new Desert('Cupcake', 305, 3.7, 67, 4.3, 413, 3, 8),
new Desert('Gingerbread', 356, 16.0, 49, 3.9, 327, 7, 16),
new Desert('Jelly bean', 375, 0.0, 94, 0.0, 50, 0, 0),
new Desert('Lollipop', 392, 0.2, 98, 0.0, 38, 0, 2),
new Desert('Honeycomb', 408, 3.2, 87, 6.5, 562, 0, 45),
new Desert('Donut', 452, 25.0, 51, 4.9, 326, 2, 22),
new Desert('KitKat', 518, 26.0, 65, 7.0, 54, 12, 6),
];
void _sort/*<T>*/(Comparable<dynamic/*=T*/> getField(Desert d), int columnIndex, bool ascending) {
setState(() {
_deserts.sort((Desert a, Desert b) {
if (!ascending) {
final Desert c = a;
a = b;
b = c;
}
final Comparable<dynamic/*=T*/> aValue = getField(a);
final Comparable<dynamic/*=T*/> bValue = getField(b);
return Comparable.compare(aValue, bValue);
});
_sortColumnIndex = columnIndex;
_sortAscending = ascending;
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: new Text('Data tables')),
body: new Block(
children: <Widget>[
new Material(
child: new IntrinsicHeight(
child: new Block(
scrollDirection: Axis.horizontal,
children: <Widget>[
new DataTable(
sortColumnIndex: _sortColumnIndex,
sortAscending: _sortAscending,
columns: <DataColumn>[
new DataColumn(
label: new Text('Dessert (100g serving)'),
onSort: (int columnIndex, bool ascending) => _sort/*<String>*/((Desert d) => d.name, columnIndex, ascending)
),
new DataColumn(
label: new Text('Calories'),
tooltip: 'The total amount of food energy in the given serving size.',
numeric: true,
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.calories, columnIndex, ascending)
),
new DataColumn(
label: new Text('Fat (g)'),
numeric: true,
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.fat, columnIndex, ascending)
),
new DataColumn(
label: new Text('Carbs (g)'),
numeric: true,
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.carbs, columnIndex, ascending)
),
new DataColumn(
label: new Text('Protein (g)'),
numeric: true,
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.protein, columnIndex, ascending)
),
new DataColumn(
label: new Text('Sodium (mg)'),
numeric: true,
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.sodium, columnIndex, ascending)
),
new DataColumn(
label: new Text('Calcium (%)'),
tooltip: 'The amount of calcium as a percentage of the recommended daily amount.',
numeric: true,
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.calcium, columnIndex, ascending)
),
new DataColumn(
label: new Text('Iron (%)'),
numeric: true,
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.iron, columnIndex, ascending)
),
],
rows: _deserts.map/*<DataRow>*/((Desert desert) {
return new DataRow(
key: new ValueKey<Desert>(desert),
selected: desert.selected,
onSelectChanged: (bool selected) { setState(() { desert.selected = selected; }); },
cells: <DataCell>[
new DataCell(new Text('${desert.name}')),
new DataCell(new Text('${desert.calories}')),
new DataCell(new Text('${desert.fat.toStringAsFixed(1)}')),
new DataCell(new Text('${desert.carbs}')),
new DataCell(new Text('${desert.protein.toStringAsFixed(1)}')),
new DataCell(new Text('${desert.sodium}')),
new DataCell(new Text('${desert.calcium}%')),
new DataCell(new Text('${desert.iron}%')),
]
);
}).toList(growable: false)
)
]
)
)
)
]
)
);
}
}
......@@ -13,6 +13,7 @@ import '../demo/buttons_demo.dart';
import '../demo/cards_demo.dart';
import '../demo/colors_demo.dart';
import '../demo/chip_demo.dart';
import '../demo/data_table_demo.dart';
import '../demo/date_picker_demo.dart';
import '../demo/dialog_demo.dart';
import '../demo/drop_down_demo.dart';
......@@ -122,6 +123,7 @@ class GalleryHomeState extends State<GalleryHome> {
new GalleryItem(title: 'Cards', builder: () => new CardsDemo()),
new GalleryItem(title: 'Chips', builder: () => new ChipDemo()),
new GalleryItem(title: 'Date picker', builder: () => new DatePickerDemo()),
new GalleryItem(title: 'Data tables', builder: () => new DataTableDemo()),
new GalleryItem(title: 'Dialog', builder: () => new DialogDemo()),
new GalleryItem(title: 'Drop-down button', builder: () => new DropDownDemo()),
new GalleryItem(title: 'Expand/collapse list control', builder: () => new TwoLevelListDemo()),
......
......@@ -19,13 +19,14 @@ export 'src/material/chip.dart';
export 'src/material/circle_avatar.dart';
export 'src/material/colors.dart';
export 'src/material/constants.dart';
export 'src/material/data_table.dart';
export 'src/material/date_picker.dart';
export 'src/material/date_picker_dialog.dart';
export 'src/material/dialog.dart';
export 'src/material/divider.dart';
export 'src/material/drawer.dart';
export 'src/material/drawer_header.dart';
export 'src/material/drawer_item.dart';
export 'src/material/divider.dart';
export 'src/material/drop_down.dart';
export 'src/material/flat_button.dart';
export 'src/material/flexible_space_bar.dart';
......@@ -33,10 +34,10 @@ export 'src/material/floating_action_button.dart';
export 'src/material/grid_tile.dart';
export 'src/material/grid_tile_bar.dart';
export 'src/material/icon.dart';
export 'src/material/icons.dart';
export 'src/material/icon_button.dart';
export 'src/material/icon_theme.dart';
export 'src/material/icon_theme_data.dart';
export 'src/material/icons.dart';
export 'src/material/ink_well.dart';
export 'src/material/input.dart';
export 'src/material/list.dart';
......
......@@ -64,6 +64,9 @@ class Checkbox extends StatelessWidget {
/// If null, the checkbox will be displayed as disabled.
final ValueChanged<bool> onChanged;
/// The width of a checkbox widget.
static const double width = 18.0;
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
......@@ -114,10 +117,9 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
}
const double _kMidpoint = 0.5;
const double _kEdgeSize = 18.0;
const double _kEdgeSize = Checkbox.width;
const double _kEdgeRadius = 1.0;
const double _kStrokeWidth = 2.0;
const double _kOffset = kRadialReactionRadius - _kEdgeSize / 2.0;
class _RenderCheckbox extends RenderToggleable {
_RenderCheckbox({
......@@ -135,11 +137,13 @@ class _RenderCheckbox extends RenderToggleable {
@override
void paint(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas;
final double offsetX = _kOffset + offset.dx;
final double offsetY = _kOffset + offset.dy;
paintRadialReaction(canvas, offset, const Point(kRadialReactionRadius, kRadialReactionRadius));
final double offsetX = offset.dx + (size.width - _kEdgeSize) / 2.0;
final double offsetY = offset.dy + (size.height - _kEdgeSize) / 2.0;
paintRadialReaction(canvas, offset, size.center(Point.origin));
double t = position.value;
......
......@@ -12,6 +12,7 @@ class Colors {
/// Completely invisible.
static const Color transparent = const Color(0x00000000);
/// Completely opaque black.
static const Color black = const Color(0xFF000000);
......@@ -21,6 +22,11 @@ class Colors {
/// Black with 54% opacity.
static const Color black54 = const Color(0x8A000000);
/// Black with 38% opacity.
///
/// Used for the placeholder text in data tables in light themes.
static const Color black38 = const Color(0x61000000);
/// Black with 45% opacity.
///
/// Used for modal barriers.
......@@ -28,14 +34,15 @@ class Colors {
/// Black with 26% opacity.
///
/// Used for disabled radio buttons and text of disabled flat buttons in the light theme.
/// Used for disabled radio buttons and the text of disabled flat buttons in light themes.
static const Color black26 = const Color(0x42000000);
/// Black with 12% opacity.
///
/// Used for the background of disabled raised buttons in the light theme.
/// Used for the background of disabled raised buttons in light themes.
static const Color black12 = const Color(0x1F000000);
/// Completely opaque white.
static const Color white = const Color(0xFFFFFFFF);
......@@ -44,17 +51,18 @@ class Colors {
/// White with 32% opacity.
///
/// Used for disabled radio buttons and text of disabled flat buttons in the dark theme.
/// Used for disabled radio buttons and the text of disabled flat buttons in dark themes.
static const Color white30 = const Color(0x4DFFFFFF);
/// White with 12% opacity.
///
/// Used for the background of disabled raised buttons in the dark theme.
/// Used for the background of disabled raised buttons in dark themes.
static const Color white12 = const Color(0x1FFFFFFF);
/// White with 10% opacity.
static const Color white10 = const Color(0x1AFFFFFF);
/// The red primary swatch.
static const Map<int, Color> red = const <int, Color>{
50: const Color(0xFFFFEBEE),
......
This diff is collapsed.
......@@ -7,9 +7,19 @@ import 'package:flutter/widgets.dart';
import 'material.dart';
import 'scaffold.dart';
/// Throws an exception of the given build context is not contained in a [Material] widget.
/// Asserts that the given context has a [Material] ancestor.
///
/// Does nothing if asserts are disabled.
/// Used by many material design widgets to make sure that they are
/// only used in contexts where they can print ink onto some material.
///
/// To invoke this function, use the following pattern, typically in the
/// relevant Widget's [build] method:
///
/// ```dart
/// assert(debugCheckHasMaterial(context));
/// ```
///
/// Does nothing if asserts are disabled. Always returns true.
bool debugCheckHasMaterial(BuildContext context) {
assert(() {
if (context.widget is! Material && context.ancestorWidgetOfExactType(Material) == null) {
......@@ -33,9 +43,22 @@ bool debugCheckHasMaterial(BuildContext context) {
return true;
}
/// Throws an exception of the given build context is not contained in a [Scaffold] widget.
/// Asserts that the given context has a [Scaffold] ancestor.
///
/// Used by some material design widgets to make sure that they are
/// only used in contexts where they can communicate with a Scaffold.
///
/// For example, the [AppBar] in some situations requires a Scaffold
/// to do the right thing with scrolling.
///
/// To invoke this function, use the following pattern, typically in the
/// relevant Widget's [build] method:
///
/// ```dart
/// assert(debugCheckHasScaffold(context));
/// ```
///
/// Does nothing if asserts are disabled.
/// Does nothing if asserts are disabled. Always returns true.
bool debugCheckHasScaffold(BuildContext context) {
assert(() {
if (Scaffold.of(context) == null) {
......
......@@ -15,11 +15,13 @@ import 'theme.dart';
import 'material.dart';
const Duration _kDropDownMenuDuration = const Duration(milliseconds: 300);
const double _kTopMargin = 6.0;
const double _kMenuItemHeight = 48.0;
const EdgeInsets _kMenuVerticalPadding = const EdgeInsets.symmetric(vertical: 8.0);
const EdgeInsets _kMenuHorizontalPadding = const EdgeInsets.symmetric(horizontal: 36.0);
const double _kBaselineOffsetFromBottom = 20.0;
const Border _kDropDownUnderline = const Border(bottom: const BorderSide(color: const Color(0xFFBDBDBD), width: 2.0));
const double _kBottomBorderHeight = 2.0;
const Border _kDropDownUnderline = const Border(bottom: const BorderSide(color: const Color(0xFFBDBDBD), width: _kBottomBorderHeight));
class _DropDownMenuPainter extends CustomPainter {
const _DropDownMenuPainter({
......@@ -235,7 +237,7 @@ class DropDownMenuItem<T> extends StatelessWidget {
Widget build(BuildContext context) {
return new Container(
height: _kMenuItemHeight,
padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 6.0),
padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: _kTopMargin),
child: new DefaultTextStyle(
style: Theme.of(context).textTheme.subhead,
child: new Baseline(
......@@ -247,6 +249,32 @@ class DropDownMenuItem<T> extends StatelessWidget {
}
}
/// An inherited widget that causes any descendant [DropDownButton]
/// widgets to not include their regular underline.
///
/// This is used by [DataTable] to remove the underline from any
/// [DropDownButton] widgets placed within material data tables, as
/// required by the material design specification.
class DropDownButtonHideUnderline extends InheritedWidget {
/// Creates a [DropDownButtonHideUnderline]. A non-null [child] must
/// be given.
DropDownButtonHideUnderline({
Key key,
Widget child
}) : super(key: key, child: child) {
assert(child != null);
}
/// Returns whether the underline of [DropDownButton] widgets should
/// be hidden.
static bool at(BuildContext context) {
return context.inheritFromWidgetOfExactType(DropDownButtonHideUnderline) != null;
}
@override
bool updateShouldNotify(DropDownButtonHideUnderline old) => false;
}
/// A material design button for selecting from a list of items.
///
/// A dropdown button lets the user select from a number of items. The button
......@@ -336,11 +364,7 @@ class _DropDownButtonState<T> extends State<DropDownButton<T>> {
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
return new GestureDetector(
onTap: _handleTap,
child: new Container(
decoration: new BoxDecoration(border: _kDropDownUnderline),
child: new Row(
Widget result = new Row(
children: <Widget>[
new IndexedStack(
children: config.items,
......@@ -350,12 +374,25 @@ class _DropDownButtonState<T> extends State<DropDownButton<T>> {
),
new Container(
child: new Icon(icon: Icons.arrow_drop_down, size: 36.0),
padding: const EdgeInsets.only(top: 6.0)
padding: const EdgeInsets.only(top: _kTopMargin)
)
],
mainAxisAlignment: MainAxisAlignment.collapse
)
)
);
if (DropDownButtonHideUnderline.at(context)) {
result = new Padding(
padding: const EdgeInsets.only(bottom: _kBottomBorderHeight),
child: result
);
} else {
result = new Container(
decoration: const BoxDecoration(border: _kDropDownUnderline),
child: result
);
}
return new GestureDetector(
onTap: _handleTap,
child: result
);
}
}
......@@ -46,7 +46,7 @@ class Icon extends StatelessWidget {
/// Icons occupy a square with width and height equal to size.
final double size;
/// The icon to display.
/// The icon to display. The available icons are described in [Icons].
final IconData icon;
/// The color to use when drawing the icon.
......
......@@ -64,6 +64,31 @@ class InkResponse extends StatefulWidget {
/// The shape (e.g., circle, rectangle) to use for the highlight drawn around this part of the material.
final BoxShape highlightShape;
/// The rectangle to use for the highlight effect and for clipping
/// the splash effects if [containedInkWell] is true.
///
/// This method is intended to be overridden by descendants that
/// specialize [InkResponse] for unusual cases. For example,
/// [RowInkWell] implements this method to return the rectangle
/// corresponding to the row that the widget is in.
///
/// The default behavior returns null, which is equivalent to
/// returning the referenceBox argument's bounding box (though
/// slightly more efficient).
RectCallback getRectCallback(RenderBox referenceBox) => null;
/// Asserts that the given context satisfies the prerequisites for
/// this class.
///
/// This method is intended to be overridden by descendants that
/// specialize [InkResponse] for unusual cases. For example,
/// [RowInkWell] implements this method to verify that the widget is
/// in a table.
bool debugCheckContext(BuildContext context) {
assert(debugCheckHasMaterial(context));
return true;
}
@override
_InkResponseState<InkResponse> createState() => new _InkResponseState<InkResponse>();
}
......@@ -85,6 +110,7 @@ class _InkResponseState<T extends InkResponse> extends State<T> {
referenceBox: referenceBox,
color: Theme.of(context).highlightColor,
shape: config.highlightShape,
rectCallback: config.getRectCallback(referenceBox),
onRemoved: () {
assert(_lastHighlight != null);
_lastHighlight = null;
......@@ -105,11 +131,13 @@ class _InkResponseState<T extends InkResponse> extends State<T> {
RenderBox referenceBox = context.findRenderObject();
assert(Material.of(context) != null);
InkSplash splash;
RectCallback rectCallback = config.getRectCallback(referenceBox);
splash = Material.of(context).splashAt(
referenceBox: referenceBox,
position: referenceBox.globalToLocal(position),
color: Theme.of(context).splashColor,
containedInWell: config.containedInkWell,
containedInkWell: config.containedInkWell,
rectCallback: config.containedInkWell ? rectCallback : null,
onRemoved: () {
if (_splashes != null) {
assert(_splashes.contains(splash));
......@@ -176,7 +204,7 @@ class _InkResponseState<T extends InkResponse> extends State<T> {
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
assert(config.debugCheckContext(context));
final bool enabled = config.onTap != null || config.onDoubleTap != null || config.onLongPress != null;
return new GestureDetector(
onTapDown: enabled ? _handleTapDown : null,
......
......@@ -12,10 +12,15 @@ import 'constants.dart';
import 'shadows.dart';
import 'theme.dart';
/// The various kinds of material in material design.
/// Signature for callback used by ink effects to obtain the rectangle for the effect.
typedef Rect RectCallback();
/// The various kinds of material in material design. Used to
/// configure the default behavior of [Material] widgets.
///
/// See also:
/// * [Material]
///
/// * [Material], in particular [Material.type]
/// * [kMaterialEdges]
enum MaterialType {
/// Infinite extent using default theme canvas color.
......@@ -27,7 +32,7 @@ enum MaterialType {
/// A circle, no color by default (used for floating action buttons).
circle,
/// Rounded edges, no color by default (used for MaterialButton buttons).
/// Rounded edges, no color by default (used for [MaterialButton] buttons).
button,
/// A transparent piece of material that draws ink splashes and highlights.
......@@ -95,13 +100,36 @@ abstract class MaterialInkController {
Color get color;
/// Begin a splash, centered at position relative to referenceBox.
/// If containedInWell is true, then the splash will be sized to fit
/// the referenceBox, then clipped to it when drawn.
///
/// If containedInkWell is true, then the splash will be sized to fit
/// the well rectangle, then clipped to it when drawn. The well
/// rectangle is the box returned by rectCallback, if provided, or
/// otherwise is the bounds of the referenceBox.
///
/// If containedInkWell is false, then rectCallback should be null.
/// The ink splash is clipped only to the edges of the [Material].
/// This is the default.
///
/// When the splash is removed, onRemoved will be invoked.
InkSplash splashAt({ RenderBox referenceBox, Point position, Color color, bool containedInWell, VoidCallback onRemoved });
InkSplash splashAt({
RenderBox referenceBox,
Point position,
Color color,
bool containedInkWell: false,
RectCallback rectCallback,
VoidCallback onRemoved
});
/// Begin a highlight, coincident with the referenceBox.
InkHighlight highlightAt({ RenderBox referenceBox, Color color, BoxShape shape: BoxShape.rectangle, VoidCallback onRemoved });
/// Begin a highlight animation. If a rectCallback is given, then it
/// provides the highlight rectangle, otherwise, the highlight
/// rectangle is coincident with the referenceBox.
InkHighlight highlightAt({
RenderBox referenceBox,
Color color,
BoxShape shape: BoxShape.rectangle,
RectCallback rectCallback,
VoidCallback onRemoved
});
/// Add an arbitrary InkFeature to this InkController.
void addInkFeature(InkFeature feature);
......@@ -147,22 +175,27 @@ class Material extends StatefulWidget {
/// The widget below this widget in the tree.
final Widget child;
/// The kind of material (e.g., card or canvas).
/// The kind of material to show (e.g., card or canvas). This
/// affects the shape of the widget, the roundness of its corners if
/// the shape is rectangular, and the default color.
final MaterialType type;
/// The z-coordinate at which to place this material.
final int elevation;
/// The color of the material.
/// The color to paint the material.
///
/// Must be opaque. To create a transparent piece of material, use
/// [MaterialType.transparency].
///
/// By default, the color is derived from the [type] of material.
final Color color;
/// The typographical style to use for text within this material.
final TextStyle textStyle;
/// The ink controller from the closest instance of this class that encloses the given context.
/// The ink controller from the closest instance of this class that
/// encloses the given context.
static MaterialInkController of(BuildContext context) {
final _RenderInkFeatures result = context.ancestorRenderObjectOfType(const TypeMatcher<_RenderInkFeatures>());
return result;
......@@ -273,13 +306,24 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
RenderBox referenceBox,
Point position,
Color color,
bool containedInWell,
bool containedInkWell: false,
RectCallback rectCallback,
VoidCallback onRemoved
}) {
double radius;
if (containedInWell) {
radius = _getSplashTargetSize(referenceBox.size, position);
RectCallback clipCallback;
if (containedInkWell) {
Size size;
if (rectCallback != null) {
size = rectCallback().size;
clipCallback = rectCallback;
} else {
size = referenceBox.size;
clipCallback = () => Point.origin & size;
}
radius = _getSplashTargetSize(size, position);
} else {
assert(rectCallback == null);
radius = _kDefaultSplashRadius;
}
_InkSplash splash = new _InkSplash(
......@@ -288,8 +332,8 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
position: position,
color: color,
targetRadius: radius,
clipToReferenceBox: containedInWell,
repositionToReferenceBox: !containedInWell,
clipCallback: clipCallback,
repositionToReferenceBox: !containedInkWell,
onRemoved: onRemoved
);
addInkFeature(splash);
......@@ -309,6 +353,7 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
RenderBox referenceBox,
Color color,
BoxShape shape: BoxShape.rectangle,
RectCallback rectCallback,
VoidCallback onRemoved
}) {
_InkHighlight highlight = new _InkHighlight(
......@@ -316,6 +361,7 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
referenceBox: referenceBox,
color: color,
shape: shape,
rectCallback: rectCallback,
onRemoved: onRemoved
);
addInkFeature(highlight);
......@@ -436,7 +482,7 @@ class _InkSplash extends InkFeature implements InkSplash {
this.position,
this.color,
this.targetRadius,
this.clipToReferenceBox,
this.clipCallback,
this.repositionToReferenceBox,
VoidCallback onRemoved
}) : super(renderer: renderer, referenceBox: referenceBox, onRemoved: onRemoved) {
......@@ -460,7 +506,7 @@ class _InkSplash extends InkFeature implements InkSplash {
final Point position;
final Color color;
final double targetRadius;
final bool clipToReferenceBox;
final RectCallback clipCallback;
final bool repositionToReferenceBox;
Animation<double> _radius;
......@@ -499,25 +545,23 @@ class _InkSplash extends InkFeature implements InkSplash {
void paintFeature(Canvas canvas, Matrix4 transform) {
Paint paint = new Paint()..color = color.withAlpha(_alpha.value);
Point center = position;
if (repositionToReferenceBox)
center = Point.lerp(center, referenceBox.size.center(Point.origin), _radiusController.value);
Offset originOffset = MatrixUtils.getAsTranslation(transform);
if (originOffset == null) {
canvas.save();
canvas.transform(transform.storage);
if (clipToReferenceBox)
canvas.clipRect(Point.origin & referenceBox.size);
if (repositionToReferenceBox)
center = Point.lerp(center, Point.origin, _radiusController.value);
if (clipCallback != null)
canvas.clipRect(clipCallback());
canvas.drawCircle(center, _radius.value, paint);
canvas.restore();
} else {
if (clipToReferenceBox) {
if (clipCallback != null) {
canvas.save();
canvas.clipRect(originOffset.toPoint() & referenceBox.size);
canvas.clipRect(clipCallback().shift(originOffset));
}
if (repositionToReferenceBox)
center = Point.lerp(center, referenceBox.size.center(Point.origin), _radiusController.value);
canvas.drawCircle(center + originOffset, _radius.value, paint);
if (clipToReferenceBox)
if (clipCallback != null)
canvas.restore();
}
}
......@@ -527,6 +571,7 @@ class _InkHighlight extends InkFeature implements InkHighlight {
_InkHighlight({
_RenderInkFeatures renderer,
RenderBox referenceBox,
this.rectCallback,
Color color,
this.shape,
VoidCallback onRemoved
......@@ -542,6 +587,8 @@ class _InkHighlight extends InkFeature implements InkHighlight {
).animate(_alphaController);
}
final RectCallback rectCallback;
@override
Color get color => _color;
Color _color;
......@@ -597,13 +644,14 @@ class _InkHighlight extends InkFeature implements InkHighlight {
void paintFeature(Canvas canvas, Matrix4 transform) {
Paint paint = new Paint()..color = color.withAlpha(_alpha.value);
Offset originOffset = MatrixUtils.getAsTranslation(transform);
final Rect rect = (rectCallback != null ? rectCallback() : Point.origin & referenceBox.size);
if (originOffset == null) {
canvas.save();
canvas.transform(transform.storage);
_paintHighlight(canvas, Point.origin & referenceBox.size, paint);
_paintHighlight(canvas, rect, paint);
canvas.restore();
} else {
_paintHighlight(canvas, originOffset.toPoint() & referenceBox.size, paint);
_paintHighlight(canvas, rect.shift(originOffset), paint);
}
}
......
......@@ -93,6 +93,32 @@ class RenderBlock extends RenderBox
);
return false;
});
assert(() {
switch (mainAxis) {
case Axis.horizontal:
if (!constraints.maxHeight.isInfinite)
return true;
break;
case Axis.vertical:
if (!constraints.maxWidth.isInfinite)
return true;
break;
}
// TODO(ianh): Detect if we're actually nested blocks and say something
// more specific to the exact situation in that case, and don't mention
// nesting blocks in the negative case.
throw new FlutterError(
'RenderBlock must have a bounded constraint for its cross axis.\n'
'RenderBlock forces its children to expand to fit the block\'s container, '
'so it must be placed in a parent that does constrain the block\'s cross '
'axis to a finite dimension. If you are attempting to nest a block with '
'one direction inside a block of another direction, you will want to '
'wrap the inner one inside a box that fixes the dimension in that direction, '
'for example, a RenderIntrinsicWidth or RenderIntrinsicHeight object. '
'This is relatively expensive, however.' // (that's why we don't do it automatically)
);
return false;
});
BoxConstraints innerConstraints = _getInnerConstraints(constraints);
double position = 0.0;
RenderBox child = firstChild;
......
......@@ -609,12 +609,12 @@ abstract class RenderBox extends RenderObject {
/// baseline, regardless of padding, font size differences, etc. If there is
/// no baseline, this function returns the distance from the y-coordinate of
/// the position of the box to the y-coordinate of the bottom of the box
/// (i.e., the height of the box) unless the the caller passes true
/// (i.e., the height of the box) unless the caller passes true
/// for `onlyReal`, in which case the function returns null.
///
/// Only call this function calling [layout] on this box. You are only
/// allowed to call this from the parent of this box during that parent's
/// [performLayout] or [paint] functions.
/// Only call this function after calling [layout] on this box. You
/// are only allowed to call this from the parent of this box during
/// that parent's [performLayout] or [paint] functions.
double getDistanceToBaseline(TextBaseline baseline, { bool onlyReal: false }) {
assert(!needsLayout);
assert(!_debugDoingBaseline);
......@@ -724,6 +724,10 @@ abstract class RenderBox extends RenderObject {
'as big as possible, but it was put inside another render object '
'that allows its children to pick their own size.\n'
'$information'
'The constraints that applied to the $runtimeType were:\n'
' $constraints\n'
'The exact size it was given was:\n'
' $_size\n'
'See https://flutter.io/layout/ for more information.'
);
}
......@@ -788,7 +792,7 @@ abstract class RenderBox extends RenderObject {
// if we have cached data, then someone must have used our data
assert(_ancestorUsesBaseline);
final RenderObject parent = this.parent;
parent.markNeedsLayout();
parent?.markNeedsLayout();
assert(parent == this.parent);
// Now that they're dirty, we can forget that they used the
// baseline. If they use it again, then we'll set the bit
......
......@@ -456,6 +456,7 @@ class RenderIntrinsicWidth extends RenderProxyBox {
if (child == null)
return constraints.constrainWidth(0.0);
double childResult = child.getMaxIntrinsicWidth(constraints);
assert(!childResult.isInfinite);
return constraints.constrainWidth(_applyStep(childResult, _stepWidth));
}
......@@ -465,6 +466,7 @@ class RenderIntrinsicWidth extends RenderProxyBox {
if (child == null)
return constraints.constrainHeight(0.0);
double childResult = child.getMinIntrinsicHeight(_getInnerConstraints(constraints));
assert(!childResult.isInfinite);
return constraints.constrainHeight(_applyStep(childResult, _stepHeight));
}
......@@ -474,6 +476,7 @@ class RenderIntrinsicWidth extends RenderProxyBox {
if (child == null)
return constraints.constrainHeight(0.0);
double childResult = child.getMaxIntrinsicHeight(_getInnerConstraints(constraints));
assert(!childResult.isInfinite);
return constraints.constrainHeight(_applyStep(childResult, _stepHeight));
}
......
......@@ -821,9 +821,16 @@ class RenderCustomSingleChildLayoutBox extends RenderShiftedBox {
}
}
/// Positions its child vertically according to the child's baseline.
/// Shifts the child down such that the child's baseline (or the
/// bottom of the child, if the child has no baseline) is [baseline]
/// logical pixels below the top of this box, then sizes this box to
/// contain the child. If [baseline] is less than the distance from
/// the top of the child to the baseline of the child, then the child
/// is top-aligned instead.
class RenderBaseline extends RenderShiftedBox {
/// Creates a [RenderBaseline] object.
///
/// The [baseline] and [baselineType] arguments are required.
RenderBaseline({
RenderBox child,
double baseline,
......@@ -862,10 +869,13 @@ class RenderBaseline extends RenderShiftedBox {
void performLayout() {
if (child != null) {
child.layout(constraints.loosen(), parentUsesSize: true);
size = constraints.constrain(child.size);
double delta = baseline - child.getDistanceToBaseline(baselineType);
final double childBaseline = child.getDistanceToBaseline(baselineType);
final double actualBaseline = math.max(baseline, childBaseline);
final double top = actualBaseline - childBaseline;
final BoxParentData childParentData = child.parentData;
childParentData.offset = new Offset(0.0, delta);
childParentData.offset = new Offset(0.0, top);
final Size childSize = child.size;
size = constraints.constrain(new Size(childSize.width, top + childSize.height));
} else {
performResize();
}
......
......@@ -957,9 +957,17 @@ class IntrinsicHeight extends SingleChildRenderObjectWidget {
RenderIntrinsicHeight createRenderObject(BuildContext context) => new RenderIntrinsicHeight();
}
/// Positions its child vertically according to the child's baseline.
/// Shifts the child down such that the child's baseline (or the
/// bottom of the child, if the child has no baseline) is [baseline]
/// logical pixels below the top of this box, then sizes this box to
/// contain the child. If [baseline] is less than the distance from
/// the top of the child to the baseline of the child, then the child
/// is top-aligned instead.
class Baseline extends SingleChildRenderObjectWidget {
Baseline({ Key key, this.baseline, this.baselineType: TextBaseline.alphabetic, Widget child })
/// Creates a [Baseline] object.
///
/// The [baseline] and [baselineType] arguments are required.
Baseline({ Key key, this.baseline, this.baselineType, Widget child })
: super(key: key, child: child) {
assert(baseline != null);
assert(baselineType != null);
......
......@@ -5,6 +5,7 @@
import 'dart:collection';
import 'framework.dart';
import 'table.dart';
Key _firstNonUniqueKey(Iterable<Widget> widgets) {
Set<Key> keySet = new HashSet<Key>();
......@@ -18,6 +19,20 @@ Key _firstNonUniqueKey(Iterable<Widget> widgets) {
return null;
}
/// Asserts if the given child list contains any duplicate non-null keys.
///
/// To invoke this function, use the following pattern, typically in the
/// relevant Widget's constructor:
///
/// ```dart
/// assert(!debugChildrenHaveDuplicateKeys(this, children));
/// ```
///
/// For a version of this function that can be used in contexts where
/// the list of items does not have a particular parent, see
/// [debugItemsHaveDuplicateKeys].
///
/// Does nothing if asserts are disabled. Always returns true.
bool debugChildrenHaveDuplicateKeys(Widget parent, Iterable<Widget> children) {
assert(() {
final Key nonUniqueKey = _firstNonUniqueKey(children);
......@@ -33,12 +48,54 @@ bool debugChildrenHaveDuplicateKeys(Widget parent, Iterable<Widget> children) {
return false;
}
/// Asserts if the given list of items contains any duplicate non-null keys.
///
/// To invoke this function, use the following pattern:
///
/// ```dart
/// assert(!debugItemsHaveDuplicateKeys(items));
/// ```
///
/// For a version of this function specifically intended for parents
/// checking their children lists, see [debugChildrenHaveDuplicateKeys].
///
/// Does nothing if asserts are disabled. Always returns true.
bool debugItemsHaveDuplicateKeys(Iterable<Widget> items) {
assert(() {
final Key nonUniqueKey = _firstNonUniqueKey(items);
if (nonUniqueKey != null)
throw new FlutterError('Duplicate key found: $nonUniqueKey.\n');
throw new FlutterError('Duplicate key found: $nonUniqueKey.');
return true;
});
return false;
}
/// Asserts that the given context has a [Table] ancestor.
///
/// Used by [RowInkWell] to make sure that it is only used in an appropriate context.
///
/// To invoke this function, use the following pattern, typically in the
/// relevant Widget's [build] method:
///
/// ```dart
/// assert(debugCheckHasTable(context));
/// ```
///
/// Does nothing if asserts are disabled. Always returns true.
bool debugCheckHasTable(BuildContext context) {
assert(() {
if (context.widget is! Table && context.ancestorWidgetOfExactType(Table) == null) {
Element element = context;
throw new FlutterError(
'No Table widget found.\n'
'${context.widget.runtimeType} widgets require a Table widget ancestor.\n'
'The specific widget that could not find a Table ancestor was:\n'
' ${context.widget}\n'
'The ownership chain for the affected widget is:\n'
' ${element.debugGetCreatorChain(10)}'
);
}
return true;
});
return true;
}
......@@ -1009,6 +1009,7 @@ abstract class Element implements BuildContext {
}
Element inflateWidget(Widget newWidget, dynamic newSlot) {
assert(newWidget != null);
Key key = newWidget.key;
if (key is GlobalKey) {
Element newChild = _retakeInactiveElement(key, newWidget);
......
......@@ -25,6 +25,25 @@ class TableRow {
final LocalKey key;
final Decoration decoration;
final List<Widget> children;
@override
String toString() {
StringBuffer result = new StringBuffer();
result.write('TableRow(');
if (key != null)
result.write('$key, ');
if (decoration != null)
result.write('$decoration, ');
if (children != null) {
result.write('child list is null');
} else if (children.length == 0) {
result.write('no children');
} else {
result.write('$children');
}
result.write(')');
return result.toString();
}
}
class _TableElementRow {
......@@ -54,6 +73,7 @@ class Table extends RenderObjectWidget {
assert(children != null);
assert(defaultColumnWidth != null);
assert(defaultVerticalAlignment != null);
assert(!children.any((TableRow row) => row.children.any((Widget cell) => cell == null)));
assert(() {
List<Widget> flatChildren = children.expand((TableRow row) => row.children).toList(growable: false);
return !debugChildrenHaveDuplicateKeys(this, flatChildren);
......@@ -125,7 +145,10 @@ class _TableElement extends RenderObjectElement {
_children = widget.children.map((TableRow row) {
return new _TableElementRow(
key: row.key,
children: row.children.map((Widget child) => inflateWidget(child, null)).toList(growable: false)
children: row.children.map/*<Element>*/((Widget child) {
assert(child != null);
return inflateWidget(child, null);
}).toList(growable: false)
);
}).toList(growable: false);
assert(() { _debugWillReattachChildren = false; return true; });
......
......@@ -26,7 +26,7 @@ void main() {
RenderTable table;
layout(new RenderPositionedBox(child: table = new RenderTable()));
expect(table.size, equals(const Size(800.0, 0.0)));
expect(table.size, equals(const Size(0.0, 0.0)));
});
test('Table test: combinations', () {
......@@ -39,13 +39,13 @@ void main() {
textBaseline: TextBaseline.alphabetic
)));
expect(table.size, equals(const Size(800.0, 0.0)));
expect(table.size, equals(const Size(0.0, 0.0)));
table.setChild(2, 4, sizedBox(100.0, 200.0));
pumpFrame();
expect(table.size, equals(new Size(800.0, 200.0)));
expect(table.size, equals(new Size(100.0, 200.0)));
table.setChild(0, 0, sizedBox(10.0, 30.0));
table.setChild(1, 0, sizedBox(20.0, 20.0));
......@@ -53,7 +53,7 @@ void main() {
pumpFrame();
expect(table.size, equals(new Size(800.0, 230.0)));
expect(table.size, equals(new Size(130.0, 230.0)));
});
test('Table test: removing cells', () {
......
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