// Copyright 2014 The Flutter 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'; import 'package:flutter/material.dart'; import '../../../data/gallery_options.dart'; import '../../../layout/text_scale.dart'; import '../category_menu_page.dart'; import '../model/product.dart'; import '../page_status.dart'; import 'balanced_layout.dart'; import 'desktop_product_columns.dart'; import 'product_card.dart'; import 'product_columns.dart'; const double _topPadding = 34.0; const double _bottomPadding = 44.0; const double _cardToScreenWidthRatio = 0.59; class MobileAsymmetricView extends StatelessWidget { const MobileAsymmetricView({super.key, required this.products}); final List products; List _buildColumns(BuildContext context, BoxConstraints constraints) { if (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.generate(_listItemCount(products.length), (int index) { double width = _cardToScreenWidthRatio * MediaQuery.widthOf(context); Widget column; if (index.isEven) { /// 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 SizedBox( 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 product in products) SizedBox( width: _cardToScreenWidthRatio * MediaQuery.widthOf(context), 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: (BuildContext context, Widget? child) => AnimatedBuilder( animation: PageStatus.of(context)!.menuController, builder: (BuildContext context, Widget? child) => ExcludeSemantics( excluding: !productPageIsVisible(context), child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return ListView( restorationId: 'product_page_list_view', scrollDirection: Axis.horizontal, padding: const EdgeInsetsDirectional.fromSTEB(0, _topPadding, 16, _bottomPadding), physics: const BouncingScrollPhysics(), children: _buildColumns(context, constraints), ); }, ), ), ), ); } } class DesktopAsymmetricView extends StatelessWidget { const DesktopAsymmetricView({super.key, required this.products}); final List 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 num paddingScaleFactor = textScaleFactor >= 1.5 ? 0.25 : 1; // Calculate number of columns final double sidebar = desktopCategoryMenuPageWidth(context: context); final num minimumBoundaryWidth = 84 * paddingScaleFactor; final double columnWidth = 186 * imageScaleFactor; final double columnGapWidth = 24 * imageScaleFactor; final double windowWidth = MediaQuery.widthOf(context); 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: (BuildContext context, Widget? child) => ExcludeSemantics( excluding: !productPageIsVisible(context), child: DesktopColumns( columnCount: columnCount, products: products, largeImageWidth: actualColumnWidth, smallImageWidth: columnCount > 1 ? columnWidth - columnGapWidth : actualColumnWidth, ), ), ); } } class DesktopColumns extends StatelessWidget { const DesktopColumns({ super.key, required this.columnCount, required this.products, required this.largeImageWidth, required this.smallImageWidth, }); final int columnCount; final List products; final double largeImageWidth; final double smallImageWidth; @override Widget build(BuildContext context) { final Widget gap = Container(width: 24); final List> productCardLists = balancedLayout( context: context, columnCount: columnCount, products: products, largeImageWidth: largeImageWidth, smallImageWidth: smallImageWidth, ); final productCardColumns = List.generate(columnCount, (int column) { final bool alignToEnd = column.isOdd || (column == columnCount - 1); final bool startLarge = column.isOdd; final bool lowerStart = column.isOdd; return DesktopProductCardColumn( alignToEnd: alignToEnd, startLarge: startLarge, lowerStart: lowerStart, products: productCardLists[column], largeImageWidth: largeImageWidth, smallImageWidth: smallImageWidth, ); }); return ListView( physics: const AlwaysScrollableScrollPhysics(), children: [ Container(height: 60), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Spacer(), ...List.generate(2 * columnCount - 1, (int generalizedColumnIndex) { if (generalizedColumnIndex.isEven) { return productCardColumns[generalizedColumnIndex ~/ 2]; } else { return gap; } }), const Spacer(), ], ), Container(height: 60), ], ); } }