shrine_order.dart 10.9 KB
Newer Older
1 2 3 4
// 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.

5
import 'package:flutter/foundation.dart';
6 7 8 9 10 11 12
import 'package:flutter/material.dart';

import '../shrine_demo.dart' show ShrinePageRoute;
import 'shrine_page.dart';
import 'shrine_theme.dart';
import 'shrine_types.dart';

13 14
// Displays the product title's, description, and order quantity dropdown.
class _ProductItem extends StatelessWidget {
15 16 17 18 19 20
  _ProductItem({
    Key key,
    @required this.product,
    @required this.quantity,
    @required this.onChanged,
  }) : super(key: key) {
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 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
    assert(product != null);
    assert(quantity != null);
    assert(onChanged != null);
  }

  final Product product;
  final int quantity;
  final ValueChanged<int> onChanged;

  @override
  Widget build(BuildContext context) {
    final ShrineTheme theme = ShrineTheme.of(context);
    return new Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: <Widget>[
        new Text(product.name, style: theme.featureTitleStyle),
        const SizedBox(height: 24.0),
        new Text(product.description, style: theme.featureStyle),
        const SizedBox(height: 16.0),
        new Padding(
          padding: const EdgeInsets.only(top: 8.0, bottom: 8.0, right: 88.0),
          child: new DropdownButtonHideUnderline(
            child: new Container(
              decoration: new BoxDecoration(
                border: new Border.all(
                  color: const Color(0xFFD9D9D9),
                ),
              ),
              child: new DropdownButton<int>(
                items: <int>[0, 1, 2, 3, 4, 5].map((int value) {
                  return new DropdownMenuItem<int>(
                    value: value,
                    child: new Padding(
                      padding: const EdgeInsets.only(left: 8.0),
                      child: new Text('Quantity $value', style: theme.quantityMenuStyle),
                    ),
                  );
                }).toList(),
                value: quantity,
                onChanged: onChanged,
              ),
            ),
          ),
        ),
      ],
    );
  }
}

// Vendor name and description
class _VendorItem extends StatelessWidget {
73
  _VendorItem({ Key key, @required this.vendor }) : super(key: key) {
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 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 137 138 139 140 141 142 143
    assert(vendor != null);
  }

  final Vendor vendor;

  @override
  Widget build(BuildContext context) {
    final ShrineTheme theme = ShrineTheme.of(context);
    return new Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: <Widget>[
        new SizedBox(
          height: 24.0,
          child: new Align(
            alignment: FractionalOffset.bottomLeft,
            child: new Text(vendor.name, style: theme.vendorTitleStyle),
          ),
        ),
        const SizedBox(height: 16.0),
        new Text(vendor.description, style: theme.vendorStyle),
      ],
    );
  }
}

// Layout the order page's heading: the product's image, the
// title/description/dropdown product item, and the vendor item.
class _HeadingLayout extends MultiChildLayoutDelegate {
  _HeadingLayout();

  static final String image = 'image';
  static final String icon = 'icon';
  static final String product = 'product';
  static final String vendor = 'vendor';

  @override
  void performLayout(Size size) {
    const double margin = 56.0;
    final bool landscape = size.width > size.height;
    final double imageWidth = (landscape ? size.width / 2.0 : size.width) - margin * 2.0;
    final BoxConstraints imageConstraints = new BoxConstraints(maxHeight: 224.0, maxWidth: imageWidth);
    final Size imageSize = layoutChild(image, imageConstraints);
    final double imageY = 0.0;
    positionChild(image, new Offset(margin, imageY));

    final double productWidth = landscape ? size.width / 2.0 : size.width - margin;
    final BoxConstraints productConstraints = new BoxConstraints(maxWidth: productWidth);
    final Size productSize = layoutChild(product, productConstraints);
    final double productX = landscape ? size.width / 2.0 : margin;
    final double productY = landscape ? 0.0 : imageY + imageSize.height + 16.0;
    positionChild(product, new Offset(productX, productY));

    final Size iconSize = layoutChild(icon, new BoxConstraints.loose(size));
    positionChild(icon, new Offset(productX - iconSize.width - 16.0, productY + 8.0));

    final double vendorWidth = landscape ? size.width - margin : productWidth;
    layoutChild(vendor, new BoxConstraints(maxWidth: vendorWidth));
    final double vendorX = landscape ? margin : productX;
    final double vendorY = productY + productSize.height + 16.0;
    positionChild(vendor, new Offset(vendorX, vendorY));
  }

  @override
  bool shouldRelayout(_HeadingLayout oldDelegate) => true;
}

// Describes a product and vendor in detail, supports specifying
// a order quantity (0-5). Appears at the top of the OrderPage.
class _Heading extends StatelessWidget {
144 145 146 147 148 149
  _Heading({
    Key key,
    @required this.product,
    @required this.quantity,
    this.quantityChanged,
  }) : super(key: key) {
150 151 152 153 154 155 156 157 158 159
    assert(product != null);
    assert(quantity != null && quantity >= 0 && quantity <= 5);
  }

  final Product product;
  final int quantity;
  final ValueChanged<int> quantityChanged;

  @override
  Widget build(BuildContext context) {
160 161 162 163 164 165 166 167 168 169 170 171 172
    final Size screenSize = MediaQuery.of(context).size;
    return new SizedBox(
      height: (screenSize.height - kToolbarHeight) * 1.35,
      child: new Material(
        type: MaterialType.card,
        elevation: 0,
        child: new Padding(
          padding: const EdgeInsets.only(left: 16.0, top: 18.0, right: 16.0, bottom: 24.0),
          child: new CustomMultiChildLayout(
            delegate: new _HeadingLayout(),
            children: <Widget>[
              new LayoutId(
                id: _HeadingLayout.image,
173
                child: new Hero(
174
                  tag: product.tag,
175 176 177 178 179
                  child: new Image.asset(
                    product.imageAsset,
                    fit: BoxFit.contain,
                    alignment: FractionalOffset.center,
                  ),
Adam Barth's avatar
Adam Barth committed
180 181
                ),
              ),
182 183
              new LayoutId(
                id: _HeadingLayout.icon,
184
                child: const Icon(
185 186 187
                  Icons.info_outline,
                  size: 24.0,
                  color: const Color(0xFFFFE0E0),
188
                ),
189 190 191 192 193 194 195
              ),
              new LayoutId(
                id: _HeadingLayout.product,
                child: new _ProductItem(
                  product: product,
                  quantity: quantity,
                  onChanged: quantityChanged,
Adam Barth's avatar
Adam Barth committed
196 197
                ),
              ),
198 199 200 201 202 203
              new LayoutId(
                id: _HeadingLayout.vendor,
                child: new _VendorItem(vendor: product.vendor),
              ),
            ],
          ),
Adam Barth's avatar
Adam Barth committed
204 205
        ),
      ),
206 207 208 209 210
    );
  }
}

class OrderPage extends StatefulWidget {
211 212 213 214 215 216
  OrderPage({
    Key key,
    @required this.order,
    @required this.products,
    @required this.shoppingCart,
  }) : super(key: key) {
217
    assert(order != null);
218
    assert(products != null && products.isNotEmpty);
219
    assert(shoppingCart != null);
220 221 222 223
  }

  final Order order;
  final List<Product> products;
224
  final Map<Product, Order> shoppingCart;
225 226 227 228 229

  @override
  _OrderPageState createState() => new _OrderPageState();
}

230 231 232
// Displays a product's heading above photos of all of the other products
// arranged in two columns. Enables the user to specify a quantity and add an
// order to the shopping cart.
233
class _OrderPageState extends State<OrderPage> {
234 235 236 237 238
  GlobalKey<ScaffoldState> scaffoldKey;

  @override
  void initState() {
    super.initState();
239
    scaffoldKey = new GlobalKey<ScaffoldState>(debugLabel: 'Shrine Order ${widget.order}');
240
  }
241 242 243 244 245 246 247 248

  Order get currentOrder => ShrineOrderRoute.of(context).order;

  set currentOrder(Order value) {
    ShrineOrderRoute.of(context).order = value;
  }

  void updateOrder({ int quantity, bool inCart }) {
249
    final Order newOrder = currentOrder.copyWith(quantity: quantity, inCart: inCart);
250 251
    if (currentOrder != newOrder) {
      setState(() {
252
        widget.shoppingCart[newOrder.product] = newOrder;
253 254 255 256 257 258 259 260 261 262 263 264 265
        currentOrder = newOrder;
      });
    }
  }

  void showSnackBarMessage(String message) {
    scaffoldKey.currentState.showSnackBar(new SnackBar(content: new Text(message)));
  }

  @override
  Widget build(BuildContext context) {
    return new ShrinePage(
      scaffoldKey: scaffoldKey,
266 267
      products: widget.products,
      shoppingCart: widget.shoppingCart,
268 269 270
      floatingActionButton: new FloatingActionButton(
        onPressed: () {
          updateOrder(inCart: true);
271
          final int n = currentOrder.quantity;
272
          final String item = currentOrder.product.name;
273
          showSnackBarMessage(
274
            'There ${ n == 1 ? "is one $item item" : "are $n $item items" } in the shopping cart.'
275
          );
276 277
        },
        backgroundColor: const Color(0xFF16F0F0),
278
        child: const Icon(
Ian Hickson's avatar
Ian Hickson committed
279
          Icons.add_shopping_cart,
Adam Barth's avatar
Adam Barth committed
280 281
          color: Colors.black,
        ),
282
      ),
Adam Barth's avatar
Adam Barth committed
283 284
      body: new CustomScrollView(
        slivers: <Widget>[
285 286
          new SliverToBoxAdapter(
            child: new _Heading(
287
              product: widget.order.product,
288 289 290
              quantity: currentOrder.quantity,
              quantityChanged: (int value) { updateOrder(quantity: value); },
            ),
291
          ),
Adam Barth's avatar
Adam Barth committed
292
          new SliverPadding(
293
            padding: const EdgeInsets.fromLTRB(8.0, 32.0, 8.0, 8.0),
294
            sliver: new SliverGrid(
295 296
              gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
                maxCrossAxisExtent: 248.0,
Adam Barth's avatar
Adam Barth committed
297 298 299 300
                mainAxisSpacing: 8.0,
                crossAxisSpacing: 8.0,
              ),
              delegate: new SliverChildListDelegate(
301 302
                widget.products
                  .where((Product product) => product != widget.order.product)
Adam Barth's avatar
Adam Barth committed
303 304 305 306 307
                  .map((Product product) {
                    return new Card(
                      elevation: 1,
                      child: new Image.asset(
                        product.imageAsset,
308
                        fit: BoxFit.contain,
Adam Barth's avatar
Adam Barth committed
309 310 311 312 313 314 315 316
                      ),
                    );
                  }).toList(),
              ),
            ),
          ),
        ],
      ),
317 318 319 320
    );
  }
}

321 322 323 324 325
// Displays a full-screen modal OrderPage.
//
// The order field will be replaced each time the user reconfigures the order.
// When the user backs out of this route the completer's value will be the
// final value of the order field.
326 327 328 329
class ShrineOrderRoute extends ShrinePageRoute<Order> {
  ShrineOrderRoute({
    this.order,
    WidgetBuilder builder,
Adam Barth's avatar
Adam Barth committed
330
    RouteSettings settings: const RouteSettings(),
331
  }) : super(builder: builder, settings: settings) {
332 333 334 335 336 337 338 339 340 341
    assert(order != null);
  }

  Order order;

  @override
  Order get currentResult => order;

  static ShrineOrderRoute of(BuildContext context) => ModalRoute.of(context);
}