blob: d367a1d3c229c8bcaba904b54c2f36cf3bdab657 [file] [log] [blame]
// Copyright 2019 The Fuchsia 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/material.dart';
import 'sizer.dart';
import 'tile_model.dart';
import 'utils.dart';
typedef TileChromeBuilder = Widget Function(
BuildContext context, TileModel tile);
typedef CustomTilesBuilder = Widget Function(
BuildContext context, List<TileModel> tiles);
/// A [Widget] that renders a tile give its [TileModel]. If the tile type is
/// [TileType.content], it calls [chromeBuilder] to build a widget to render
/// the tile. Otherwise, it renders the [tiles] children in row or column
/// order. It calls [sizerBuilder] to get a sizing widget to display between
/// rows or columns of tiles.
class Tile extends StatelessWidget {
final TileModel model;
final TileChromeBuilder chromeBuilder;
final TileSizerBuilder sizerBuilder;
final CustomTilesBuilder customTilesBuilder;
final double sizerThickness;
final ValueChanged<TileModel> onFloat;
const Tile({
@required this.model,
@required this.chromeBuilder,
this.customTilesBuilder,
this.sizerBuilder,
this.sizerThickness,
this.onFloat,
});
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: model,
builder: (context, child) {
final type = model.type;
if (model.isContent) {
return chromeBuilder(context, model);
} else if (model.tiles.isEmpty) {
return SizedBox.shrink();
} else {
return LayoutBuilder(
builder: (context, constraints) {
assert(constraints.isTight);
// Filter out tiles that are floating.
final availableTiles = model.tiles;
/// Filter out tiles that don't fit based on their minSize.
final availableSize =
_availableSize(type, availableTiles, constraints.biggest);
final tiles = _fit(type, availableTiles, availableSize).toList();
bool fit = tiles.length == availableTiles.length;
if (type != TileType.custom &&
(fit || customTilesBuilder == null)) {
// Float the tiles that did not fit.
availableTiles
.where((t) => !tiles.contains(t))
.toList()
.forEach(onFloat?.call);
// All tiles fit, set flex,width and height and return tiles.
final flex = _totalFlex(tiles);
for (var tile in tiles) {
tile
..flex = tile.flex / flex
..width = availableSize.width
..height = availableSize.height;
}
// Generate tile widgets with sizer widgets interleaved.
final count = tiles.length + tiles.length - 1;
final tileWidgets = List<Widget>.generate(count, (index) {
if (index.isOdd) {
return Sizer(
direction: model.type == TileType.row
? Axis.horizontal
: Axis.vertical,
tileBefore: tiles[index ~/ 2],
tileAfter: tiles[index ~/ 2 + 1],
sizerBuilder: sizerBuilder,
);
} else {
return _tileBuilder(
tile: tiles[index ~/ 2],
chromeBuilder: chromeBuilder,
sizerBuilder: sizerBuilder,
customTilesBuilder: customTilesBuilder,
sizerThickness: sizerThickness,
onFloat: onFloat,
);
}
});
return model.type == TileType.row
? Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: tileWidgets,
)
: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: tileWidgets,
);
} else {
// Flatten all tiles to group them for tabbing between them.
final allTiles = flatten(availableTiles).toList();
// Display tabs only when we have more than 1 tile.
if (allTiles.length > 1) {
return customTilesBuilder?.call(context, allTiles) ??
Offstage();
} else {
return _tileBuilder(
tile: allTiles.first,
chromeBuilder: chromeBuilder,
sizerBuilder: sizerBuilder,
customTilesBuilder: customTilesBuilder,
sizerThickness: sizerThickness,
onFloat: onFloat);
}
}
},
);
}
},
);
}
Widget _tileBuilder({
TileModel tile,
TileChromeBuilder chromeBuilder,
TileSizerBuilder sizerBuilder,
CustomTilesBuilder customTilesBuilder,
ValueChanged<TileModel> onFloat,
double sizerThickness,
}) =>
AnimatedBuilder(
animation: tile,
child: Tile(
model: tile,
chromeBuilder: chromeBuilder,
sizerBuilder: sizerBuilder,
customTilesBuilder: customTilesBuilder,
onFloat: onFloat,
sizerThickness: sizerThickness,
),
builder: (context, child) => SizedBox(
width: tile.width,
height: tile.height,
child: child,
),
);
/// Fit as many tiles as possible within [size].
Iterable<TileModel> _fit(
TileType type, Iterable<TileModel> tiles, Size size) {
if (tiles.isEmpty) {
return tiles;
}
// Calculate the normalized flex of each tile.
final flex = _totalFlex(tiles);
final width = size.width;
final height = size.height;
// Find tiles that don't fit based on their minSize.
final unfitableTiles = tiles.where((tile) {
double tileFlex = tile.flex / flex;
double tileWidth = type == TileType.column ? tileFlex * width : width;
double tileHeight = type == TileType.row ? tileFlex * height : height;
return type == TileType.column
? tile.minSize.width > tileWidth
: tile.minSize.height > tileHeight;
});
if (unfitableTiles.isEmpty) {
return tiles;
} else {
// Remove the last tile in unfittableTiles from tiles and run fit again.
return _fit(type, tiles.where((t) => t != unfitableTiles.last), size);
}
}
// Returns the size available after sizers thickness is accounted for.
Size _availableSize(TileType type, Iterable<TileModel> tiles, Size size) {
final numTiles = tiles.length;
final numSizers = numTiles - 1;
// Calculate the width/height of a tile minus sizer thickness.
final width = type == TileType.column
? (size.width - sizerThickness * numSizers)
: size.width;
final height = type == TileType.row
? (size.height - sizerThickness * numSizers)
: size.height;
return Size(width, height);
}
double _totalFlex(Iterable<TileModel> tiles) =>
tiles.map((t) => t.flex).reduce((f1, f2) => f1 + f2);
}