mirror of
https://github.com/flutter/samples.git
synced 2025-11-12 07:48:55 +00:00
296 lines
9.4 KiB
Dart
296 lines
9.4 KiB
Dart
// 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(),
|
|
);
|
|
}
|
|
}
|