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.
This commit is contained in:
parent
db2f66aab1
commit
a91bc0ba9c
145
examples/material_gallery/lib/demo/data_table_demo.dart
Normal file
145
examples/material_gallery/lib/demo/data_table_demo.dart
Normal file
@ -0,0 +1,145 @@
|
||||
// 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 Canvas canvas = context.canvas;
|
||||
|
||||
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),
|
||||
|
766
packages/flutter/lib/src/material/data_table.dart
Normal file
766
packages/flutter/lib/src/material/data_table.dart
Normal file
@ -0,0 +1,766 @@
|
||||
// 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 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'checkbox.dart';
|
||||
import 'colors.dart';
|
||||
import 'debug.dart';
|
||||
import 'drop_down.dart';
|
||||
import 'icon.dart';
|
||||
import 'icon_theme.dart';
|
||||
import 'icon_theme_data.dart';
|
||||
import 'icons.dart';
|
||||
import 'ink_well.dart';
|
||||
import 'material.dart';
|
||||
import 'theme.dart';
|
||||
import 'theme_data.dart';
|
||||
import 'tooltip.dart';
|
||||
|
||||
typedef void DataColumnSortCallback(int columnIndex, bool ascending);
|
||||
|
||||
typedef void DataCellEditCallback(Rect cell);
|
||||
|
||||
/// Column configuration for a [DataTable].
|
||||
///
|
||||
/// One column configuration must be provided for each column to
|
||||
/// display in the table. The list of [DataColumn] objects is passed
|
||||
/// as the `columns` argument to the [new DataTable] constructor.
|
||||
class DataColumn {
|
||||
/// Creates the configuration for a column of a [DataTable].
|
||||
///
|
||||
/// The [label] argument must not be null.
|
||||
const DataColumn({
|
||||
this.label,
|
||||
this.tooltip,
|
||||
this.numeric: false,
|
||||
this.onSort
|
||||
});
|
||||
|
||||
/// The column heading.
|
||||
///
|
||||
/// Typically, this will be a [Text] widget. It could also be an
|
||||
/// [Icon] (typically using size 18), or a [Row] with an icon and
|
||||
/// some text.
|
||||
///
|
||||
/// The label should not include the sort indicator.
|
||||
final Widget label;
|
||||
|
||||
/// The column heading's tooltip.
|
||||
///
|
||||
/// This is a longer description of the column heading, for cases
|
||||
/// where the heading might have been abbreviated to keep the column
|
||||
/// width to a reasonable size.
|
||||
final String tooltip;
|
||||
|
||||
/// Whether this column represents numeric data or not.
|
||||
///
|
||||
/// The contents of cells of columns containing numeric data are
|
||||
/// right-aligned.
|
||||
final bool numeric;
|
||||
|
||||
/// Invoked when the user asks to sort the table using this column.
|
||||
///
|
||||
/// If null, the column will not be considered sortable.
|
||||
///
|
||||
/// See [DataTable.sortColumnIndex] and [DataTable.sortAscending].
|
||||
final DataColumnSortCallback onSort;
|
||||
|
||||
bool get _debugInteractive => onSort != null;
|
||||
}
|
||||
|
||||
/// Row configuration and cell data for a [DataTable].
|
||||
///
|
||||
/// One row configuration must be provided for each row to
|
||||
/// display in the table. The list of [DataRow] objects is passed
|
||||
/// as the `rows` argument to the [new DataTable] constructor.
|
||||
///
|
||||
/// The data for this row of the table is provided in the [cells]
|
||||
/// property of the [DataRow] object.
|
||||
class DataRow {
|
||||
/// Creates the configuration for a row of a [DataTable].
|
||||
///
|
||||
/// The [cells] argument must not be null.
|
||||
const DataRow({
|
||||
this.key,
|
||||
this.selected: false,
|
||||
this.onSelectChanged,
|
||||
this.cells
|
||||
});
|
||||
|
||||
/// A [Key] that uniquely identifies this row. This is used to
|
||||
/// ensure that if a row is added or removed, any stateful widgets
|
||||
/// related to this row (e.g. an in-progress checkbox animation)
|
||||
/// remain on the right row visually.
|
||||
///
|
||||
/// If the table never changes once created, no key is necessary.
|
||||
final LocalKey key;
|
||||
|
||||
/// Invoked when the user selects or unselects a selectable row.
|
||||
///
|
||||
/// If this is not null, then the row is selectable. The current
|
||||
/// selection state of the row is given by [selected].
|
||||
///
|
||||
/// If any row is selectable, then the table's heading row will have
|
||||
/// a checkbox that can be checked to select all selectable rows
|
||||
/// (and which is checked if all the rows are selected), and each
|
||||
/// subsequent row will have a checkbox to toggle just that row.
|
||||
///
|
||||
/// A row whose [onSelectChanged] callback is null is ignored for
|
||||
/// the purposes of determining the state of the "all" checkbox,
|
||||
/// and its checkbox is disabled.
|
||||
final ValueChanged<bool> onSelectChanged;
|
||||
|
||||
/// Whether the row is selected.
|
||||
///
|
||||
/// If [onSelectChanged] is non-null for any row in the table, then
|
||||
/// a checkbox is shown at the start of each row. If the row is
|
||||
/// selected (true), the checkbox will be checked and the row will
|
||||
/// be highlighted.
|
||||
///
|
||||
/// Otherwise, the checkbox, if present, will not be checked.
|
||||
final bool selected;
|
||||
|
||||
/// The data for this row.
|
||||
///
|
||||
/// There must be exactly as many cells as there are columns in the
|
||||
/// table.
|
||||
final List<DataCell> cells;
|
||||
|
||||
bool get _debugInteractive => onSelectChanged != null || cells.any((DataCell cell) => cell._debugInteractive);
|
||||
}
|
||||
|
||||
/// The data for a cell of a [DataTable].
|
||||
///
|
||||
/// One list of [DataCell] objects must be provided for each [DataRow]
|
||||
/// in the [DataTable], in the [new DataRow] constructor's `cells`
|
||||
/// argument.
|
||||
class DataCell {
|
||||
/// Creates an object to hold the data for a cell in a [DataTable].
|
||||
///
|
||||
/// The first argument is the widget to show for the cell, typically
|
||||
/// a [Text] or [DropDownButton] widget; this becomes the [widget]
|
||||
/// property and must not be null.
|
||||
///
|
||||
/// If the cell has no data, then a [Text] widget with placeholder
|
||||
/// text should be provided instead, and then the [placeholder]
|
||||
/// argument should be set to true.
|
||||
const DataCell(this.widget, {
|
||||
this.placeholder: false,
|
||||
this.showEditIcon: false,
|
||||
this.onTap
|
||||
});
|
||||
|
||||
/// The data for the row.
|
||||
///
|
||||
/// Typically a [Text] widget or a [DropDownButton] widget.
|
||||
///
|
||||
/// If the cell has no data, then a [Text] widget with placeholder
|
||||
/// text should be provided instead, and [placeholder] should be set
|
||||
/// to true.
|
||||
final Widget widget;
|
||||
|
||||
/// Whether the [widget] is actually a placeholder.
|
||||
///
|
||||
/// If this is true, the default text style for the cell is changed
|
||||
/// to be appropriate for placeholder text.
|
||||
final bool placeholder;
|
||||
|
||||
/// Whether to show an edit icon at the end of the cell.
|
||||
///
|
||||
/// This does not make the cell actually editable; the caller must
|
||||
/// implement editing behavior if desired (initiated from the
|
||||
/// [onTap] callback).
|
||||
///
|
||||
/// If this is set, [onTap] should also be set, otherwise tapping
|
||||
/// the icon will have no effect.
|
||||
final bool showEditIcon;
|
||||
|
||||
/// Invoked if the cell is tapped.
|
||||
///
|
||||
/// If non-null, tapping the cell will invoke this callback. If
|
||||
/// null, tapping the cell will attempt to select the row (if
|
||||
/// [TableRow.onSelectChanged] is provided).
|
||||
final VoidCallback onTap;
|
||||
|
||||
bool get _debugInteractive => onTap != null;
|
||||
}
|
||||
|
||||
/// A material design data table.
|
||||
///
|
||||
/// Displaying data in a table is expensive, because to lay out the
|
||||
/// table all the data must be measured twice, once to negotiate the
|
||||
/// dimensions to use for each column, and once to actually lay out
|
||||
/// the table given the results of the negotiation.
|
||||
///
|
||||
// /// For this reason, if you have a lot of data (say, more than a dozen
|
||||
// /// rows with a dozen columns, though the precise limits depend on the
|
||||
// /// target device), it is suggested that you use a [DataCard] which
|
||||
// /// automatically splits the data into multiple pages.
|
||||
// ///
|
||||
/// See also:
|
||||
///
|
||||
/// * [DataColumn]
|
||||
/// * [DataRow]
|
||||
/// * [DataCell]
|
||||
/// * <https://www.google.com/design/spec/components/data-tables.html>
|
||||
class DataTable extends StatelessWidget {
|
||||
/// Creates a widget describing a data table.
|
||||
///
|
||||
/// The [columns] argument must be a list of as many [DataColumn]
|
||||
/// objects as the table is to have columns, ignoring the leading
|
||||
/// checkbox column if any. The [columns] argument must have a
|
||||
/// length greater than zero.
|
||||
///
|
||||
/// The [rows] argument must be a list of as many [DataRow] objects
|
||||
/// as the table is to have rows, ignoring the leading heading row
|
||||
/// that contains the column headings (derived from the [columns]
|
||||
/// argument). There may be zero rows, but the rows argument must
|
||||
/// not be null.
|
||||
///
|
||||
/// Each [DataRow] object in [rows] must have as many [DataCell]
|
||||
/// objects in the [DataRow.cells] list as the table has columns.
|
||||
///
|
||||
/// If the table is sorted, the column that provides the current
|
||||
/// primary key should be specified by index in [sortColumnIndex], 0
|
||||
/// meaning the first column in [columns], 1 being the next one, and
|
||||
/// so forth.
|
||||
///
|
||||
/// The actual sort order can be specified using [sortAscending]; if
|
||||
/// the sort order is ascending, this should be true (the default),
|
||||
/// otherwise it should be false.
|
||||
DataTable({
|
||||
Key key,
|
||||
List<DataColumn> columns,
|
||||
this.sortColumnIndex,
|
||||
this.sortAscending: true,
|
||||
this.rows
|
||||
}) : columns = columns,
|
||||
_onlyTextColumn = _initOnlyTextColumn(columns), super(key: key) {
|
||||
assert(columns != null);
|
||||
assert(columns.length > 0);
|
||||
assert(sortAscending != null);
|
||||
assert(rows != null);
|
||||
assert(!rows.any((DataRow row) => row.cells.length != columns.length));
|
||||
assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length));
|
||||
}
|
||||
|
||||
/// The configuration and labels for the columns in the table.
|
||||
final List<DataColumn> columns;
|
||||
|
||||
/// The current primary sort key's column.
|
||||
///
|
||||
/// If non-null, indicates that the indicated column is the column
|
||||
/// by which the data is sorted. The number must correspond to the
|
||||
/// index of the relevant column in [columns].
|
||||
///
|
||||
/// Setting this will cause the relevant column to have a sort
|
||||
/// indicator displayed.
|
||||
///
|
||||
/// When this is null, it implies that the table's sort order does
|
||||
/// not correspond to any of the columns.
|
||||
final int sortColumnIndex;
|
||||
|
||||
/// Whether the column mentioned in [sortColumnIndex], if any, is sorted
|
||||
/// in ascending order.
|
||||
///
|
||||
/// If true, the order is ascending (meaning the rows with the
|
||||
/// smallest values for the current sort column are first in the
|
||||
/// table).
|
||||
///
|
||||
/// If false, the order is descending (meaning the rows with the
|
||||
/// smallest values for the current sort column are last in the
|
||||
/// table).
|
||||
final bool sortAscending;
|
||||
|
||||
/// The data to show in each row (excluding the row that contains
|
||||
/// the column headings). Must be non-null, but may be empty.
|
||||
final List<DataRow> rows;
|
||||
|
||||
// Set by the constructor to the index of the only Column that is
|
||||
// non-numeric, if there is exactly one, otherwise null.
|
||||
final int _onlyTextColumn;
|
||||
static int _initOnlyTextColumn(List<DataColumn> columns) {
|
||||
int result;
|
||||
for (int index = 0; index < columns.length; index += 1) {
|
||||
DataColumn column = columns[index];
|
||||
if (!column.numeric) {
|
||||
if (result != null)
|
||||
return null;
|
||||
result = index;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool get _debugInteractive {
|
||||
return columns.any((DataColumn column) => column._debugInteractive)
|
||||
|| rows.any((DataRow row) => row._debugInteractive);
|
||||
}
|
||||
|
||||
static final LocalKey _headingRowKey = new UniqueKey();
|
||||
|
||||
void _handleSelectAll(bool checked) {
|
||||
for (DataRow row in rows) {
|
||||
if ((row.onSelectChanged != null) && (row.selected != checked))
|
||||
row.onSelectChanged(checked);
|
||||
}
|
||||
}
|
||||
|
||||
static const double _kHeadingRowHeight = 56.0;
|
||||
static const double _kDataRowHeight = 48.0;
|
||||
static const double _kTablePadding = 24.0;
|
||||
static const double _kColumnSpacing = 56.0;
|
||||
static const double _kSortArrowPadding = 2.0;
|
||||
static const double _kHeadingFontSize = 12.0;
|
||||
static const Duration _kSortArrowAnimationDuration = const Duration(milliseconds: 150);
|
||||
static const Color _kGrey100Opacity = const Color(0x0A000000); // Grey 100 as opacity instead of solid color
|
||||
|
||||
Widget _buildCheckbox({
|
||||
Color color,
|
||||
bool checked,
|
||||
VoidCallback onRowTap,
|
||||
ValueChanged<bool> onCheckboxChanged
|
||||
}) {
|
||||
Widget contents = new Padding(
|
||||
padding: const EdgeInsets.fromLTRB(_kTablePadding, 0.0, _kTablePadding / 2.0, 0.0),
|
||||
child: new Center(
|
||||
child: new Checkbox(
|
||||
activeColor: color,
|
||||
value: checked,
|
||||
onChanged: onCheckboxChanged
|
||||
)
|
||||
)
|
||||
);
|
||||
if (onRowTap != null) {
|
||||
contents = new RowInkWell(
|
||||
onTap: onRowTap,
|
||||
child: contents
|
||||
);
|
||||
}
|
||||
return new TableCell(
|
||||
verticalAlignment: TableCellVerticalAlignment.fill,
|
||||
child: contents
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeadingCell({
|
||||
EdgeInsets padding,
|
||||
Widget label,
|
||||
String tooltip,
|
||||
bool numeric,
|
||||
VoidCallback onSort,
|
||||
bool sorted,
|
||||
bool ascending
|
||||
}) {
|
||||
if (onSort != null) {
|
||||
final Widget arrow = new _SortArrow(
|
||||
visible: sorted,
|
||||
down: sorted ? ascending : null,
|
||||
duration: _kSortArrowAnimationDuration
|
||||
);
|
||||
final Widget arrowPadding = new SizedBox(width: _kSortArrowPadding);
|
||||
label = new Row(
|
||||
children: numeric ? <Widget>[ arrow, arrowPadding, label ]
|
||||
: <Widget>[ label, arrowPadding, arrow ]
|
||||
);
|
||||
}
|
||||
label = new Container(
|
||||
padding: padding,
|
||||
height: _kHeadingRowHeight,
|
||||
child: new Align(
|
||||
alignment: new FractionalOffset(numeric ? 1.0 : 0.0, 0.5), // TODO(ianh): RTL for non-numeric
|
||||
child: new AnimatedDefaultTextStyle(
|
||||
style: new TextStyle(
|
||||
// TODO(ianh): font family should be Roboto; see https://github.com/flutter/flutter/issues/3116
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: _kHeadingFontSize,
|
||||
color: onSort != null && sorted ? Colors.black87 : Colors.black54,
|
||||
height: _kHeadingRowHeight / _kHeadingFontSize
|
||||
),
|
||||
duration: _kSortArrowAnimationDuration,
|
||||
child: label
|
||||
)
|
||||
)
|
||||
);
|
||||
if (tooltip != null) {
|
||||
label = new Tooltip(
|
||||
message: tooltip,
|
||||
child: label
|
||||
);
|
||||
}
|
||||
if (onSort != null) {
|
||||
label = new InkWell(
|
||||
onTap: onSort,
|
||||
// TODO(ianh): When we do RTL, we need to use 'end' ordering for the non-numeric case
|
||||
child: label
|
||||
);
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
Widget _buildDataCell({
|
||||
EdgeInsets padding,
|
||||
Widget label,
|
||||
bool numeric,
|
||||
bool placeholder,
|
||||
bool showEditIcon,
|
||||
VoidCallback onTap,
|
||||
VoidCallback onSelectChanged
|
||||
}) {
|
||||
if (showEditIcon) {
|
||||
final Widget icon = new Icon(icon: Icons.edit, size: 18.0);
|
||||
label = new Flexible(child: label);
|
||||
label = new Row(children: numeric ? <Widget>[ icon, label ] : <Widget>[ label, icon ]);
|
||||
}
|
||||
label = new Container(
|
||||
padding: padding,
|
||||
height: _kDataRowHeight,
|
||||
child: new Align(
|
||||
alignment: new FractionalOffset(numeric ? 1.0 : 0.0, 0.5), // TODO(ianh): RTL for non-numeric
|
||||
child: new DefaultTextStyle(
|
||||
style: new TextStyle(
|
||||
// TODO(ianh): font family should be Roboto; see https://github.com/flutter/flutter/issues/3116
|
||||
fontSize: 13.0,
|
||||
color: placeholder ? Colors.black38 : Colors.black87 // TODO(ianh): defer to theme, since this won't work in e.g. the dark theme
|
||||
),
|
||||
child: new IconTheme(
|
||||
data: new IconThemeData(
|
||||
color: Colors.black54
|
||||
),
|
||||
child: new DropDownButtonHideUnderline(child: label)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
if (onTap != null) {
|
||||
label = new InkWell(
|
||||
onTap: onTap,
|
||||
child: label
|
||||
);
|
||||
} else if (onSelectChanged != null) {
|
||||
label = new RowInkWell(
|
||||
onTap: onSelectChanged,
|
||||
child: label
|
||||
);
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(!_debugInteractive || debugCheckHasMaterial(context));
|
||||
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final BoxDecoration _kSelectedDecoration = new BoxDecoration(
|
||||
backgroundColor: _kGrey100Opacity, // has to be transparent so you can see the ink on the material
|
||||
border: new Border(bottom: new BorderSide(color: theme.dividerColor))
|
||||
);
|
||||
final BoxDecoration _kUnselectedDecoration = new BoxDecoration(
|
||||
border: new Border(bottom: new BorderSide(color: theme.dividerColor))
|
||||
);
|
||||
|
||||
final bool showCheckboxColumn = rows.any((DataRow row) => row.onSelectChanged != null);
|
||||
final bool allChecked = showCheckboxColumn && !rows.any((DataRow row) => row.onSelectChanged != null && !row.selected);
|
||||
|
||||
List<TableColumnWidth> tableColumns = new List<TableColumnWidth>(columns.length + (showCheckboxColumn ? 1 : 0));
|
||||
List<TableRow> tableRows = new List<TableRow>.generate(
|
||||
rows.length + 1, // the +1 is for the header row
|
||||
(int index) {
|
||||
return new TableRow(
|
||||
key: index == 0 ? _headingRowKey : rows[index - 1].key,
|
||||
decoration: index > 0 && rows[index - 1].selected ? _kSelectedDecoration
|
||||
: _kUnselectedDecoration,
|
||||
children: new List<Widget>(tableColumns.length)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
int rowIndex;
|
||||
|
||||
int displayColumnIndex = 0;
|
||||
if (showCheckboxColumn) {
|
||||
tableColumns[0] = new FixedColumnWidth(_kTablePadding + Checkbox.width + _kTablePadding / 2.0);
|
||||
tableRows[0].children[0] = _buildCheckbox(
|
||||
color: theme.accentColor,
|
||||
checked: allChecked,
|
||||
onCheckboxChanged: _handleSelectAll
|
||||
);
|
||||
rowIndex = 1;
|
||||
for (DataRow row in rows) {
|
||||
tableRows[rowIndex].children[0] = _buildCheckbox(
|
||||
color: theme.accentColor,
|
||||
checked: row.selected,
|
||||
onRowTap: () => row.onSelectChanged(!row.selected),
|
||||
onCheckboxChanged: row.onSelectChanged
|
||||
);
|
||||
rowIndex += 1;
|
||||
}
|
||||
displayColumnIndex += 1;
|
||||
}
|
||||
|
||||
for (int dataColumnIndex = 0; dataColumnIndex < columns.length; dataColumnIndex += 1) {
|
||||
DataColumn column = columns[dataColumnIndex];
|
||||
final EdgeInsets padding = new EdgeInsets.fromLTRB(
|
||||
dataColumnIndex == 0 ? showCheckboxColumn ? _kTablePadding / 2.0 : _kTablePadding : _kColumnSpacing / 2.0,
|
||||
0.0,
|
||||
dataColumnIndex == columns.length - 1 ? _kTablePadding : _kColumnSpacing / 2.0,
|
||||
0.0
|
||||
);
|
||||
if (dataColumnIndex == _onlyTextColumn) {
|
||||
tableColumns[displayColumnIndex] = const IntrinsicColumnWidth(flex: 1.0);
|
||||
} else {
|
||||
tableColumns[displayColumnIndex] = const IntrinsicColumnWidth();
|
||||
}
|
||||
tableRows[0].children[displayColumnIndex] = _buildHeadingCell(
|
||||
padding: padding,
|
||||
label: column.label,
|
||||
tooltip: column.tooltip,
|
||||
numeric: column.numeric,
|
||||
onSort: () => column.onSort(dataColumnIndex, sortColumnIndex == dataColumnIndex ? !sortAscending : true),
|
||||
sorted: dataColumnIndex == sortColumnIndex,
|
||||
ascending: sortAscending
|
||||
);
|
||||
rowIndex = 1;
|
||||
for (DataRow row in rows) {
|
||||
DataCell cell = row.cells[dataColumnIndex];
|
||||
tableRows[rowIndex].children[displayColumnIndex] = _buildDataCell(
|
||||
padding: padding,
|
||||
label: cell.widget,
|
||||
numeric: column.numeric,
|
||||
placeholder: cell.placeholder,
|
||||
showEditIcon: cell.showEditIcon,
|
||||
onTap: cell.onTap,
|
||||
onSelectChanged: () => row.onSelectChanged(!row.selected)
|
||||
);
|
||||
rowIndex += 1;
|
||||
}
|
||||
displayColumnIndex += 1;
|
||||
}
|
||||
|
||||
return new Table(
|
||||
columnWidths: tableColumns.asMap(),
|
||||
children: tableRows
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A rectangular area of a Material that responds to touch but clips
|
||||
/// its ink splashes to the current table row of the nearest table.
|
||||
///
|
||||
/// Must have an ancestor [Material] widget in which to cause ink
|
||||
/// reactions and an ancestor [Table] widget to establish a row.
|
||||
///
|
||||
/// The RowInkWell must be in the same coordinate space (modulo
|
||||
/// translations) as the [Table]. If it's rotated or scaled or
|
||||
/// otherwise transformed, it will not be able to describe the
|
||||
/// rectangle of the row in its own coordinate system as a [Rect], and
|
||||
/// thus the splash will not occur. (In general, this is easy to
|
||||
/// achieve: just put the RowInkWell as the direct child of the
|
||||
/// [Table], and put the other contents of the cell inside it.)
|
||||
class RowInkWell extends InkResponse {
|
||||
RowInkWell({
|
||||
Key key,
|
||||
Widget child,
|
||||
GestureTapCallback onTap,
|
||||
GestureTapCallback onDoubleTap,
|
||||
GestureLongPressCallback onLongPress,
|
||||
ValueChanged<bool> onHighlightChanged
|
||||
}) : super(
|
||||
key: key,
|
||||
child: child,
|
||||
onTap: onTap,
|
||||
onDoubleTap: onDoubleTap,
|
||||
onLongPress: onLongPress,
|
||||
onHighlightChanged: onHighlightChanged,
|
||||
containedInkWell: true,
|
||||
highlightShape: BoxShape.rectangle
|
||||
);
|
||||
|
||||
@override
|
||||
RectCallback getRectCallback(RenderBox referenceBox) {
|
||||
return () {
|
||||
RenderObject cell = referenceBox;
|
||||
AbstractNode table = cell.parent;
|
||||
Matrix4 transform = new Matrix4.identity();
|
||||
while (table is RenderObject && table is! RenderTable) {
|
||||
RenderTable parentBox = table;
|
||||
parentBox.applyPaintTransform(cell, transform);
|
||||
assert(table == cell.parent);
|
||||
cell = table;
|
||||
table = table.parent;
|
||||
}
|
||||
if (table is RenderTable) {
|
||||
TableCellParentData cellParentData = cell.parentData;
|
||||
assert(cellParentData.y != null);
|
||||
Rect rect = table.getRowBox(cellParentData.y);
|
||||
// The rect is in the table's coordinate space. We need to change it to the
|
||||
// RowInkWell's coordinate space.
|
||||
table.applyPaintTransform(cell, transform);
|
||||
Offset offset = MatrixUtils.getAsTranslation(transform);
|
||||
if (offset != null)
|
||||
return rect.shift(-offset);
|
||||
}
|
||||
return Rect.zero;
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool debugCheckContext(BuildContext context) {
|
||||
assert(debugCheckHasTable(context));
|
||||
return super.debugCheckContext(context);
|
||||
}
|
||||
}
|
||||
|
||||
class _SortArrow extends StatefulWidget {
|
||||
_SortArrow({
|
||||
Key key,
|
||||
this.visible,
|
||||
this.down,
|
||||
this.duration
|
||||
}) : super(key: key);
|
||||
|
||||
final bool visible;
|
||||
|
||||
final bool down;
|
||||
|
||||
final Duration duration;
|
||||
|
||||
@override
|
||||
_SortArrowState createState() => new _SortArrowState();
|
||||
}
|
||||
|
||||
class _SortArrowState extends State<_SortArrow> {
|
||||
|
||||
AnimationController _opacityController;
|
||||
Animation<double> _opacityAnimation;
|
||||
|
||||
AnimationController _orientationController;
|
||||
Animation<double> _orientationAnimation;
|
||||
double _orientationOffset = 0.0;
|
||||
|
||||
bool _down;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_opacityAnimation = new CurvedAnimation(
|
||||
parent: _opacityController = new AnimationController(
|
||||
duration: config.duration
|
||||
),
|
||||
curve: Curves.ease
|
||||
)
|
||||
..addListener(_rebuild);
|
||||
_opacityController.value = config.visible ? 1.0 : 0.0;
|
||||
_orientationAnimation = new Tween<double>(
|
||||
begin: 0.0,
|
||||
end: math.PI
|
||||
).animate(new CurvedAnimation(
|
||||
parent: _orientationController = new AnimationController(
|
||||
duration: config.duration
|
||||
),
|
||||
curve: Curves.easeIn
|
||||
))
|
||||
..addListener(_rebuild)
|
||||
..addStatusListener(_resetOrientationAnimation);
|
||||
if (config.visible)
|
||||
_orientationOffset = config.down ? 0.0 : math.PI;
|
||||
}
|
||||
|
||||
void _rebuild() {
|
||||
setState(() {
|
||||
// The animations changed, so we need to rebuild.
|
||||
});
|
||||
}
|
||||
|
||||
void _resetOrientationAnimation(AnimationStatus status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
assert(_orientationAnimation.value == math.PI);
|
||||
_orientationOffset += math.PI;
|
||||
_orientationController.value = 0.0; // TODO(ianh): This triggers a pointless rebuild.
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateConfig(_SortArrow oldConfig) {
|
||||
super.didUpdateConfig(oldConfig);
|
||||
bool skipArrow = false;
|
||||
bool newDown = config.down != null ? config.down : _down;
|
||||
if (oldConfig.visible != config.visible) {
|
||||
if (config.visible && (_opacityController.status == AnimationStatus.dismissed)) {
|
||||
_orientationController.stop();
|
||||
_orientationController.value = 0.0;
|
||||
_orientationOffset = newDown ? 0.0 : math.PI;
|
||||
skipArrow = true;
|
||||
}
|
||||
if (config.visible) {
|
||||
_opacityController.forward();
|
||||
} else {
|
||||
_opacityController.reverse();
|
||||
}
|
||||
}
|
||||
if ((_down != newDown) && !skipArrow) {
|
||||
if (_orientationController.status == AnimationStatus.dismissed) {
|
||||
_orientationController.forward();
|
||||
} else {
|
||||
_orientationController.reverse();
|
||||
}
|
||||
}
|
||||
_down = newDown;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_opacityController.dispose();
|
||||
_orientationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
static const double _kArrowIconBaselineOffset = -1.5;
|
||||
static const double _kArrowIconSize = 16.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return new Opacity(
|
||||
opacity: _opacityAnimation.value,
|
||||
child: new Transform(
|
||||
transform: new Matrix4.rotationZ(_orientationOffset + _orientationAnimation.value)
|
||||
..setTranslationRaw(0.0, _kArrowIconBaselineOffset, 0.0),
|
||||
alignment: FractionalOffset.center,
|
||||
child: new Icon(
|
||||
icon: Icons.arrow_downward,
|
||||
size: _kArrowIconSize,
|
||||
color: Colors.black87
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
TODO(ianh): implement DataTableCard
|
||||
|
||||
DataTableCard
|
||||
- top: 64px
|
||||
- caption, top left
|
||||
- 20px Roboto Regular, black87
|
||||
- persistent actions, top left
|
||||
- header when there's a selection
|
||||
- accent 50?
|
||||
- show number of selected items
|
||||
- different actions
|
||||
- actions, top right
|
||||
- 24px icons, black54
|
||||
|
||||
bottom:
|
||||
- 56px
|
||||
- handles pagination
|
||||
- 12px Roboto Regular, black54
|
||||
|
||||
*/
|
@ -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.
|
||||
///
|
||||
/// Does nothing if asserts are disabled.
|
||||
/// 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. 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,26 +364,35 @@ class _DropDownButtonState<T> extends State<DropDownButton<T>> {
|
||||
@override
|
||||
Widget build(BuildContext 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(
|
||||
onTap: _handleTap,
|
||||
child: new Container(
|
||||
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
|
||||
)
|
||||
)
|
||||
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();
|
||||
}
|
||||
|
@ -19,17 +19,47 @@ class TableCellParentData extends BoxParentData {
|
||||
int y;
|
||||
|
||||
@override
|
||||
String toString() => '${super.toString()}; $verticalAlignment';
|
||||
String toString() => '${super.toString()}; ${verticalAlignment == null ? "default vertical alignment" : "$verticalAlignment"}';
|
||||
}
|
||||
|
||||
/// Base class to describe how wide a column in a [RenderTable] should be.
|
||||
abstract class TableColumnWidth {
|
||||
/// Abstract const constructor. This constructor enables subclasses to provide
|
||||
/// const constructors so that they can be used in const expressions.
|
||||
const TableColumnWidth();
|
||||
|
||||
/// The smallest width that the column can have.
|
||||
///
|
||||
/// The `cells` argument is an iterable that provides all the cells
|
||||
/// in the table for this column. Walking the cells is by definition
|
||||
/// O(N), so algorithms that do that should be considered expensive.
|
||||
///
|
||||
/// The `containerWidth` argument is the `maxWidth` of the incoming
|
||||
/// constraints for the table, and might be infinite.
|
||||
double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth);
|
||||
|
||||
/// The ideal width that the column should have. This must be equal
|
||||
/// to or greater than the [minIntrinsicWidth]. The column might be
|
||||
/// bigger than this width, e.g. if the column is flexible or if the
|
||||
/// table's width ends up being forced to be bigger than the sum of
|
||||
/// all the maxIntrinsicWidth values.
|
||||
///
|
||||
/// The `cells` argument is an iterable that provides all the cells
|
||||
/// in the table for this column. Walking the cells is by definition
|
||||
/// O(N), so algorithms that do that should be considered expensive.
|
||||
///
|
||||
/// The `containerWidth` argument is the `maxWidth` of the incoming
|
||||
/// constraints for the table, and might be infinite.
|
||||
double maxIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth);
|
||||
|
||||
/// The flex factor to apply to the cell if there is any room left
|
||||
/// over when laying out the table. The remaining space is
|
||||
/// distributed to any columns with flex in proportion to their flex
|
||||
/// value (higher values get more space).
|
||||
///
|
||||
/// The `cells` argument is an iterable that provides all the cells
|
||||
/// in the table for this column. Walking the cells is by definition
|
||||
/// O(N), so algorithms that do that should be considered expensive.
|
||||
double flex(Iterable<RenderBox> cells) => null;
|
||||
|
||||
@override
|
||||
@ -40,8 +70,12 @@ abstract class TableColumnWidth {
|
||||
/// cells in that column.
|
||||
///
|
||||
/// This is a very expensive way to size a column.
|
||||
///
|
||||
/// A flex value can be provided. If specified (and non-null), the
|
||||
/// column will participate in the distribution of remaining space
|
||||
/// once all the non-flexible columns have been sized.
|
||||
class IntrinsicColumnWidth extends TableColumnWidth {
|
||||
const IntrinsicColumnWidth();
|
||||
const IntrinsicColumnWidth({ double flex }) : _flex = flex;
|
||||
|
||||
@override
|
||||
double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) {
|
||||
@ -58,6 +92,11 @@ class IntrinsicColumnWidth extends TableColumnWidth {
|
||||
result = math.max(result, cell.getMaxIntrinsicWidth(const BoxConstraints()));
|
||||
return result;
|
||||
}
|
||||
|
||||
final double _flex;
|
||||
|
||||
@override
|
||||
double flex(Iterable<RenderBox> cells) => _flex;
|
||||
}
|
||||
|
||||
/// Sizes the column to a specific number of pixels.
|
||||
@ -169,10 +208,13 @@ class MaxColumnWidth extends TableColumnWidth {
|
||||
|
||||
@override
|
||||
double flex(Iterable<RenderBox> cells) {
|
||||
double aFlex = a.flex(cells);
|
||||
final double aFlex = a.flex(cells);
|
||||
if (aFlex == null)
|
||||
return b.flex(cells);
|
||||
return math.max(aFlex, b.flex(cells));
|
||||
final double bFlex = b.flex(cells);
|
||||
if (bFlex == null)
|
||||
return null;
|
||||
return math.max(aFlex, bFlex);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -215,7 +257,10 @@ class MinColumnWidth extends TableColumnWidth {
|
||||
double aFlex = a.flex(cells);
|
||||
if (aFlex == null)
|
||||
return b.flex(cells);
|
||||
return math.min(aFlex, b.flex(cells));
|
||||
double bFlex = b.flex(cells);
|
||||
if (bFlex == null)
|
||||
return null;
|
||||
return math.min(aFlex, bFlex);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -535,26 +580,34 @@ class RenderTable extends RenderBox {
|
||||
}
|
||||
assert(cells != null);
|
||||
assert(cells.length % columns == 0);
|
||||
// remove cells that are moving away
|
||||
// fill a set with the cells that are moving (it's important not
|
||||
// to dropChild a child that's remaining with us, because that
|
||||
// would clear their parentData field)
|
||||
final Set<RenderBox> lostChildren = new HashSet<RenderBox>();
|
||||
for (int y = 0; y < _rows; y += 1) {
|
||||
for (int x = 0; x < _columns; x += 1) {
|
||||
int xyOld = x + y * _columns;
|
||||
int xyNew = x + y * columns;
|
||||
if (_children[xyOld] != null && (x >= columns || xyNew >= cells.length || _children[xyOld] != cells[xyNew]))
|
||||
dropChild(_children[xyOld]);
|
||||
lostChildren.add(_children[xyOld]);
|
||||
}
|
||||
}
|
||||
// adopt cells that are arriving
|
||||
// adopt cells that are arriving, and cross cells that are just moving off our list of lostChildren
|
||||
int y = 0;
|
||||
while (y * columns < cells.length) {
|
||||
for (int x = 0; x < columns; x += 1) {
|
||||
int xyNew = x + y * columns;
|
||||
int xyOld = x + y * _columns;
|
||||
if (cells[xyNew] != null && (x >= _columns || y >= _rows || _children[xyOld] != cells[xyNew]))
|
||||
adoptChild(cells[xyNew]);
|
||||
if (cells[xyNew] != null && (x >= _columns || y >= _rows || _children[xyOld] != cells[xyNew])) {
|
||||
if (!lostChildren.remove(cells[xyNew]))
|
||||
adoptChild(cells[xyNew]);
|
||||
}
|
||||
}
|
||||
y += 1;
|
||||
}
|
||||
// drop all the lost children
|
||||
for (RenderBox oldChild in lostChildren)
|
||||
dropChild(oldChild);
|
||||
// update our internal values
|
||||
_columns = columns;
|
||||
_rows = cells.length ~/ columns;
|
||||
@ -666,7 +719,7 @@ class RenderTable extends RenderBox {
|
||||
// honorable mention, most likely to improve if taught about memoization award
|
||||
assert(constraints.debugAssertIsValid());
|
||||
assert(_children.length == rows * columns);
|
||||
final List<double> widths = computeColumnWidths(constraints);
|
||||
final List<double> widths = _computeColumnWidths(constraints);
|
||||
double rowTop = 0.0;
|
||||
for (int y = 0; y < rows; y += 1) {
|
||||
double rowHeight = 0.0;
|
||||
@ -694,6 +747,10 @@ class RenderTable extends RenderBox {
|
||||
return _baselineDistance;
|
||||
}
|
||||
|
||||
/// Returns the list of [RenderBox] objects that are in the given
|
||||
/// column, in row order, starting from the first row.
|
||||
///
|
||||
/// This is a lazily-evaluated iterable.
|
||||
Iterable<RenderBox> column(int x) sync* {
|
||||
for (int y = 0; y < rows; y += 1) {
|
||||
final int xy = x + y * columns;
|
||||
@ -703,6 +760,10 @@ class RenderTable extends RenderBox {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the list of [RenderBox] objects that are on the given
|
||||
/// row, in column order, starting with the first column.
|
||||
///
|
||||
/// This is a lazily-evaluated iterable.
|
||||
Iterable<RenderBox> row(int y) sync* {
|
||||
final int start = y * columns;
|
||||
final int end = (y + 1) * columns;
|
||||
@ -713,48 +774,166 @@ class RenderTable extends RenderBox {
|
||||
}
|
||||
}
|
||||
|
||||
List<double> computeColumnWidths(BoxConstraints constraints) {
|
||||
List<double> _computeColumnWidths(BoxConstraints constraints) {
|
||||
assert(_children.length == rows * columns);
|
||||
// We apply the constraints to the column widths in the order of
|
||||
// least important to most important:
|
||||
// 1. apply the ideal widths (maxIntrinsicWidth)
|
||||
// 2. grow the flex columns so that the table has the maxWidth (if
|
||||
// finite) or the minWidth (if not)
|
||||
// 3. if there were no flex columns, then grow the table to the
|
||||
// minWidth.
|
||||
// 4. apply the maximum width of the table, shrinking columns as
|
||||
// necessary, applying minimum column widths as we go
|
||||
|
||||
// 1. apply ideal widths, and collect information we'll need later
|
||||
final List<double> widths = new List<double>(columns);
|
||||
final List<double> minWidths = new List<double>(columns);
|
||||
final List<double> flexes = new List<double>(columns);
|
||||
double totalMinWidth = 0.0;
|
||||
double totalMaxWidth = constraints.maxWidth.isFinite ? constraints.maxWidth : 0.0;
|
||||
double tableWidth = 0.0; // running tally of the sum of widths[x] for all x
|
||||
double unflexedTableWidth = 0.0; // sum of the maxIntrinsicWidths of any column that has null flex
|
||||
double totalFlex = 0.0;
|
||||
for (int x = 0; x < columns; x += 1) {
|
||||
TableColumnWidth columnWidth = _columnWidths[x] ?? defaultColumnWidth;
|
||||
Iterable<RenderBox> columnCells = column(x);
|
||||
double minIntrinsicWidth = columnWidth.minIntrinsicWidth(columnCells, constraints.maxWidth);
|
||||
widths[x] = minIntrinsicWidth;
|
||||
totalMinWidth += minIntrinsicWidth;
|
||||
if (!constraints.maxWidth.isFinite) {
|
||||
double maxIntrinsicWidth = columnWidth.maxIntrinsicWidth(columnCells, constraints.maxWidth);
|
||||
assert(minIntrinsicWidth <= maxIntrinsicWidth);
|
||||
totalMaxWidth += maxIntrinsicWidth;
|
||||
}
|
||||
// apply ideal width (maxIntrinsicWidth)
|
||||
final double maxIntrinsicWidth = columnWidth.maxIntrinsicWidth(columnCells, constraints.maxWidth);
|
||||
assert(maxIntrinsicWidth.isFinite);
|
||||
assert(maxIntrinsicWidth >= 0.0);
|
||||
widths[x] = maxIntrinsicWidth;
|
||||
tableWidth += maxIntrinsicWidth;
|
||||
// collect min width information while we're at it
|
||||
final double minIntrinsicWidth = columnWidth.minIntrinsicWidth(columnCells, constraints.maxWidth);
|
||||
assert(minIntrinsicWidth.isFinite);
|
||||
assert(minIntrinsicWidth >= 0.0);
|
||||
minWidths[x] = minIntrinsicWidth;
|
||||
assert(maxIntrinsicWidth >= minIntrinsicWidth);
|
||||
// collect flex information while we're at it
|
||||
double flex = columnWidth.flex(columnCells);
|
||||
if (flex != null) {
|
||||
assert(flex != 0.0);
|
||||
assert(flex.isFinite);
|
||||
assert(flex > 0.0);
|
||||
flexes[x] = flex;
|
||||
totalFlex += flex;
|
||||
} else {
|
||||
unflexedTableWidth += maxIntrinsicWidth;
|
||||
}
|
||||
}
|
||||
assert(!widths.any((double value) => value == null));
|
||||
// table is going to be the biggest of:
|
||||
// - the incoming minimum width
|
||||
// - the sum of the cells' minimum widths
|
||||
// - the incoming maximum width if it is finite, or else the table's ideal shrink-wrap width
|
||||
double tableWidth = math.max(constraints.minWidth, math.max(totalMinWidth, totalMaxWidth));
|
||||
double remainingWidth = tableWidth - totalMinWidth;
|
||||
if (remainingWidth > 0.0) {
|
||||
if (totalFlex > 0.0) {
|
||||
final double maxWidthConstraint = constraints.maxWidth;
|
||||
final double minWidthConstraint = constraints.minWidth;
|
||||
|
||||
// 2. grow the flex columns so that the table has the maxWidth (if
|
||||
// finite) or the minWidth (if not)
|
||||
if (totalFlex > 0.0) {
|
||||
// this can only grow the table, but it _will_ grow the table at
|
||||
// least as big as the target width.
|
||||
double targetWidth;
|
||||
if (maxWidthConstraint.isFinite) {
|
||||
targetWidth = maxWidthConstraint;
|
||||
} else {
|
||||
targetWidth = minWidthConstraint;
|
||||
}
|
||||
if (tableWidth < targetWidth) {
|
||||
final double remainingWidth = targetWidth - unflexedTableWidth;
|
||||
assert(remainingWidth.isFinite);
|
||||
assert(remainingWidth >= 0.0);
|
||||
for (int x = 0; x < columns; x += 1) {
|
||||
if (flexes[x] != null) {
|
||||
widths[x] += math.max((flexes[x] / totalFlex) * remainingWidth - widths[x], 0.0);
|
||||
final double flexedWidth = remainingWidth * flexes[x] / totalFlex;
|
||||
assert(flexedWidth.isFinite);
|
||||
assert(flexedWidth >= 0.0);
|
||||
if (widths[x] < flexedWidth) {
|
||||
final double delta = flexedWidth - widths[x];
|
||||
tableWidth += delta;
|
||||
widths[x] = flexedWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int x = 0; x < columns; x += 1)
|
||||
widths[x] += remainingWidth / columns;
|
||||
assert(tableWidth >= targetWidth);
|
||||
}
|
||||
} else // step 2 and 3 are mutually exclusive
|
||||
|
||||
// 3. if there were no flex columns, then grow the table to the
|
||||
// minWidth.
|
||||
if (tableWidth < minWidthConstraint) {
|
||||
final double delta = (minWidthConstraint - tableWidth) / columns;
|
||||
for (int x = 0; x < columns; x += 1)
|
||||
widths[x] += delta;
|
||||
tableWidth = minWidthConstraint;
|
||||
}
|
||||
|
||||
// beyond this point, unflexedTableWidth is no longer valid
|
||||
assert(() { unflexedTableWidth = null; return true; });
|
||||
|
||||
// 4. apply the maximum width of the table, shrinking columns as
|
||||
// necessary, applying minimum column widths as we go
|
||||
if (tableWidth > maxWidthConstraint) {
|
||||
double deficit = tableWidth - maxWidthConstraint;
|
||||
// Some columns may have low flex but have all the free space.
|
||||
// (Consider a case with a 1px wide column of flex 1000.0 and
|
||||
// a 1000px wide column of flex 1.0; the sizes coming from the
|
||||
// maxIntrinsicWidths. If the maximum table width is 2px, then
|
||||
// just applying the flexes to the deficit would result in a
|
||||
// table with one column at -998px and one column at 990px,
|
||||
// which is wildly unhelpful.)
|
||||
// Similarly, some columns may be flexible, but not actually
|
||||
// be shrinkable due to a large minimum width. (Consider a
|
||||
// case with two columns, one is flex and one isn't, both have
|
||||
// 1000px maxIntrinsicWidths, but the flex one has 1000px
|
||||
// minIntrinsicWidth also. The whole deficit will have to come
|
||||
// from the non-flex column.)
|
||||
// So what we do is we repeatedly iterate through the flexible
|
||||
// columns shrinking them proportionally until we have no
|
||||
// available columns, then do the same to the non-flexible ones.
|
||||
int availableColumns = columns;
|
||||
while (deficit > 0.0 && totalFlex > 0.0) {
|
||||
double newTotalFlex = 0.0;
|
||||
for (int x = 0; x < columns; x += 1) {
|
||||
if (flexes[x] != null) {
|
||||
final double newWidth = widths[x] - deficit * flexes[x] / totalFlex;
|
||||
assert(newWidth.isFinite);
|
||||
assert(newWidth >= 0.0);
|
||||
if (newWidth <= minWidths[x]) {
|
||||
// shrank to minimum
|
||||
deficit -= widths[x] - minWidths[x];
|
||||
widths[x] = minWidths[x];
|
||||
flexes[x] = null;
|
||||
availableColumns -= 1;
|
||||
} else {
|
||||
deficit -= widths[x] - newWidth;
|
||||
widths[x] = newWidth;
|
||||
newTotalFlex += flexes[x];
|
||||
}
|
||||
}
|
||||
}
|
||||
totalFlex = newTotalFlex;
|
||||
}
|
||||
if (deficit > 0.0) {
|
||||
// Now we have to take out the remaining space from the
|
||||
// columns that aren't minimum sized.
|
||||
// To make this fair, we repeatedly remove equal amounts from
|
||||
// each column, clamped to the minimum width, until we run out
|
||||
// of columns that aren't at their minWidth.
|
||||
do {
|
||||
final double delta = deficit / availableColumns;
|
||||
int newAvailableColumns = 0;
|
||||
for (int x = 0; x < columns; x += 1) {
|
||||
double availableDelta = widths[x] - minWidths[x];
|
||||
if (availableDelta > 0.0) {
|
||||
if (availableDelta <= delta) {
|
||||
// shrank to minimum
|
||||
deficit -= widths[x] - minWidths[x];
|
||||
widths[x] = minWidths[x];
|
||||
} else {
|
||||
deficit -= availableDelta;
|
||||
widths[x] -= availableDelta;
|
||||
newAvailableColumns += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
availableColumns = newAvailableColumns;
|
||||
} while (deficit > 0.0 && availableColumns > 0);
|
||||
}
|
||||
}
|
||||
return widths;
|
||||
@ -764,16 +943,30 @@ class RenderTable extends RenderBox {
|
||||
List<double> _rowTops = <double>[];
|
||||
List<double> _columnLefts;
|
||||
|
||||
/// Returns the position and dimensions of the box that the given
|
||||
/// row covers, in this render object's coordinate space (so the
|
||||
/// left coordinate is always 0.0).
|
||||
///
|
||||
/// The row being queried must exist.
|
||||
///
|
||||
/// This is only valid after layout.
|
||||
Rect getRowBox(int row) {
|
||||
assert(row >= 0);
|
||||
assert(row < rows);
|
||||
assert(!needsLayout);
|
||||
return new Rect.fromLTRB(0.0, _rowTops[row], size.width, _rowTops[row + 1]);
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
assert(_children.length == rows * columns);
|
||||
if (rows * columns == 0) {
|
||||
// TODO(ianh): if columns is zero, this should be zero width
|
||||
// TODO(ianh): if columns is not zero, this should be based on the column width specifications
|
||||
size = constraints.constrain(const Size(double.INFINITY, 0.0));
|
||||
size = constraints.constrain(const Size(0.0, 0.0));
|
||||
return;
|
||||
}
|
||||
final List<double> widths = computeColumnWidths(constraints);
|
||||
final List<double> widths = _computeColumnWidths(constraints);
|
||||
final List<double> positions = new List<double>(columns);
|
||||
_rowTops.clear();
|
||||
positions[0] = 0.0;
|
||||
|
@ -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', () {
|
||||
|
Loading…
x
Reference in New Issue
Block a user