shrine_order.dart 11 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
  const _ProductItem({
16 17 18 19
    Key key,
    @required this.product,
    @required this.quantity,
    @required this.onChanged,
20 21 22 23
  }) : assert(product != null),
       assert(quantity != null),
       assert(onChanged != null),
       super(key: key);
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

  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 {
72
  const _VendorItem({ Key key, @required this.vendor })
73 74
    : assert(vendor != null),
      super(key: key);
75 76 77 78 79 80 81 82 83 84 85 86 87

  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(
88
            alignment: Alignment.bottomLeft,
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
            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 {
143
  const _Heading({
144 145 146 147
    Key key,
    @required this.product,
    @required this.quantity,
    this.quantityChanged,
148 149 150
  }) : assert(product != null),
       assert(quantity != null && quantity >= 0 && quantity <= 5),
       super(key: key);
151 152 153 154 155 156 157

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

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

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

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

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

228 229 230
// 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.
231
class _OrderPageState extends State<OrderPage> {
232 233 234 235 236
  GlobalKey<ScaffoldState> scaffoldKey;

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

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

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

  void updateOrder({ int quantity, bool inCart }) {
247
    final Order newOrder = currentOrder.copyWith(quantity: quantity, inCart: inCart);
248 249
    if (currentOrder != newOrder) {
      setState(() {
250
        widget.shoppingCart[newOrder.product] = newOrder;
251 252 253 254 255 256 257 258 259 260 261 262 263
        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,
264 265
      products: widget.products,
      shoppingCart: widget.shoppingCart,
266 267 268
      floatingActionButton: new FloatingActionButton(
        onPressed: () {
          updateOrder(inCart: true);
269
          final int n = currentOrder.quantity;
270
          final String item = currentOrder.product.name;
271
          showSnackBarMessage(
272
            'There ${ n == 1 ? "is one $item item" : "are $n $item items" } in the shopping cart.'
273
          );
274 275
        },
        backgroundColor: const Color(0xFF16F0F0),
276
        child: const Icon(
Ian Hickson's avatar
Ian Hickson committed
277
          Icons.add_shopping_cart,
Adam Barth's avatar
Adam Barth committed
278 279
          color: Colors.black,
        ),
280
      ),
Adam Barth's avatar
Adam Barth committed
281 282
      body: new CustomScrollView(
        slivers: <Widget>[
283 284
          new SliverToBoxAdapter(
            child: new _Heading(
285
              product: widget.order.product,
286 287 288
              quantity: currentOrder.quantity,
              quantityChanged: (int value) { updateOrder(quantity: value); },
            ),
289
          ),
Adam Barth's avatar
Adam Barth committed
290
          new SliverPadding(
291
            padding: const EdgeInsets.fromLTRB(8.0, 32.0, 8.0, 8.0),
292
            sliver: new SliverGrid(
293 294
              gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
                maxCrossAxisExtent: 248.0,
Adam Barth's avatar
Adam Barth committed
295 296 297 298
                mainAxisSpacing: 8.0,
                crossAxisSpacing: 8.0,
              ),
              delegate: new SliverChildListDelegate(
299 300
                widget.products
                  .where((Product product) => product != widget.order.product)
Adam Barth's avatar
Adam Barth committed
301 302
                  .map((Product product) {
                    return new Card(
303
                      elevation: 1.0,
Adam Barth's avatar
Adam Barth committed
304 305
                      child: new Image.asset(
                        product.imageAsset,
306
                        package: product.imageAssetPackage,
307
                        fit: BoxFit.contain,
Adam Barth's avatar
Adam Barth committed
308 309 310 311 312 313 314 315
                      ),
                    );
                  }).toList(),
              ),
            ),
          ),
        ],
      ),
316 317 318 319
    );
  }
}

320 321 322 323 324
// 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.
325 326
class ShrineOrderRoute extends ShrinePageRoute<Order> {
  ShrineOrderRoute({
327
    @required this.order,
328
    WidgetBuilder builder,
Adam Barth's avatar
Adam Barth committed
329
    RouteSettings settings: const RouteSettings(),
330 331
  }) : assert(order != null),
       super(builder: builder, settings: settings);
332 333 334 335 336 337 338 339

  Order order;

  @override
  Order get currentResult => order;

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