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'; ...@@ -13,6 +13,7 @@ import '../demo/buttons_demo.dart';
import '../demo/cards_demo.dart'; import '../demo/cards_demo.dart';
import '../demo/colors_demo.dart'; import '../demo/colors_demo.dart';
import '../demo/chip_demo.dart'; import '../demo/chip_demo.dart';
import '../demo/data_table_demo.dart';
import '../demo/date_picker_demo.dart'; import '../demo/date_picker_demo.dart';
import '../demo/dialog_demo.dart'; import '../demo/dialog_demo.dart';
import '../demo/drop_down_demo.dart'; import '../demo/drop_down_demo.dart';
...@@ -122,6 +123,7 @@ class GalleryHomeState extends State<GalleryHome> { ...@@ -122,6 +123,7 @@ class GalleryHomeState extends State<GalleryHome> {
new GalleryItem(title: 'Cards', builder: () => new CardsDemo()), new GalleryItem(title: 'Cards', builder: () => new CardsDemo()),
new GalleryItem(title: 'Chips', builder: () => new ChipDemo()), new GalleryItem(title: 'Chips', builder: () => new ChipDemo()),
new GalleryItem(title: 'Date picker', builder: () => new DatePickerDemo()), 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: 'Dialog', builder: () => new DialogDemo()),
new GalleryItem(title: 'Drop-down button', builder: () => new DropDownDemo()), new GalleryItem(title: 'Drop-down button', builder: () => new DropDownDemo()),
new GalleryItem(title: 'Expand/collapse list control', builder: () => new TwoLevelListDemo()), new GalleryItem(title: 'Expand/collapse list control', builder: () => new TwoLevelListDemo()),
......
...@@ -19,13 +19,14 @@ export 'src/material/chip.dart'; ...@@ -19,13 +19,14 @@ export 'src/material/chip.dart';
export 'src/material/circle_avatar.dart'; export 'src/material/circle_avatar.dart';
export 'src/material/colors.dart'; export 'src/material/colors.dart';
export 'src/material/constants.dart'; export 'src/material/constants.dart';
export 'src/material/data_table.dart';
export 'src/material/date_picker.dart'; export 'src/material/date_picker.dart';
export 'src/material/date_picker_dialog.dart'; export 'src/material/date_picker_dialog.dart';
export 'src/material/dialog.dart'; export 'src/material/dialog.dart';
export 'src/material/divider.dart';
export 'src/material/drawer.dart'; export 'src/material/drawer.dart';
export 'src/material/drawer_header.dart'; export 'src/material/drawer_header.dart';
export 'src/material/drawer_item.dart'; export 'src/material/drawer_item.dart';
export 'src/material/divider.dart';
export 'src/material/drop_down.dart'; export 'src/material/drop_down.dart';
export 'src/material/flat_button.dart'; export 'src/material/flat_button.dart';
export 'src/material/flexible_space_bar.dart'; export 'src/material/flexible_space_bar.dart';
...@@ -33,10 +34,10 @@ export 'src/material/floating_action_button.dart'; ...@@ -33,10 +34,10 @@ export 'src/material/floating_action_button.dart';
export 'src/material/grid_tile.dart'; export 'src/material/grid_tile.dart';
export 'src/material/grid_tile_bar.dart'; export 'src/material/grid_tile_bar.dart';
export 'src/material/icon.dart'; export 'src/material/icon.dart';
export 'src/material/icons.dart';
export 'src/material/icon_button.dart'; export 'src/material/icon_button.dart';
export 'src/material/icon_theme.dart'; export 'src/material/icon_theme.dart';
export 'src/material/icon_theme_data.dart'; export 'src/material/icon_theme_data.dart';
export 'src/material/icons.dart';
export 'src/material/ink_well.dart'; export 'src/material/ink_well.dart';
export 'src/material/input.dart'; export 'src/material/input.dart';
export 'src/material/list.dart'; export 'src/material/list.dart';
......
...@@ -64,6 +64,9 @@ class Checkbox extends StatelessWidget { ...@@ -64,6 +64,9 @@ class Checkbox extends StatelessWidget {
/// If null, the checkbox will be displayed as disabled. /// If null, the checkbox will be displayed as disabled.
final ValueChanged<bool> onChanged; final ValueChanged<bool> onChanged;
/// The width of a checkbox widget.
static const double width = 18.0;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
...@@ -114,10 +117,9 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -114,10 +117,9 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
} }
const double _kMidpoint = 0.5; const double _kMidpoint = 0.5;
const double _kEdgeSize = 18.0; const double _kEdgeSize = Checkbox.width;
const double _kEdgeRadius = 1.0; const double _kEdgeRadius = 1.0;
const double _kStrokeWidth = 2.0; const double _kStrokeWidth = 2.0;
const double _kOffset = kRadialReactionRadius - _kEdgeSize / 2.0;
class _RenderCheckbox extends RenderToggleable { class _RenderCheckbox extends RenderToggleable {
_RenderCheckbox({ _RenderCheckbox({
...@@ -135,11 +137,13 @@ class _RenderCheckbox extends RenderToggleable { ...@@ -135,11 +137,13 @@ class _RenderCheckbox extends RenderToggleable {
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas; 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; double t = position.value;
......
...@@ -12,6 +12,7 @@ class Colors { ...@@ -12,6 +12,7 @@ class Colors {
/// Completely invisible. /// Completely invisible.
static const Color transparent = const Color(0x00000000); static const Color transparent = const Color(0x00000000);
/// Completely opaque black. /// Completely opaque black.
static const Color black = const Color(0xFF000000); static const Color black = const Color(0xFF000000);
...@@ -21,6 +22,11 @@ class Colors { ...@@ -21,6 +22,11 @@ class Colors {
/// Black with 54% opacity. /// Black with 54% opacity.
static const Color black54 = const Color(0x8A000000); 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. /// Black with 45% opacity.
/// ///
/// Used for modal barriers. /// Used for modal barriers.
...@@ -28,14 +34,15 @@ class Colors { ...@@ -28,14 +34,15 @@ class Colors {
/// Black with 26% opacity. /// 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); static const Color black26 = const Color(0x42000000);
/// Black with 12% opacity. /// 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); static const Color black12 = const Color(0x1F000000);
/// Completely opaque white. /// Completely opaque white.
static const Color white = const Color(0xFFFFFFFF); static const Color white = const Color(0xFFFFFFFF);
...@@ -44,17 +51,18 @@ class Colors { ...@@ -44,17 +51,18 @@ class Colors {
/// White with 32% opacity. /// 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); static const Color white30 = const Color(0x4DFFFFFF);
/// White with 12% opacity. /// 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); static const Color white12 = const Color(0x1FFFFFFF);
/// White with 10% opacity. /// White with 10% opacity.
static const Color white10 = const Color(0x1AFFFFFF); static const Color white10 = const Color(0x1AFFFFFF);
/// The red primary swatch. /// The red primary swatch.
static const Map<int, Color> red = const <int, Color>{ static const Map<int, Color> red = const <int, Color>{
50: const Color(0xFFFFEBEE), 50: const Color(0xFFFFEBEE),
......
This diff is collapsed.
...@@ -7,9 +7,19 @@ import 'package:flutter/widgets.dart'; ...@@ -7,9 +7,19 @@ import 'package:flutter/widgets.dart';
import 'material.dart'; import 'material.dart';
import 'scaffold.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) { bool debugCheckHasMaterial(BuildContext context) {
assert(() { assert(() {
if (context.widget is! Material && context.ancestorWidgetOfExactType(Material) == null) { if (context.widget is! Material && context.ancestorWidgetOfExactType(Material) == null) {
...@@ -33,9 +43,22 @@ bool debugCheckHasMaterial(BuildContext context) { ...@@ -33,9 +43,22 @@ bool debugCheckHasMaterial(BuildContext context) {
return true; 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) { bool debugCheckHasScaffold(BuildContext context) {
assert(() { assert(() {
if (Scaffold.of(context) == null) { if (Scaffold.of(context) == null) {
......
...@@ -15,11 +15,13 @@ import 'theme.dart'; ...@@ -15,11 +15,13 @@ import 'theme.dart';
import 'material.dart'; import 'material.dart';
const Duration _kDropDownMenuDuration = const Duration(milliseconds: 300); const Duration _kDropDownMenuDuration = const Duration(milliseconds: 300);
const double _kTopMargin = 6.0;
const double _kMenuItemHeight = 48.0; const double _kMenuItemHeight = 48.0;
const EdgeInsets _kMenuVerticalPadding = const EdgeInsets.symmetric(vertical: 8.0); const EdgeInsets _kMenuVerticalPadding = const EdgeInsets.symmetric(vertical: 8.0);
const EdgeInsets _kMenuHorizontalPadding = const EdgeInsets.symmetric(horizontal: 36.0); const EdgeInsets _kMenuHorizontalPadding = const EdgeInsets.symmetric(horizontal: 36.0);
const double _kBaselineOffsetFromBottom = 20.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 { class _DropDownMenuPainter extends CustomPainter {
const _DropDownMenuPainter({ const _DropDownMenuPainter({
...@@ -235,7 +237,7 @@ class DropDownMenuItem<T> extends StatelessWidget { ...@@ -235,7 +237,7 @@ class DropDownMenuItem<T> extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Container( return new Container(
height: _kMenuItemHeight, 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( child: new DefaultTextStyle(
style: Theme.of(context).textTheme.subhead, style: Theme.of(context).textTheme.subhead,
child: new Baseline( child: new Baseline(
...@@ -247,6 +249,32 @@ class DropDownMenuItem<T> extends StatelessWidget { ...@@ -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 material design button for selecting from a list of items.
/// ///
/// A dropdown button lets the user select from a number of items. The button /// A dropdown button lets the user select from a number of items. The button
...@@ -336,26 +364,35 @@ class _DropDownButtonState<T> extends State<DropDownButton<T>> { ...@@ -336,26 +364,35 @@ class _DropDownButtonState<T> extends State<DropDownButton<T>> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
Widget result = new Row(
children: <Widget>[
new IndexedStack(
children: config.items,
key: indexedStackKey,
index: _selectedIndex,
alignment: FractionalOffset.topCenter
),
new Container(
child: new Icon(icon: Icons.arrow_drop_down, size: 36.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( return new GestureDetector(
onTap: _handleTap, onTap: _handleTap,
child: new Container( child: result
decoration: new BoxDecoration(border: _kDropDownUnderline),
child: new Row(
children: <Widget>[
new IndexedStack(
children: config.items,
key: indexedStackKey,
index: _selectedIndex,
alignment: FractionalOffset.topCenter
),
new Container(
child: new Icon(icon: Icons.arrow_drop_down, size: 36.0),
padding: const EdgeInsets.only(top: 6.0)
)
],
mainAxisAlignment: MainAxisAlignment.collapse
)
)
); );
} }
} }
...@@ -46,7 +46,7 @@ class Icon extends StatelessWidget { ...@@ -46,7 +46,7 @@ class Icon extends StatelessWidget {
/// Icons occupy a square with width and height equal to size. /// Icons occupy a square with width and height equal to size.
final double size; final double size;
/// The icon to display. /// The icon to display. The available icons are described in [Icons].
final IconData icon; final IconData icon;
/// The color to use when drawing the icon. /// The color to use when drawing the icon.
......
...@@ -64,6 +64,31 @@ class InkResponse extends StatefulWidget { ...@@ -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. /// The shape (e.g., circle, rectangle) to use for the highlight drawn around this part of the material.
final BoxShape highlightShape; 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 @override
_InkResponseState<InkResponse> createState() => new _InkResponseState<InkResponse>(); _InkResponseState<InkResponse> createState() => new _InkResponseState<InkResponse>();
} }
...@@ -85,6 +110,7 @@ class _InkResponseState<T extends InkResponse> extends State<T> { ...@@ -85,6 +110,7 @@ class _InkResponseState<T extends InkResponse> extends State<T> {
referenceBox: referenceBox, referenceBox: referenceBox,
color: Theme.of(context).highlightColor, color: Theme.of(context).highlightColor,
shape: config.highlightShape, shape: config.highlightShape,
rectCallback: config.getRectCallback(referenceBox),
onRemoved: () { onRemoved: () {
assert(_lastHighlight != null); assert(_lastHighlight != null);
_lastHighlight = null; _lastHighlight = null;
...@@ -105,11 +131,13 @@ class _InkResponseState<T extends InkResponse> extends State<T> { ...@@ -105,11 +131,13 @@ class _InkResponseState<T extends InkResponse> extends State<T> {
RenderBox referenceBox = context.findRenderObject(); RenderBox referenceBox = context.findRenderObject();
assert(Material.of(context) != null); assert(Material.of(context) != null);
InkSplash splash; InkSplash splash;
RectCallback rectCallback = config.getRectCallback(referenceBox);
splash = Material.of(context).splashAt( splash = Material.of(context).splashAt(
referenceBox: referenceBox, referenceBox: referenceBox,
position: referenceBox.globalToLocal(position), position: referenceBox.globalToLocal(position),
color: Theme.of(context).splashColor, color: Theme.of(context).splashColor,
containedInWell: config.containedInkWell, containedInkWell: config.containedInkWell,
rectCallback: config.containedInkWell ? rectCallback : null,
onRemoved: () { onRemoved: () {
if (_splashes != null) { if (_splashes != null) {
assert(_splashes.contains(splash)); assert(_splashes.contains(splash));
...@@ -176,7 +204,7 @@ class _InkResponseState<T extends InkResponse> extends State<T> { ...@@ -176,7 +204,7 @@ class _InkResponseState<T extends InkResponse> extends State<T> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context)); assert(config.debugCheckContext(context));
final bool enabled = config.onTap != null || config.onDoubleTap != null || config.onLongPress != null; final bool enabled = config.onTap != null || config.onDoubleTap != null || config.onLongPress != null;
return new GestureDetector( return new GestureDetector(
onTapDown: enabled ? _handleTapDown : null, onTapDown: enabled ? _handleTapDown : null,
......
...@@ -12,10 +12,15 @@ import 'constants.dart'; ...@@ -12,10 +12,15 @@ import 'constants.dart';
import 'shadows.dart'; import 'shadows.dart';
import 'theme.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: /// See also:
/// * [Material] ///
/// * [Material], in particular [Material.type]
/// * [kMaterialEdges] /// * [kMaterialEdges]
enum MaterialType { enum MaterialType {
/// Infinite extent using default theme canvas color. /// Infinite extent using default theme canvas color.
...@@ -27,7 +32,7 @@ enum MaterialType { ...@@ -27,7 +32,7 @@ enum MaterialType {
/// A circle, no color by default (used for floating action buttons). /// A circle, no color by default (used for floating action buttons).
circle, circle,
/// Rounded edges, no color by default (used for MaterialButton buttons). /// Rounded edges, no color by default (used for [MaterialButton] buttons).
button, button,
/// A transparent piece of material that draws ink splashes and highlights. /// A transparent piece of material that draws ink splashes and highlights.
...@@ -95,13 +100,36 @@ abstract class MaterialInkController { ...@@ -95,13 +100,36 @@ abstract class MaterialInkController {
Color get color; Color get color;
/// Begin a splash, centered at position relative to referenceBox. /// 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. /// 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. /// Begin a highlight animation. If a rectCallback is given, then it
InkHighlight highlightAt({ RenderBox referenceBox, Color color, BoxShape shape: BoxShape.rectangle, VoidCallback onRemoved }); /// 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. /// Add an arbitrary InkFeature to this InkController.
void addInkFeature(InkFeature feature); void addInkFeature(InkFeature feature);
...@@ -147,22 +175,27 @@ class Material extends StatefulWidget { ...@@ -147,22 +175,27 @@ class Material extends StatefulWidget {
/// The widget below this widget in the tree. /// The widget below this widget in the tree.
final Widget child; 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; final MaterialType type;
/// The z-coordinate at which to place this material. /// The z-coordinate at which to place this material.
final int elevation; 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 /// Must be opaque. To create a transparent piece of material, use
/// [MaterialType.transparency]. /// [MaterialType.transparency].
///
/// By default, the color is derived from the [type] of material.
final Color color; final Color color;
/// The typographical style to use for text within this material. /// The typographical style to use for text within this material.
final TextStyle textStyle; 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) { static MaterialInkController of(BuildContext context) {
final _RenderInkFeatures result = context.ancestorRenderObjectOfType(const TypeMatcher<_RenderInkFeatures>()); final _RenderInkFeatures result = context.ancestorRenderObjectOfType(const TypeMatcher<_RenderInkFeatures>());
return result; return result;
...@@ -273,13 +306,24 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController ...@@ -273,13 +306,24 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
RenderBox referenceBox, RenderBox referenceBox,
Point position, Point position,
Color color, Color color,
bool containedInWell, bool containedInkWell: false,
RectCallback rectCallback,
VoidCallback onRemoved VoidCallback onRemoved
}) { }) {
double radius; double radius;
if (containedInWell) { RectCallback clipCallback;
radius = _getSplashTargetSize(referenceBox.size, position); 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 { } else {
assert(rectCallback == null);
radius = _kDefaultSplashRadius; radius = _kDefaultSplashRadius;
} }
_InkSplash splash = new _InkSplash( _InkSplash splash = new _InkSplash(
...@@ -288,8 +332,8 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController ...@@ -288,8 +332,8 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
position: position, position: position,
color: color, color: color,
targetRadius: radius, targetRadius: radius,
clipToReferenceBox: containedInWell, clipCallback: clipCallback,
repositionToReferenceBox: !containedInWell, repositionToReferenceBox: !containedInkWell,
onRemoved: onRemoved onRemoved: onRemoved
); );
addInkFeature(splash); addInkFeature(splash);
...@@ -309,6 +353,7 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController ...@@ -309,6 +353,7 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
RenderBox referenceBox, RenderBox referenceBox,
Color color, Color color,
BoxShape shape: BoxShape.rectangle, BoxShape shape: BoxShape.rectangle,
RectCallback rectCallback,
VoidCallback onRemoved VoidCallback onRemoved
}) { }) {
_InkHighlight highlight = new _InkHighlight( _InkHighlight highlight = new _InkHighlight(
...@@ -316,6 +361,7 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController ...@@ -316,6 +361,7 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
referenceBox: referenceBox, referenceBox: referenceBox,
color: color, color: color,
shape: shape, shape: shape,
rectCallback: rectCallback,
onRemoved: onRemoved onRemoved: onRemoved
); );
addInkFeature(highlight); addInkFeature(highlight);
...@@ -436,7 +482,7 @@ class _InkSplash extends InkFeature implements InkSplash { ...@@ -436,7 +482,7 @@ class _InkSplash extends InkFeature implements InkSplash {
this.position, this.position,
this.color, this.color,
this.targetRadius, this.targetRadius,
this.clipToReferenceBox, this.clipCallback,
this.repositionToReferenceBox, this.repositionToReferenceBox,
VoidCallback onRemoved VoidCallback onRemoved
}) : super(renderer: renderer, referenceBox: referenceBox, onRemoved: onRemoved) { }) : super(renderer: renderer, referenceBox: referenceBox, onRemoved: onRemoved) {
...@@ -460,7 +506,7 @@ class _InkSplash extends InkFeature implements InkSplash { ...@@ -460,7 +506,7 @@ class _InkSplash extends InkFeature implements InkSplash {
final Point position; final Point position;
final Color color; final Color color;
final double targetRadius; final double targetRadius;
final bool clipToReferenceBox; final RectCallback clipCallback;
final bool repositionToReferenceBox; final bool repositionToReferenceBox;
Animation<double> _radius; Animation<double> _radius;
...@@ -499,25 +545,23 @@ class _InkSplash extends InkFeature implements InkSplash { ...@@ -499,25 +545,23 @@ class _InkSplash extends InkFeature implements InkSplash {
void paintFeature(Canvas canvas, Matrix4 transform) { void paintFeature(Canvas canvas, Matrix4 transform) {
Paint paint = new Paint()..color = color.withAlpha(_alpha.value); Paint paint = new Paint()..color = color.withAlpha(_alpha.value);
Point center = position; Point center = position;
if (repositionToReferenceBox)
center = Point.lerp(center, referenceBox.size.center(Point.origin), _radiusController.value);
Offset originOffset = MatrixUtils.getAsTranslation(transform); Offset originOffset = MatrixUtils.getAsTranslation(transform);
if (originOffset == null) { if (originOffset == null) {
canvas.save(); canvas.save();
canvas.transform(transform.storage); canvas.transform(transform.storage);
if (clipToReferenceBox) if (clipCallback != null)
canvas.clipRect(Point.origin & referenceBox.size); canvas.clipRect(clipCallback());
if (repositionToReferenceBox)
center = Point.lerp(center, Point.origin, _radiusController.value);
canvas.drawCircle(center, _radius.value, paint); canvas.drawCircle(center, _radius.value, paint);
canvas.restore(); canvas.restore();
} else { } else {
if (clipToReferenceBox) { if (clipCallback != null) {
canvas.save(); 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); canvas.drawCircle(center + originOffset, _radius.value, paint);
if (clipToReferenceBox) if (clipCallback != null)
canvas.restore(); canvas.restore();
} }
} }
...@@ -527,6 +571,7 @@ class _InkHighlight extends InkFeature implements InkHighlight { ...@@ -527,6 +571,7 @@ class _InkHighlight extends InkFeature implements InkHighlight {
_InkHighlight({ _InkHighlight({
_RenderInkFeatures renderer, _RenderInkFeatures renderer,
RenderBox referenceBox, RenderBox referenceBox,
this.rectCallback,
Color color, Color color,
this.shape, this.shape,
VoidCallback onRemoved VoidCallback onRemoved
...@@ -542,6 +587,8 @@ class _InkHighlight extends InkFeature implements InkHighlight { ...@@ -542,6 +587,8 @@ class _InkHighlight extends InkFeature implements InkHighlight {
).animate(_alphaController); ).animate(_alphaController);
} }
final RectCallback rectCallback;
@override @override
Color get color => _color; Color get color => _color;
Color _color; Color _color;
...@@ -597,13 +644,14 @@ class _InkHighlight extends InkFeature implements InkHighlight { ...@@ -597,13 +644,14 @@ class _InkHighlight extends InkFeature implements InkHighlight {
void paintFeature(Canvas canvas, Matrix4 transform) { void paintFeature(Canvas canvas, Matrix4 transform) {
Paint paint = new Paint()..color = color.withAlpha(_alpha.value); Paint paint = new Paint()..color = color.withAlpha(_alpha.value);
Offset originOffset = MatrixUtils.getAsTranslation(transform); Offset originOffset = MatrixUtils.getAsTranslation(transform);
final Rect rect = (rectCallback != null ? rectCallback() : Point.origin & referenceBox.size);
if (originOffset == null) { if (originOffset == null) {
canvas.save(); canvas.save();
canvas.transform(transform.storage); canvas.transform(transform.storage);
_paintHighlight(canvas, Point.origin & referenceBox.size, paint); _paintHighlight(canvas, rect, paint);
canvas.restore(); canvas.restore();
} else { } else {
_paintHighlight(canvas, originOffset.toPoint() & referenceBox.size, paint); _paintHighlight(canvas, rect.shift(originOffset), paint);
} }
} }
......
...@@ -93,6 +93,32 @@ class RenderBlock extends RenderBox ...@@ -93,6 +93,32 @@ class RenderBlock extends RenderBox
); );
return false; 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); BoxConstraints innerConstraints = _getInnerConstraints(constraints);
double position = 0.0; double position = 0.0;
RenderBox child = firstChild; RenderBox child = firstChild;
......
...@@ -609,12 +609,12 @@ abstract class RenderBox extends RenderObject { ...@@ -609,12 +609,12 @@ abstract class RenderBox extends RenderObject {
/// baseline, regardless of padding, font size differences, etc. If there is /// baseline, regardless of padding, font size differences, etc. If there is
/// no baseline, this function returns the distance from the y-coordinate of /// 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 /// 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. /// for `onlyReal`, in which case the function returns null.
/// ///
/// Only call this function calling [layout] on this box. You are only /// Only call this function after calling [layout] on this box. You
/// allowed to call this from the parent of this box during that parent's /// are only allowed to call this from the parent of this box during
/// [performLayout] or [paint] functions. /// that parent's [performLayout] or [paint] functions.
double getDistanceToBaseline(TextBaseline baseline, { bool onlyReal: false }) { double getDistanceToBaseline(TextBaseline baseline, { bool onlyReal: false }) {
assert(!needsLayout); assert(!needsLayout);
assert(!_debugDoingBaseline); assert(!_debugDoingBaseline);
...@@ -724,6 +724,10 @@ abstract class RenderBox extends RenderObject { ...@@ -724,6 +724,10 @@ abstract class RenderBox extends RenderObject {
'as big as possible, but it was put inside another render object ' 'as big as possible, but it was put inside another render object '
'that allows its children to pick their own size.\n' 'that allows its children to pick their own size.\n'
'$information' '$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.' 'See https://flutter.io/layout/ for more information.'
); );
} }
...@@ -788,7 +792,7 @@ abstract class RenderBox extends RenderObject { ...@@ -788,7 +792,7 @@ abstract class RenderBox extends RenderObject {
// if we have cached data, then someone must have used our data // if we have cached data, then someone must have used our data
assert(_ancestorUsesBaseline); assert(_ancestorUsesBaseline);
final RenderObject parent = this.parent; final RenderObject parent = this.parent;
parent.markNeedsLayout(); parent?.markNeedsLayout();
assert(parent == this.parent); assert(parent == this.parent);
// Now that they're dirty, we can forget that they used the // Now that they're dirty, we can forget that they used the
// baseline. If they use it again, then we'll set the bit // baseline. If they use it again, then we'll set the bit
......
...@@ -456,6 +456,7 @@ class RenderIntrinsicWidth extends RenderProxyBox { ...@@ -456,6 +456,7 @@ class RenderIntrinsicWidth extends RenderProxyBox {
if (child == null) if (child == null)
return constraints.constrainWidth(0.0); return constraints.constrainWidth(0.0);
double childResult = child.getMaxIntrinsicWidth(constraints); double childResult = child.getMaxIntrinsicWidth(constraints);
assert(!childResult.isInfinite);
return constraints.constrainWidth(_applyStep(childResult, _stepWidth)); return constraints.constrainWidth(_applyStep(childResult, _stepWidth));
} }
...@@ -465,6 +466,7 @@ class RenderIntrinsicWidth extends RenderProxyBox { ...@@ -465,6 +466,7 @@ class RenderIntrinsicWidth extends RenderProxyBox {
if (child == null) if (child == null)
return constraints.constrainHeight(0.0); return constraints.constrainHeight(0.0);
double childResult = child.getMinIntrinsicHeight(_getInnerConstraints(constraints)); double childResult = child.getMinIntrinsicHeight(_getInnerConstraints(constraints));
assert(!childResult.isInfinite);
return constraints.constrainHeight(_applyStep(childResult, _stepHeight)); return constraints.constrainHeight(_applyStep(childResult, _stepHeight));
} }
...@@ -474,6 +476,7 @@ class RenderIntrinsicWidth extends RenderProxyBox { ...@@ -474,6 +476,7 @@ class RenderIntrinsicWidth extends RenderProxyBox {
if (child == null) if (child == null)
return constraints.constrainHeight(0.0); return constraints.constrainHeight(0.0);
double childResult = child.getMaxIntrinsicHeight(_getInnerConstraints(constraints)); double childResult = child.getMaxIntrinsicHeight(_getInnerConstraints(constraints));
assert(!childResult.isInfinite);
return constraints.constrainHeight(_applyStep(childResult, _stepHeight)); return constraints.constrainHeight(_applyStep(childResult, _stepHeight));
} }
......
...@@ -821,9 +821,16 @@ class RenderCustomSingleChildLayoutBox extends RenderShiftedBox { ...@@ -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 { class RenderBaseline extends RenderShiftedBox {
/// Creates a [RenderBaseline] object.
///
/// The [baseline] and [baselineType] arguments are required.
RenderBaseline({ RenderBaseline({
RenderBox child, RenderBox child,
double baseline, double baseline,
...@@ -862,10 +869,13 @@ class RenderBaseline extends RenderShiftedBox { ...@@ -862,10 +869,13 @@ class RenderBaseline extends RenderShiftedBox {
void performLayout() { void performLayout() {
if (child != null) { if (child != null) {
child.layout(constraints.loosen(), parentUsesSize: true); child.layout(constraints.loosen(), parentUsesSize: true);
size = constraints.constrain(child.size); final double childBaseline = child.getDistanceToBaseline(baselineType);
double delta = baseline - child.getDistanceToBaseline(baselineType); final double actualBaseline = math.max(baseline, childBaseline);
final double top = actualBaseline - childBaseline;
final BoxParentData childParentData = child.parentData; 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 { } else {
performResize(); performResize();
} }
......
...@@ -957,9 +957,17 @@ class IntrinsicHeight extends SingleChildRenderObjectWidget { ...@@ -957,9 +957,17 @@ class IntrinsicHeight extends SingleChildRenderObjectWidget {
RenderIntrinsicHeight createRenderObject(BuildContext context) => new RenderIntrinsicHeight(); 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 { 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) { : super(key: key, child: child) {
assert(baseline != null); assert(baseline != null);
assert(baselineType != null); assert(baselineType != null);
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'dart:collection'; import 'dart:collection';
import 'framework.dart'; import 'framework.dart';
import 'table.dart';
Key _firstNonUniqueKey(Iterable<Widget> widgets) { Key _firstNonUniqueKey(Iterable<Widget> widgets) {
Set<Key> keySet = new HashSet<Key>(); Set<Key> keySet = new HashSet<Key>();
...@@ -18,6 +19,20 @@ Key _firstNonUniqueKey(Iterable<Widget> widgets) { ...@@ -18,6 +19,20 @@ Key _firstNonUniqueKey(Iterable<Widget> widgets) {
return null; 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) { bool debugChildrenHaveDuplicateKeys(Widget parent, Iterable<Widget> children) {
assert(() { assert(() {
final Key nonUniqueKey = _firstNonUniqueKey(children); final Key nonUniqueKey = _firstNonUniqueKey(children);
...@@ -33,12 +48,54 @@ bool debugChildrenHaveDuplicateKeys(Widget parent, Iterable<Widget> children) { ...@@ -33,12 +48,54 @@ bool debugChildrenHaveDuplicateKeys(Widget parent, Iterable<Widget> children) {
return false; 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) { bool debugItemsHaveDuplicateKeys(Iterable<Widget> items) {
assert(() { assert(() {
final Key nonUniqueKey = _firstNonUniqueKey(items); final Key nonUniqueKey = _firstNonUniqueKey(items);
if (nonUniqueKey != null) if (nonUniqueKey != null)
throw new FlutterError('Duplicate key found: $nonUniqueKey.\n'); throw new FlutterError('Duplicate key found: $nonUniqueKey.');
return true; return true;
}); });
return false; 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 { ...@@ -1009,6 +1009,7 @@ abstract class Element implements BuildContext {
} }
Element inflateWidget(Widget newWidget, dynamic newSlot) { Element inflateWidget(Widget newWidget, dynamic newSlot) {
assert(newWidget != null);
Key key = newWidget.key; Key key = newWidget.key;
if (key is GlobalKey) { if (key is GlobalKey) {
Element newChild = _retakeInactiveElement(key, newWidget); Element newChild = _retakeInactiveElement(key, newWidget);
......
...@@ -25,6 +25,25 @@ class TableRow { ...@@ -25,6 +25,25 @@ class TableRow {
final LocalKey key; final LocalKey key;
final Decoration decoration; final Decoration decoration;
final List<Widget> children; 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 { class _TableElementRow {
...@@ -54,6 +73,7 @@ class Table extends RenderObjectWidget { ...@@ -54,6 +73,7 @@ class Table extends RenderObjectWidget {
assert(children != null); assert(children != null);
assert(defaultColumnWidth != null); assert(defaultColumnWidth != null);
assert(defaultVerticalAlignment != null); assert(defaultVerticalAlignment != null);
assert(!children.any((TableRow row) => row.children.any((Widget cell) => cell == null)));
assert(() { assert(() {
List<Widget> flatChildren = children.expand((TableRow row) => row.children).toList(growable: false); List<Widget> flatChildren = children.expand((TableRow row) => row.children).toList(growable: false);
return !debugChildrenHaveDuplicateKeys(this, flatChildren); return !debugChildrenHaveDuplicateKeys(this, flatChildren);
...@@ -125,7 +145,10 @@ class _TableElement extends RenderObjectElement { ...@@ -125,7 +145,10 @@ class _TableElement extends RenderObjectElement {
_children = widget.children.map((TableRow row) { _children = widget.children.map((TableRow row) {
return new _TableElementRow( return new _TableElementRow(
key: row.key, 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); }).toList(growable: false);
assert(() { _debugWillReattachChildren = false; return true; }); assert(() { _debugWillReattachChildren = false; return true; });
......
...@@ -26,7 +26,7 @@ void main() { ...@@ -26,7 +26,7 @@ void main() {
RenderTable table; RenderTable table;
layout(new RenderPositionedBox(child: table = new RenderTable())); 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', () { test('Table test: combinations', () {
...@@ -39,13 +39,13 @@ void main() { ...@@ -39,13 +39,13 @@ void main() {
textBaseline: TextBaseline.alphabetic 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)); table.setChild(2, 4, sizedBox(100.0, 200.0));
pumpFrame(); 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(0, 0, sizedBox(10.0, 30.0));
table.setChild(1, 0, sizedBox(20.0, 20.0)); table.setChild(1, 0, sizedBox(20.0, 20.0));
...@@ -53,7 +53,7 @@ void main() { ...@@ -53,7 +53,7 @@ void main() {
pumpFrame(); 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', () { 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