shrine_home.dart 13 KB
Newer Older
1 2 3 4 5 6 7
// Copyright 2016 The Chromium 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:async';

import 'package:flutter/material.dart';
8 9
import 'package:flutter/rendering.dart';
import 'package:meta/meta.dart';
10 11 12 13 14 15 16

import 'shrine_data.dart';
import 'shrine_order.dart';
import 'shrine_page.dart';
import 'shrine_theme.dart';
import 'shrine_types.dart';

17
const double unitSize = kToolbarHeight;
18

19 20
final List<Product> _products = new List<Product>.from(allProducts());
final Map<Product, Order> _shoppingCart = <Product, Order>{};
21

22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
const int _childrenPerBlock = 8;
const int _rowsPerBlock = 5;

int _minIndexInRow(int rowIndex) {
  final int blockIndex = rowIndex ~/ _rowsPerBlock;
  return const <int>[0, 2, 4, 6, 7][rowIndex % _rowsPerBlock] + blockIndex * _childrenPerBlock;
}

int _maxIndexInRow(int rowIndex) {
  final int blockIndex = rowIndex ~/ _rowsPerBlock;
  return const <int>[1, 3, 5, 6, 7][rowIndex % _rowsPerBlock] + blockIndex * _childrenPerBlock;
}

int _rowAtIndex(int index) {
  final int blockCount = index ~/ _childrenPerBlock;
  return const <int>[0, 0, 1, 1, 2, 2, 3, 4][index - blockCount * _childrenPerBlock] + blockCount * _rowsPerBlock;
}

int _columnAtIndex(int index) {
  return const <int>[0, 1, 0, 1, 0, 1, 0, 0][index % _childrenPerBlock];
}

int _columnSpanAtIndex(int index) {
  return const <int>[1, 1, 1, 1, 1, 1, 2, 2][index % _childrenPerBlock];
}

48 49
// The Shrine home page arranges the product cards into two columns. The card
// on every 4th and 5th row spans two columns.
50 51
class _ShrineGridLayout extends SliverGridLayout {
  const _ShrineGridLayout({
52 53 54 55 56 57 58 59 60 61
    @required this.rowStride,
    @required this.columnStride,
    @required this.tileHeight,
    @required this.tileWidth,
  });

  final double rowStride;
  final double columnStride;
  final double tileHeight;
  final double tileWidth;
62

63 64 65
  @override
  int getMinChildIndexForScrollOffset(double scrollOffset) {
    return _minIndexInRow(scrollOffset ~/ rowStride);
66 67
  }

68 69 70
  @override
  int getMaxChildIndexForScrollOffset(double scrollOffset) {
    return _maxIndexInRow(scrollOffset ~/ rowStride);
71 72 73
  }

  @override
74 75 76 77 78 79 80 81 82
  SliverGridGeometry getGeometryForChildIndex(int index) {
    final int row = _rowAtIndex(index);
    final int column = _columnAtIndex(index);
    final int columnSpan = _columnSpanAtIndex(index);
    return new SliverGridGeometry(
      scrollOffset: row * rowStride,
      crossAxisOffset: column * columnStride,
      mainAxisExtent: tileHeight,
      crossAxisExtent: tileWidth + (columnSpan - 1) * columnStride,
83 84 85 86
    );
  }

  @override
87
  double computeMaxScrollOffset(int childCount) {
88 89 90 91 92 93 94 95
    if (childCount == 0)
      return 0.0;
    final int rowCount = _rowAtIndex(childCount - 1) + 1;
    final double rowSpacing = rowStride - tileHeight;
    return rowStride * rowCount - rowSpacing;
  }
}

96
class _ShrineGridDelegate extends SliverGridDelegate {
97
  static const double _spacing = 8.0;
98 99 100

  @override
  SliverGridLayout getLayout(SliverConstraints constraints) {
101
    final double tileWidth = (constraints.crossAxisExtent - _spacing) / 2.0;
102
    const double tileHeight = 40.0 + 144.0 + 40.0;
103
    return new _ShrineGridLayout(
104 105
      tileWidth: tileWidth,
      tileHeight: tileHeight,
106 107
      rowStride: tileHeight + _spacing,
      columnStride: tileWidth + _spacing,
108 109
    );
  }
110 111

  @override
112
  bool shouldRelayout(covariant SliverGridDelegate oldDelegate) => false;
113 114
}

115 116
// Displays the Vendor's name and avatar.
class _VendorItem extends StatelessWidget {
117
  const _VendorItem({ Key key, @required this.vendor })
118 119
    : assert(vendor != null),
      super(key: key);
120 121 122 123 124 125 126 127 128 129 130 131

  final Vendor vendor;

  @override
  Widget build(BuildContext context) {
    return new SizedBox(
      height: 24.0,
      child: new Row(
        children: <Widget>[
          new SizedBox(
            width: 24.0,
            child: new ClipRRect(
132
              borderRadius: new BorderRadius.circular(12.0),
133 134 135 136 137
              child: new Image.asset(
                vendor.avatarAsset,
                package: vendor.avatarAssetPackage,
                fit: BoxFit.cover,
              ),
138
            ),
139
          ),
140
          const SizedBox(width: 8.0),
141
          new Expanded(
142 143 144 145
            child: new Text(vendor.name, style: ShrineTheme.of(context).vendorItemStyle),
          ),
        ],
      ),
146 147 148 149
    );
  }
}

150 151 152
// Displays the product's price. If the product is in the shopping cart then the
// background is highlighted.
abstract class _PriceItem extends StatelessWidget {
153 154 155
  const _PriceItem({ Key key, @required this.product })
      : assert(product != null),
        super(key: key);
156 157 158

  final Product product;

Hans Muller's avatar
Hans Muller committed
159
  Widget buildItem(BuildContext context, TextStyle style, EdgeInsets padding) {
160
    BoxDecoration decoration;
161
    if (_shoppingCart[product] != null)
162
      decoration = new BoxDecoration(color: ShrineTheme.of(context).priceHighlightColor);
163 164

    return new Container(
Hans Muller's avatar
Hans Muller committed
165
      padding: padding,
166
      decoration: decoration,
167
      child: new Text(product.priceString, style: style),
Hans Muller's avatar
Hans Muller committed
168 169 170 171
    );
  }
}

172
class _ProductPriceItem extends _PriceItem {
173
  const _ProductPriceItem({ Key key, Product product }) : super(key: key, product: product);
Hans Muller's avatar
Hans Muller committed
174 175 176 177 178 179

  @override
  Widget build(BuildContext context) {
    return buildItem(
      context,
      ShrineTheme.of(context).priceStyle,
180
      const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
Hans Muller's avatar
Hans Muller committed
181 182 183 184
    );
  }
}

185
class _FeaturePriceItem extends _PriceItem {
186
  const _FeaturePriceItem({ Key key, Product product }) : super(key: key, product: product);
Hans Muller's avatar
Hans Muller committed
187 188 189 190 191 192

  @override
  Widget build(BuildContext context) {
    return buildItem(
      context,
      ShrineTheme.of(context).featurePriceStyle,
193
      const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
194 195 196 197
    );
  }
}

198 199
class _HeadingLayout extends MultiChildLayoutDelegate {
  _HeadingLayout();
200

201 202 203 204 205
  static const String price = 'price';
  static const String image = 'image';
  static const String title = 'title';
  static const String description = 'description';
  static const String vendor = 'vendor';
206 207 208

  @override
  void performLayout(Size size) {
209 210 211
    final Size priceSize = layoutChild(price, new BoxConstraints.loose(size));
    positionChild(price, new Offset(size.width - priceSize.width, 0.0));

212
    final double halfWidth = size.width / 2.0;
213
    final double halfHeight = size.height / 2.0;
214
    const double halfUnit = unitSize / 2.0;
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
    const double margin = 16.0;

    final Size imageSize = layoutChild(image, new BoxConstraints.loose(size));
    final double imageX = imageSize.width < halfWidth - halfUnit
      ? halfWidth / 2.0 - imageSize.width / 2.0 - halfUnit
      : halfWidth - imageSize.width;
    positionChild(image, new Offset(imageX, halfHeight - imageSize.height / 2.0));

    final double maxTitleWidth = halfWidth + unitSize - margin;
    final BoxConstraints titleBoxConstraints = new BoxConstraints(maxWidth: maxTitleWidth);
    final Size titleSize = layoutChild(title, titleBoxConstraints);
    final double titleX = halfWidth - unitSize;
    final double titleY = halfHeight - titleSize.height;
    positionChild(title, new Offset(titleX, titleY));

    final Size descriptionSize = layoutChild(description, titleBoxConstraints);
    final double descriptionY = titleY + titleSize.height + margin;
    positionChild(description, new Offset(titleX, descriptionY));

    layoutChild(vendor, titleBoxConstraints);
    final double vendorY = descriptionY + descriptionSize.height + margin;
    positionChild(vendor, new Offset(titleX, vendorY));
237 238 239
  }

  @override
240
  bool shouldRelayout(_HeadingLayout oldDelegate) => false;
241 242
}

243 244
// A card that highlights the "featured" catalog item.
class _Heading extends StatelessWidget {
245 246 247 248 249
  _Heading({ Key key, @required this.product })
    : assert(product != null),
      assert(product.featureTitle != null),
      assert(product.featureDescription != null),
      super(key: key);
250 251 252 253 254

  final Product product;

  @override
  Widget build(BuildContext context) {
255
    final Size screenSize = MediaQuery.of(context).size;
256
    final ShrineTheme theme = ShrineTheme.of(context);
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
    return new MergeSemantics(
      child: new SizedBox(
        height: screenSize.width > screenSize.height
          ? (screenSize.height - kToolbarHeight) * 0.85
          : (screenSize.height - kToolbarHeight) * 0.70,
        child: new Container(
          decoration: new BoxDecoration(
            color: theme.cardBackgroundColor,
            border: new Border(bottom: new BorderSide(color: theme.dividerColor)),
          ),
          child: new CustomMultiChildLayout(
            delegate: new _HeadingLayout(),
            children: <Widget>[
              new LayoutId(
                id: _HeadingLayout.price,
                child: new _FeaturePriceItem(product: product),
              ),
              new LayoutId(
                id: _HeadingLayout.image,
276 277 278 279 280
                child: new Image.asset(
                  product.imageAsset,
                  package: product.imageAssetPackage,
                  fit: BoxFit.cover,
                ),
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
              ),
              new LayoutId(
                id: _HeadingLayout.title,
                child: new Text(product.featureTitle, style: theme.featureTitleStyle),
              ),
              new LayoutId(
                id: _HeadingLayout.description,
                child: new Text(product.featureDescription, style: theme.featureStyle),
              ),
              new LayoutId(
                id: _HeadingLayout.vendor,
                child: new _VendorItem(vendor: product.vendor),
              ),
            ],
          ),
296 297
        ),
      ),
298 299 300 301
    );
  }
}

302 303 304
// A card that displays a product's image, price, and vendor. The _ProductItem
// cards appear in a grid below the heading.
class _ProductItem extends StatelessWidget {
305
  const _ProductItem({ Key key, @required this.product, this.onPressed })
306 307
    : assert(product != null),
      super(key: key);
308 309 310 311 312 313

  final Product product;
  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
314 315 316 317 318 319 320
    return new MergeSemantics(
      child: new Card(
        child: new Stack(
          children: <Widget>[
            new Column(
              children: <Widget>[
                new Align(
321
                  alignment: Alignment.centerRight,
322 323 324 325 326 327 328 329
                  child: new _ProductPriceItem(product: product),
                ),
                new Container(
                  width: 144.0,
                  height: 144.0,
                  padding: const EdgeInsets.symmetric(horizontal: 8.0),
                  child: new Hero(
                      tag: product.tag,
330 331 332 333 334
                      child: new Image.asset(
                        product.imageAsset,
                        package: product.imageAssetPackage,
                        fit: BoxFit.contain,
                      ),
335
                    ),
336
                  ),
337 338 339
                new Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 8.0),
                  child: new _VendorItem(vendor: product.vendor),
Hans Muller's avatar
Hans Muller committed
340
                ),
341 342 343 344 345 346 347 348
              ],
            ),
            new Material(
              type: MaterialType.transparency,
              child: new InkWell(onTap: onPressed),
            ),
          ],
        ),
349
      ),
350 351 352 353
    );
  }
}

354 355
// The Shrine app's home page. Displays the featured item above a grid
// of the product items.
356 357 358 359 360 361
class ShrineHome extends StatefulWidget {
  @override
  _ShrineHomeState createState() => new _ShrineHomeState();
}

class _ShrineHomeState extends State<ShrineHome> {
362 363
  static final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(debugLabel: 'Shrine Home');
  static final _ShrineGridDelegate gridDelegate = new _ShrineGridDelegate();
364

365
  Future<Null> _showOrderPage(Product product) async {
366
    final Order order = _shoppingCart[product] ?? new Order(product: product);
367
    final Order completedOrder = await Navigator.push(context, new ShrineOrderRoute(
368 369 370 371
      order: order,
      builder: (BuildContext context) {
        return new OrderPage(
          order: order,
372
          products: _products,
373
          shoppingCart: _shoppingCart,
374 375 376
        );
      }
    ));
377 378 379
    assert(completedOrder.product != null);
    if (completedOrder.quantity == 0)
      _shoppingCart.remove(completedOrder.product);
380 381 382 383 384 385
  }

  @override
  Widget build(BuildContext context) {
    final Product featured = _products.firstWhere((Product product) => product.featureDescription != null);
    return new ShrinePage(
386
      scaffoldKey: _scaffoldKey,
387 388
      products: _products,
      shoppingCart: _shoppingCart,
389 390
      body: new CustomScrollView(
        slivers: <Widget>[
391 392 393 394
          new SliverToBoxAdapter(child: new _Heading(product: featured)),
          new SliverSafeArea(
            top: false,
            minimum: const EdgeInsets.all(16.0),
395
            sliver: new SliverGrid(
396 397 398
              gridDelegate: gridDelegate,
              delegate: new SliverChildListDelegate(
                _products.map((Product product) {
399
                  return new _ProductItem(
400
                    product: product,
401
                    onPressed: () { _showOrderPage(product); },
402 403 404 405 406 407 408
                  );
                }).toList(),
              ),
            ),
          ),
        ],
      ),
409 410 411
    );
  }
}