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:
295
gallery/lib/studies/shrine/supplemental/asymmetric_view.dart
Normal file
295
gallery/lib/studies/shrine/supplemental/asymmetric_view.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
298
gallery/lib/studies/shrine/supplemental/balanced_layout.dart
Normal file
298
gallery/lib/studies/shrine/supplemental/balanced_layout.dart
Normal 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;
|
||||
}
|
||||
129
gallery/lib/studies/shrine/supplemental/cut_corners_border.dart
Normal file
129
gallery/lib/studies/shrine/supplemental/cut_corners_border.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
22
gallery/lib/studies/shrine/supplemental/layout_cache.dart
Normal file
22
gallery/lib/studies/shrine/supplemental/layout_cache.dart
Normal 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;
|
||||
}
|
||||
134
gallery/lib/studies/shrine/supplemental/product_card.dart
Normal file
134
gallery/lib/studies/shrine/supplemental/product_card.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
80
gallery/lib/studies/shrine/supplemental/product_columns.dart
Normal file
80
gallery/lib/studies/shrine/supplemental/product_columns.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user