1
0
mirror of https://github.com/flutter/samples.git synced 2026-06-07 23:09:51 +00:00

[Gallery] Fix directory structure (#312)

This commit is contained in:
Pierre-Louis
2020-02-05 20:11:54 +01:00
committed by GitHub
parent 082592e9a9
commit cee267cf88
762 changed files with 12 additions and 12 deletions

View File

@@ -0,0 +1,295 @@
// Copyright 2019 The Flutter team. 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';
import 'package:flutter/material.dart';
import 'package:gallery/data/gallery_options.dart';
import 'package:gallery/layout/text_scale.dart';
import 'package:gallery/studies/shrine/category_menu_page.dart';
import 'package:gallery/studies/shrine/model/product.dart';
import 'package:gallery/studies/shrine/supplemental/balanced_layout.dart';
import 'package:gallery/studies/shrine/page_status.dart';
import 'package:gallery/studies/shrine/supplemental/desktop_product_columns.dart';
import 'package:gallery/studies/shrine/supplemental/product_columns.dart';
import 'package:gallery/studies/shrine/supplemental/product_card.dart';
const _topPadding = 34.0;
const _bottomPadding = 44.0;
const _cardToScreenWidthRatio = 0.59;
class MobileAsymmetricView extends StatelessWidget {
const MobileAsymmetricView({Key key, this.products}) : super(key: key);
final List<Product> products;
List<Container> _buildColumns(
BuildContext context,
BoxConstraints constraints,
) {
if (products == null || products.isEmpty) {
return const [];
}
// Decide whether the page size and text size allow 2-column products.
final double cardHeight = (constraints.biggest.height -
_topPadding -
_bottomPadding -
TwoProductCardColumn.spacerHeight) /
2;
final double imageWidth =
_cardToScreenWidthRatio * constraints.biggest.width -
TwoProductCardColumn.horizontalPadding;
final double imageHeight = cardHeight -
MobileProductCard.defaultTextBoxHeight *
GalleryOptions.of(context).textScaleFactor(context);
final bool shouldUseAlternatingLayout =
imageHeight > 0 && imageWidth / imageHeight < 49 / 33;
if (shouldUseAlternatingLayout) {
// Alternating layout: a layout of alternating 2-product
// and 1-product columns.
//
// This will return a list of columns. It will oscillate between the two
// kinds of columns. Even cases of the index (0, 2, 4, etc) will be
// TwoProductCardColumn and the odd cases will be OneProductCardColumn.
//
// Each pair of columns will advance us 3 products forward (2 + 1). That's
// some kinda awkward math so we use _evenCasesIndex and _oddCasesIndex as
// helpers for creating the index of the product list that will correspond
// to the index of the list of columns.
return List<Container>.generate(_listItemCount(products.length), (index) {
double width =
_cardToScreenWidthRatio * MediaQuery.of(context).size.width;
Widget column;
if (index % 2 == 0) {
/// Even cases
final int bottom = _evenCasesIndex(index);
column = TwoProductCardColumn(
bottom: products[bottom],
top:
products.length - 1 >= bottom + 1 ? products[bottom + 1] : null,
imageAspectRatio: imageWidth / imageHeight,
);
width += 32;
} else {
/// Odd cases
column = OneProductCardColumn(
product: products[_oddCasesIndex(index)],
reverse: true,
);
}
return Container(
width: width,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: column,
),
);
}).toList();
} else {
// Alternating layout: a layout of 1-product columns.
return [
for (final product in products)
Container(
width: _cardToScreenWidthRatio * MediaQuery.of(context).size.width,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: OneProductCardColumn(
product: product,
reverse: false,
),
),
)
];
}
}
int _evenCasesIndex(int input) {
// The operator ~/ is a cool one. It's the truncating division operator. It
// divides the number and if there's a remainder / decimal, it cuts it off.
// This is like dividing and then casting the result to int. Also, it's
// functionally equivalent to floor() in this case.
return input ~/ 2 * 3;
}
int _oddCasesIndex(int input) {
assert(input > 0);
return (input / 2).ceil() * 3 - 1;
}
int _listItemCount(int totalItems) {
return (totalItems % 3 == 0)
? totalItems ~/ 3 * 2
: (totalItems / 3).ceil() * 2 - 1;
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: PageStatus.of(context).cartController,
builder: (context, child) => AnimatedBuilder(
animation: PageStatus.of(context).menuController,
builder: (context, child) => ExcludeSemantics(
excluding: !productPageIsVisible(context),
child: LayoutBuilder(
builder: (context, constraints) {
return ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsetsDirectional.fromSTEB(
0,
_topPadding,
16,
_bottomPadding,
),
children: _buildColumns(context, constraints),
physics: const AlwaysScrollableScrollPhysics(),
);
},
),
),
),
);
}
}
class DesktopAsymmetricView extends StatelessWidget {
const DesktopAsymmetricView({Key key, this.products}) : super(key: key);
final List<Product> products;
@override
Widget build(BuildContext context) {
// Determine the scale factor for the desktop asymmetric view.
final double textScaleFactor =
GalleryOptions.of(context).textScaleFactor(context);
// When text is larger, the images becomes wider, but at half the rate.
final double imageScaleFactor = reducedTextScale(context);
// When text is larger, horizontal padding becomes smaller.
final double paddingScaleFactor = textScaleFactor >= 1.5 ? 0.25 : 1;
// Calculate number of columns
final double sidebar = desktopCategoryMenuPageWidth(context: context);
final double minimumBoundaryWidth = 84 * paddingScaleFactor;
final double columnWidth = 186 * imageScaleFactor;
final double columnGapWidth = 24 * imageScaleFactor;
final double windowWidth = MediaQuery.of(context).size.width;
final int idealColumnCount = max(
1,
((windowWidth + columnGapWidth - 2 * minimumBoundaryWidth - sidebar) /
(columnWidth + columnGapWidth))
.floor(),
);
// Limit column width to fit within window when there is only one column.
final double actualColumnWidth = idealColumnCount == 1
? min(
columnWidth,
windowWidth - sidebar - 2 * minimumBoundaryWidth,
)
: columnWidth;
final int columnCount = min(idealColumnCount, max(products.length, 1));
return AnimatedBuilder(
animation: PageStatus.of(context).cartController,
builder: (context, child) => ExcludeSemantics(
excluding: !productPageIsVisible(context),
child: DesktopColumns(
columnCount: columnCount,
products: products,
largeImageWidth: actualColumnWidth,
smallImageWidth: columnCount > 1
? columnWidth - columnGapWidth
: actualColumnWidth,
),
),
);
}
}
class DesktopColumns extends StatelessWidget {
DesktopColumns({
@required this.columnCount,
@required this.products,
@required this.largeImageWidth,
@required this.smallImageWidth,
});
final int columnCount;
final List<Product> products;
final double largeImageWidth;
final double smallImageWidth;
@override
Widget build(BuildContext context) {
final Widget _gap = Container(width: 24);
final List<List<Product>> productCardLists = balancedLayout(
context: context,
columnCount: columnCount,
products: products,
largeImageWidth: largeImageWidth,
smallImageWidth: smallImageWidth,
);
final List<DesktopProductCardColumn> productCardColumns =
List<DesktopProductCardColumn>.generate(
columnCount,
(column) {
final bool alignToEnd =
(column % 2 == 1) || (column == columnCount - 1);
final bool startLarge = (column % 2 == 1);
final bool lowerStart = (column % 2 == 1);
return DesktopProductCardColumn(
alignToEnd: alignToEnd,
startLarge: startLarge,
lowerStart: lowerStart,
products: productCardLists[column],
largeImageWidth: largeImageWidth,
smallImageWidth: smallImageWidth,
);
},
);
return ListView(
scrollDirection: Axis.vertical,
children: [
Container(height: 60),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Spacer(),
...List<Widget>.generate(
2 * columnCount - 1,
(generalizedColumnIndex) {
if (generalizedColumnIndex % 2 == 0) {
return productCardColumns[generalizedColumnIndex ~/ 2];
} else {
return _gap;
}
},
),
Spacer(),
],
),
Container(height: 60),
],
physics: const AlwaysScrollableScrollPhysics(),
);
}
}

View File

@@ -0,0 +1,298 @@
// Copyright 2019 The Flutter team. 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 'package:gallery/studies/shrine/model/product.dart';
import 'package:gallery/studies/shrine/supplemental/desktop_product_columns.dart';
import 'package:gallery/studies/shrine/supplemental/layout_cache.dart';
/// A placeholder id for an empty element. See [_iterateUntilBalanced]
/// for more information.
const _emptyElement = -1;
/// To avoid infinite loops, improvements to the layout are only performed
/// when a column's height changes by more than
/// [_deviationImprovementThreshold] pixels.
const _deviationImprovementThreshold = 10;
/// Height of a product image, paired with the product's id.
class _TaggedHeightData {
const _TaggedHeightData({
@required this.index,
@required this.height,
});
/// The id of the corresponding product.
final int index;
/// The height of the product image.
final double height;
}
/// Converts a set of [_TaggedHeightData] elements to a list,
/// and add an empty element.
/// Used for iteration.
List<_TaggedHeightData> toListAndAddEmpty(Set<_TaggedHeightData> set) {
List<_TaggedHeightData> result = List<_TaggedHeightData>.from(set);
result.add(_TaggedHeightData(index: _emptyElement, height: 0));
return result;
}
/// Encode parameters for caching.
String _encodeParameters({
@required int columnCount,
@required List<Product> products,
@required double largeImageWidth,
@required double smallImageWidth,
}) {
final String productString =
[for (final product in products) product.id.toString()].join(',');
return '$columnCount;$productString,$largeImageWidth,$smallImageWidth';
}
/// Given a layout, replace integers by their corresponding products.
List<List<Product>> _generateLayout({
@required List<Product> products,
@required List<List<int>> layout,
}) {
return [
for (final column in layout)
[
for (final index in column) products[index],
]
];
}
/// Returns the size of an [Image] widget.
Size _imageSize(Image imageWidget) {
Size result;
imageWidget.image.resolve(ImageConfiguration()).addListener(
ImageStreamListener(
(info, synchronousCall) {
final finalImage = info.image;
result = Size(
finalImage.width.toDouble(),
finalImage.height.toDouble(),
);
},
),
);
return result;
}
/// Given [columnObjects], list of the set of objects in each column,
/// and [columnHeights], list of heights of each column,
/// [_iterateUntilBalanced] moves and swaps objects between columns
/// until their heights are sufficiently close to each other.
/// This prevents the layout having significant, avoidable gaps at the bottom.
void _iterateUntilBalanced(
List<Set<_TaggedHeightData>> columnObjects,
List<double> columnHeights,
) {
int failedMoves = 0;
final int columnCount = columnObjects.length;
// No need to rearrange a 1-column layout.
if (columnCount == 1) {
return;
}
while (true) {
// Loop through all possible 2-combinations of columns.
for (int source = 0; source < columnCount; ++source) {
for (int target = source + 1; target < columnCount; ++target) {
// Tries to find an object A from source column
// and an object B from target column, such that switching them
// causes the height of the two columns to be closer.
// A or B can be empty; in this case, moving an object from one
// column to the other is the best choice.
bool success = false;
final double bestHeight =
(columnHeights[source] + columnHeights[target]) / 2;
final double scoreLimit = (columnHeights[source] - bestHeight).abs();
final List<_TaggedHeightData> sourceObjects =
toListAndAddEmpty(columnObjects[source]);
final List<_TaggedHeightData> targetObjects =
toListAndAddEmpty(columnObjects[target]);
_TaggedHeightData bestA, bestB;
double bestScore;
for (final a in sourceObjects) {
for (final b in targetObjects) {
if (a.index == _emptyElement && b.index == _emptyElement) {
continue;
} else {
final double score =
(columnHeights[source] - a.height + b.height - bestHeight)
.abs();
if (score < scoreLimit - _deviationImprovementThreshold) {
success = true;
if (bestScore == null || score < bestScore) {
bestScore = score;
bestA = a;
bestB = b;
}
}
}
}
}
if (!success) {
++failedMoves;
} else {
failedMoves = 0;
// Switch A and B.
if (bestA.index != _emptyElement) {
columnObjects[source].remove(bestA);
columnObjects[target].add(bestA);
}
if (bestB.index != _emptyElement) {
columnObjects[target].remove(bestB);
columnObjects[source].add(bestB);
}
columnHeights[source] += bestB.height - bestA.height;
columnHeights[target] += bestA.height - bestB.height;
}
// If no two columns' heights can be made closer by switching
// elements, the layout is sufficiently balanced.
if (failedMoves >= columnCount * (columnCount - 1) ~/ 2) {
return;
}
}
}
}
}
/// Given a list of numbers [data], representing the heights of each image,
/// and a list of numbers [biases], representing the heights of the space
/// above each column, [_balancedDistribution] returns a layout of [data]
/// so that the height of each column is sufficiently close to each other,
/// represented as a list of lists of integers, each integer being an ID
/// for a product.
List<List<int>> _balancedDistribution({
int columnCount,
List<double> data,
List<double> biases,
}) {
assert(biases.length == columnCount);
List<Set<_TaggedHeightData>> columnObjects =
List<Set<_TaggedHeightData>>.generate(columnCount, (column) => Set());
List<double> columnHeights = List<double>.from(biases);
for (var i = 0; i < data.length; ++i) {
final int column = i % columnCount;
columnHeights[column] += data[i];
columnObjects[column].add(_TaggedHeightData(index: i, height: data[i]));
}
_iterateUntilBalanced(columnObjects, columnHeights);
return [
for (final column in columnObjects)
[for (final object in column) object.index]..sort(),
];
}
/// Generates a balanced layout for [columnCount] columns,
/// with products specified by the list [products],
/// where the larger images have width [largeImageWidth]
/// and the smaller images have width [smallImageWidth].
/// The current [context] is also given to allow caching.
List<List<Product>> balancedLayout({
BuildContext context,
int columnCount,
List<Product> products,
double largeImageWidth,
double smallImageWidth,
}) {
final String encodedParameters = _encodeParameters(
columnCount: columnCount,
products: products,
largeImageWidth: largeImageWidth,
smallImageWidth: smallImageWidth,
);
// Check if this layout is cached.
if (LayoutCache.of(context).containsKey(encodedParameters)) {
return _generateLayout(
products: products,
layout: LayoutCache.of(context)[encodedParameters],
);
}
final List<Size> productSizes = [
for (var product in products)
_imageSize(
Image.asset(
product.assetName,
package: product.assetPackage,
),
),
];
bool hasNullSize = false;
for (final productSize in productSizes) {
if (productSize == null) {
hasNullSize = true;
break;
}
}
if (hasNullSize) {
// If some image sizes are not read, return default layout.
// Default layout is not cached.
List<List<Product>> result =
List<List<Product>>.generate(columnCount, (columnIndex) => []);
for (var index = 0; index < products.length; ++index) {
result[index % columnCount].add(products[index]);
}
return result;
}
// All images have sizes. Use tailored layout.
final List<double> productHeights = [
for (final productSize in productSizes)
productSize.height /
productSize.width *
(largeImageWidth + smallImageWidth) /
2 +
productCardAdditionalHeight,
];
final List<List<int>> layout = _balancedDistribution(
columnCount: columnCount,
data: productHeights,
biases: List<double>.generate(
columnCount,
(column) => (column % 2 == 0 ? 0 : columnTopSpace),
),
);
// Add tailored layout to cache.
LayoutCache.of(context)[encodedParameters] = layout;
final List<List<Product>> result = _generateLayout(
products: products,
layout: layout,
);
return result;
}

View File

@@ -0,0 +1,129 @@
// Copyright 2019 The Flutter team. 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:ui' show lerpDouble;
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class CutCornersBorder extends OutlineInputBorder {
const CutCornersBorder({
BorderSide borderSide = const BorderSide(),
BorderRadius borderRadius = const BorderRadius.all(Radius.circular(2)),
this.cut = 7,
double gapPadding = 2,
}) : super(
borderSide: borderSide,
borderRadius: borderRadius,
gapPadding: gapPadding,
);
@override
CutCornersBorder copyWith({
BorderSide borderSide,
BorderRadius borderRadius,
double gapPadding,
double cut,
}) {
return CutCornersBorder(
borderSide: borderSide ?? this.borderSide,
borderRadius: borderRadius ?? this.borderRadius,
gapPadding: gapPadding ?? this.gapPadding,
cut: cut ?? this.cut,
);
}
final double cut;
@override
ShapeBorder lerpFrom(ShapeBorder a, double t) {
if (a is CutCornersBorder) {
final CutCornersBorder outline = a;
return CutCornersBorder(
borderRadius: BorderRadius.lerp(outline.borderRadius, borderRadius, t),
borderSide: BorderSide.lerp(outline.borderSide, borderSide, t),
cut: cut,
gapPadding: outline.gapPadding,
);
}
return super.lerpFrom(a, t);
}
@override
ShapeBorder lerpTo(ShapeBorder b, double t) {
if (b is CutCornersBorder) {
final CutCornersBorder outline = b;
return CutCornersBorder(
borderRadius: BorderRadius.lerp(borderRadius, outline.borderRadius, t),
borderSide: BorderSide.lerp(borderSide, outline.borderSide, t),
cut: cut,
gapPadding: outline.gapPadding,
);
}
return super.lerpTo(b, t);
}
Path _notchedCornerPath(Rect center, [double start = 0, double extent = 0]) {
final Path path = Path();
if (start > 0 || extent > 0) {
path.relativeMoveTo(extent + start, center.top);
_notchedSidesAndBottom(center, path);
path..lineTo(center.left + cut, center.top)..lineTo(start, center.top);
} else {
path.moveTo(center.left + cut, center.top);
_notchedSidesAndBottom(center, path);
path.lineTo(center.left + cut, center.top);
}
return path;
}
Path _notchedSidesAndBottom(Rect center, Path path) {
return path
..lineTo(center.right - cut, center.top)
..lineTo(center.right, center.top + cut)
..lineTo(center.right, center.top + center.height - cut)
..lineTo(center.right - cut, center.top + center.height)
..lineTo(center.left + cut, center.top + center.height)
..lineTo(center.left, center.top + center.height - cut)
..lineTo(center.left, center.top + cut);
}
@override
void paint(
Canvas canvas,
Rect rect, {
double gapStart,
double gapExtent = 0,
double gapPercentage = 0,
TextDirection textDirection,
}) {
assert(gapExtent != null);
assert(gapPercentage >= 0 && gapPercentage <= 1);
final Paint paint = borderSide.toPaint();
final RRect outer = borderRadius.toRRect(rect);
if (gapStart == null || gapExtent <= 0 || gapPercentage == 0) {
canvas.drawPath(_notchedCornerPath(outer.middleRect), paint);
} else {
final double extent =
lerpDouble(0.0, gapExtent + gapPadding * 2, gapPercentage);
switch (textDirection) {
case TextDirection.rtl:
{
final Path path = _notchedCornerPath(
outer.middleRect, gapStart + gapPadding - extent, extent);
canvas.drawPath(path, paint);
break;
}
case TextDirection.ltr:
{
final Path path = _notchedCornerPath(
outer.middleRect, gapStart - gapPadding, extent);
canvas.drawPath(path, paint);
break;
}
}
}
}
}

View File

@@ -0,0 +1,82 @@
// Copyright 2019 The Flutter team. 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';
import 'package:flutter/material.dart';
import 'package:gallery/studies/shrine/model/product.dart';
import 'package:gallery/studies/shrine/supplemental/product_card.dart';
/// Height of the text below each product card.
const productCardAdditionalHeight = 84.0 * 2;
/// Height of the divider between product cards.
const productCardDividerHeight = 84.0;
/// Height of the space at the top of every other column.
const columnTopSpace = 84.0;
class DesktopProductCardColumn extends StatelessWidget {
const DesktopProductCardColumn({
@required this.alignToEnd,
@required this.startLarge,
@required this.lowerStart,
@required this.products,
@required this.largeImageWidth,
@required this.smallImageWidth,
});
final List<Product> products;
final bool alignToEnd;
final bool startLarge;
final bool lowerStart;
final double largeImageWidth;
final double smallImageWidth;
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
final int currentColumnProductCount = products.length;
final int currentColumnWidgetCount =
max(2 * currentColumnProductCount - 1, 0);
return Container(
width: largeImageWidth,
child: Column(
crossAxisAlignment:
alignToEnd ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
if (lowerStart) Container(height: columnTopSpace),
...List<Widget>.generate(currentColumnWidgetCount, (index) {
Widget card;
if (index % 2 == 0) {
// This is a product.
final int productCardIndex = index ~/ 2;
card = DesktopProductCard(
product: products[productCardIndex],
imageWidth: startLarge
? ((productCardIndex % 2 == 0)
? largeImageWidth
: smallImageWidth)
: ((productCardIndex % 2 == 0)
? smallImageWidth
: largeImageWidth),
);
} else {
// This is just a divider.
card = Container(
height: productCardDividerHeight,
);
}
return RepaintBoundary(child: card);
}),
],
),
);
});
}
}

View File

@@ -0,0 +1,22 @@
// Copyright 2019 The Flutter team. 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';
class LayoutCache extends InheritedWidget {
const LayoutCache({
Key key,
@required this.layouts,
@required Widget child,
}) : super(key: key, child: child);
static Map<String, List<List<int>>> of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<LayoutCache>().layouts;
}
final Map<String, List<List<int>>> layouts;
@override
bool updateShouldNotify(LayoutCache old) => true;
}

View File

@@ -0,0 +1,134 @@
// Copyright 2019 The Flutter team. 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 'package:flutter/rendering.dart';
import 'package:gallery/l10n/gallery_localizations.dart';
import 'package:intl/intl.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:gallery/studies/shrine/model/app_state_model.dart';
import 'package:gallery/studies/shrine/model/product.dart';
import 'package:gallery/layout/adaptive.dart';
class MobileProductCard extends StatelessWidget {
const MobileProductCard({this.imageAspectRatio = 33 / 49, this.product})
: assert(imageAspectRatio == null || imageAspectRatio > 0);
final double imageAspectRatio;
final Product product;
static const double defaultTextBoxHeight = 65;
@override
Widget build(BuildContext context) {
return Semantics(
container: true,
button: true,
child: _buildProductCard(
context: context,
product: product,
imageAspectRatio: imageAspectRatio,
),
);
}
}
class DesktopProductCard extends StatelessWidget {
const DesktopProductCard({@required this.product, @required this.imageWidth});
final Product product;
final double imageWidth;
@override
Widget build(BuildContext context) {
return _buildProductCard(
context: context,
product: product,
imageWidth: imageWidth,
);
}
}
Widget _buildProductCard({
BuildContext context,
Product product,
double imageWidth,
double imageAspectRatio,
}) {
final bool isDesktop = isDisplayDesktop(context);
final NumberFormat formatter = NumberFormat.simpleCurrency(
decimalDigits: 0,
locale: Localizations.localeOf(context).toString(),
);
final ThemeData theme = Theme.of(context);
final Image imageWidget = Image.asset(
product.assetName,
package: product.assetPackage,
fit: BoxFit.cover,
width: isDesktop ? imageWidth : null,
excludeFromSemantics: true,
);
return ScopedModelDescendant<AppStateModel>(
builder: (context, child, model) {
return Semantics(
hint:
GalleryLocalizations.of(context).shrineScreenReaderProductAddToCart,
child: GestureDetector(
onTap: () {
model.addProductToCart(product.id);
},
child: child,
),
);
},
child: Stack(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
isDesktop
? imageWidget
: AspectRatio(
aspectRatio: imageAspectRatio,
child: imageWidget,
),
SizedBox(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 23),
Container(
width: imageWidth,
child: Text(
product == null ? '' : product.name(context),
style: theme.textTheme.button,
softWrap: true,
textAlign: TextAlign.center,
),
),
const SizedBox(height: 4),
Text(
product == null ? '' : formatter.format(product.price),
style: theme.textTheme.caption,
),
],
),
),
],
),
const Padding(
padding: EdgeInsets.all(16),
child: Icon(Icons.add_shopping_cart),
),
],
),
);
}

View File

@@ -0,0 +1,80 @@
// Copyright 2019 The Flutter team. 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 'package:gallery/studies/shrine/model/product.dart';
import 'package:gallery/studies/shrine/supplemental/product_card.dart';
class TwoProductCardColumn extends StatelessWidget {
const TwoProductCardColumn({
@required this.bottom,
this.top,
@required this.imageAspectRatio,
}) : assert(bottom != null);
static const double spacerHeight = 44;
static const double horizontalPadding = 28;
final Product bottom, top;
final double imageAspectRatio;
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
return ListView(
physics: const ClampingScrollPhysics(),
children: [
Padding(
padding: const EdgeInsetsDirectional.only(start: horizontalPadding),
child: top != null
? MobileProductCard(
imageAspectRatio: imageAspectRatio,
product: top,
)
: SizedBox(
height: spacerHeight,
),
),
const SizedBox(height: spacerHeight),
Padding(
padding: const EdgeInsetsDirectional.only(end: horizontalPadding),
child: MobileProductCard(
imageAspectRatio: imageAspectRatio,
product: bottom,
),
),
],
);
});
}
}
class OneProductCardColumn extends StatelessWidget {
const OneProductCardColumn({
this.product,
@required this.reverse,
});
final Product product;
// Whether the product column should align to the bottom.
final bool reverse;
@override
Widget build(BuildContext context) {
return ListView(
physics: const ClampingScrollPhysics(),
reverse: reverse,
children: [
const SizedBox(
height: 40,
),
MobileProductCard(
product: product,
),
],
);
}
}