shrine_home.dart 11.3 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// 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';

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

15
const double unitSize = kToolbarHeight;
16

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

20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
// The Shrine home page arranges the product cards into two columns. The card
// on every 4th and 5th row spans two columns.
class ShrineGridDelegate extends GridDelegate {
  int _rowAtIndex(int index) {
    final int n = index ~/ 8;
    return const <int>[0, 0, 1, 1, 2, 2, 3, 4][index - n * 8] + n * 5;
  }

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

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

  @override
  GridSpecification getGridSpecification(BoxConstraints constraints, int childCount) {
    assert(childCount >= 0);
    return new GridSpecification.fromRegularTiles(
      tileWidth: constraints.maxWidth / 2.0 - 8.0,
41 42
      // height = ProductPriceItem + product image + VendorItem
      tileHeight: 40.0 + 144.0 + 40.0,
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
      columnCount: 2,
      rowCount: childCount == 0 ? 0 : _rowAtIndex(childCount - 1) + 1,
      rowSpacing: 8.0,
      columnSpacing: 8.0
    );
  }

  @override
  GridChildPlacement getChildPlacement(GridSpecification specification, int index, Object placementData) {
    assert(index >= 0);
    return new GridChildPlacement(
      column: _columnAtIndex(index),
      row: _rowAtIndex(index),
      columnSpan: _columnSpanAtIndex(index),
      rowSpan: 1
    );
  }
}

62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
/// Displays the Vendor's name and avatar.
class VendorItem extends StatelessWidget {
  VendorItem({ Key key, this.vendor }) : super(key: key) {
    assert(vendor != null);
  }

  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(
79
              borderRadius: new BorderRadius.circular(12.0),
80
              child: new Image.asset(vendor.avatarAsset, fit: ImageFit.cover)
81 82 83 84 85 86 87 88 89 90 91 92 93 94
            )
          ),
          new SizedBox(width: 8.0),
          new Flexible(
            child: new Text(vendor.name, style: ShrineTheme.of(context).vendorItemStyle)
          )
        ]
      )
    );
  }
}

/// Displays the product's price. If the product is in the shopping cart the background
/// is highlighted.
Hans Muller's avatar
Hans Muller committed
95
abstract class PriceItem extends StatelessWidget {
96 97 98 99 100 101
  PriceItem({ Key key, this.product }) : super(key: key) {
    assert(product != null);
  }

  final Product product;

Hans Muller's avatar
Hans Muller committed
102
  Widget buildItem(BuildContext context, TextStyle style, EdgeInsets padding) {
103
    BoxDecoration decoration;
104
    if (_shoppingCart[product] != null)
105
      decoration = new BoxDecoration(backgroundColor: ShrineTheme.of(context).priceHighlightColor);
106 107

    return new Container(
Hans Muller's avatar
Hans Muller committed
108
      padding: padding,
109
      decoration: decoration,
Hans Muller's avatar
Hans Muller committed
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
      child: new Text(product.priceString, style: style)
    );
  }
}

class ProductPriceItem extends PriceItem {
  ProductPriceItem({ Key key, Product product }) : super(key: key, product: product);

  @override
  Widget build(BuildContext context) {
    return buildItem(
      context,
      ShrineTheme.of(context).priceStyle,
      const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0)
    );
  }
}

class FeaturePriceItem extends PriceItem {
  FeaturePriceItem({ Key key, Product product }) : super(key: key, product: product);

  @override
  Widget build(BuildContext context) {
    return buildItem(
      context,
      ShrineTheme.of(context).featurePriceStyle,
      const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0)
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
    );
  }
}

/// Layout the main left and right elements of a FeatureItem.
class FeatureLayout extends MultiChildLayoutDelegate {
  FeatureLayout();

  static final String left = 'left';
  static final String right = 'right';

  // Horizontally: the feature product image appears on the left and
  // occupies 50% of the available width; the feature product's
  // description apepars on the right and occupies 50% of the available
  // width + unitSize. The left and right widgets overlap and the right
  // widget is stacked on top.
  @override
  void performLayout(Size size) {
    final double halfWidth = size.width / 2.0;
    layoutChild(left, new BoxConstraints.tightFor(width: halfWidth, height: size.height));
    positionChild(left, Offset.zero);
    layoutChild(right, new BoxConstraints.expand(width: halfWidth + unitSize, height: size.height));
    positionChild(right, new Offset(halfWidth - unitSize, 0.0));
  }

  @override
  bool shouldRelayout(FeatureLayout oldDelegate) => false;
}

/// A card that highlights the "featured" catalog item.
class FeatureItem extends StatelessWidget {
  FeatureItem({ Key key, this.product }) : super(key: key) {
    assert(product.featureTitle != null);
    assert(product.featureDescription != null);
  }

  final Product product;

  @override
  Widget build(BuildContext context) {
    final ShrineTheme theme = ShrineTheme.of(context);
    return new AspectRatio(
      aspectRatio: 3.0 / 3.5,
180 181 182 183 184
      child: new Container(
        decoration: new BoxDecoration(
          backgroundColor: theme.cardBackgroundColor,
          border: new Border(bottom: new BorderSide(color: theme.dividerColor))
        ),
185 186 187 188 189 190 191
        child: new Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            new SizedBox(
              height: unitSize,
              child: new Align(
                alignment: FractionalOffset.topRight,
Hans Muller's avatar
Hans Muller committed
192
                child: new FeaturePriceItem(product: product)
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
              )
            ),
            new Flexible(
              child: new CustomMultiChildLayout(
                delegate: new FeatureLayout(),
                children: <Widget>[
                  new LayoutId(
                    id: FeatureLayout.left,
                    child: new ClipRect(
                      child: new OverflowBox(
                        minWidth: 340.0,
                        maxWidth: 340.0,
                        minHeight: 340.0,
                        maxHeight: 340.0,
                        alignment: FractionalOffset.topRight,
208
                        child: new Image.asset(product.imageAsset, fit: ImageFit.cover)
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253
                      )
                    )
                  ),
                  new LayoutId(
                    id: FeatureLayout.right,
                    child: new Padding(
                      padding: const EdgeInsets.only(right: 16.0),
                      child: new Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: <Widget>[
                          new Padding(
                            padding: const EdgeInsets.only(top: 18.0),
                            child: new Text(product.featureTitle, style: theme.featureTitleStyle)
                          ),
                          new Padding(
                            padding: const EdgeInsets.symmetric(vertical: 16.0),
                            child: new Text(product.featureDescription, style: theme.featureStyle)
                          ),
                          new VendorItem(vendor: product.vendor)
                        ]
                      )
                    )
                  )
                ]
              )
            )
          ]
        )
      )
    );
  }
}

/// A card that displays a product's image, price, and vendor.
class ProductItem extends StatelessWidget {
  ProductItem({ Key key, this.product, this.onPressed }) : super(key: key) {
    assert(product != null);
  }

  final Product product;
  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    return new Card(
254
      child: new Stack(
Hans Muller's avatar
Hans Muller committed
255
        children: <Widget>[
256 257 258 259 260 261 262 263 264 265 266
          new Column(
            children: <Widget>[
              new Align(
                alignment: FractionalOffset.centerRight,
                child: new ProductPriceItem(product: product)
              ),
              new Container(
                width: 144.0,
                height: 144.0,
                padding: const EdgeInsets.symmetric(horizontal: 8.0),
                child: new Hero(
267
                    tag: product.tag,
268
                    child: new Image.asset(product.imageAsset, fit: ImageFit.contain)
Hans Muller's avatar
Hans Muller committed
269 270
                  )
                ),
271 272 273 274 275 276 277 278 279
              new Padding(
                padding: const EdgeInsets.symmetric(horizontal: 8.0),
                child: new VendorItem(vendor: product.vendor)
              )
            ]
          ),
          new Material(
            type: MaterialType.transparency,
            child: new InkWell(onTap: onPressed)
Hans Muller's avatar
Hans Muller committed
280 281
          ),
        ]
282 283 284 285 286 287 288 289 290 291 292 293 294
      )
    );
  }
}

/// The Shrine app's home page. Displays the featured item above all of the
/// product items arranged in two columns.
class ShrineHome extends StatefulWidget {
  @override
  _ShrineHomeState createState() => new _ShrineHomeState();
}

class _ShrineHomeState extends State<ShrineHome> {
295
  static final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>(debugLabel: 'Shrine Home');
296
  static final GlobalKey<ScrollableState> scrollableKey = new GlobalKey<ScrollableState>();
297
  static final GridDelegate gridDelegate = new ShrineGridDelegate();
298 299 300

  void handleCompletedOrder(Order completedOrder) {
    assert(completedOrder.product != null);
301 302
    if (completedOrder.quantity == 0)
      _shoppingCart.remove(completedOrder.product);
303 304 305
  }

  void showOrderPage(Product product) {
306
    final Order order = _shoppingCart[product] ?? new Order(product: product);
307 308 309 310 311 312 313
    final Completer<Order> completer = new Completer<Order>();
    Navigator.push(context, new ShrineOrderRoute(
      order: order,
      completer: completer,
      builder: (BuildContext context) {
        return new OrderPage(
          order: order,
314 315
          products: _products,
          shoppingCart: _shoppingCart
316 317 318 319 320 321 322 323 324 325
        );
      }
    ));
    completer.future.then(handleCompletedOrder);
  }

  @override
  Widget build(BuildContext context) {
    final Product featured = _products.firstWhere((Product product) => product.featureDescription != null);
    return new ShrinePage(
326
      scaffoldKey: scaffoldKey,
327
      scrollableKey: scrollableKey,
328 329
      products: _products,
      shoppingCart: _shoppingCart,
330
      body: new ScrollableViewport(
331
        scrollableKey: scrollableKey,
332 333 334
        child: new RepaintBoundary(
          child: new Column(
            children: <Widget>[
335 336 337 338 339 340 341 342 343 344 345 346 347 348
              new FeatureItem(product: featured),
              new Padding(
                padding: const EdgeInsets.all(16.0),
                child: new CustomGrid(
                  delegate: gridDelegate,
                  children: _products.map((Product product) {
                    return new RepaintBoundary(
                      child: new ProductItem(
                        product: product,
                        onPressed: () { showOrderPage(product); }
                      )
                    );
                  }).toList()
                )
349 350 351
              )
            ]
          )
352 353 354 355 356
        )
      )
    );
  }
}