"Fossies" - the Fresh Open Source Software Archive

Member "flutter-1.22.4/dev/integration_tests/flutter_gallery/lib/demo/shrine/expanding_bottom_sheet.dart" (13 Nov 2020, 20701 Bytes) of package /linux/misc/flutter-1.22.4.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Dart source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file.

    1 // Copyright 2014 The Flutter Authors. All rights reserved.
    2 // Use of this source code is governed by a BSD-style license that can be
    3 // found in the LICENSE file.
    4 
    5 import 'dart:async';
    6 
    7 import 'package:flutter/material.dart';
    8 import 'package:flutter/services.dart';
    9 import 'package:meta/meta.dart';
   10 import 'package:scoped_model/scoped_model.dart';
   11 
   12 import 'package:flutter_gallery/demo/shrine/colors.dart';
   13 import 'package:flutter_gallery/demo/shrine/model/app_state_model.dart';
   14 import 'package:flutter_gallery/demo/shrine/model/product.dart';
   15 import 'package:flutter_gallery/demo/shrine/shopping_cart.dart';
   16 
   17 // These curves define the emphasized easing curve.
   18 const Cubic _kAccelerateCurve = Cubic(0.548, 0.0, 0.757, 0.464);
   19 const Cubic _kDecelerateCurve = Cubic(0.23, 0.94, 0.41, 1.0);
   20 // The time at which the accelerate and decelerate curves switch off
   21 const double _kPeakVelocityTime = 0.248210;
   22 // Percent (as a decimal) of animation that should be completed at _peakVelocityTime
   23 const double _kPeakVelocityProgress = 0.379146;
   24 const double _kCartHeight = 56.0;
   25 // Radius of the shape on the top left of the sheet.
   26 const double _kCornerRadius = 24.0;
   27 // Width for just the cart icon and no thumbnails.
   28 const double _kWidthForCartIcon = 64.0;
   29 
   30 class ExpandingBottomSheet extends StatefulWidget {
   31   const ExpandingBottomSheet({Key key, @required this.hideController})
   32       : assert(hideController != null),
   33         super(key: key);
   34 
   35   final AnimationController hideController;
   36 
   37   @override
   38   _ExpandingBottomSheetState createState() => _ExpandingBottomSheetState();
   39 
   40   static _ExpandingBottomSheetState of(BuildContext context, {bool isNullOk = false}) {
   41     assert(isNullOk != null);
   42     assert(context != null);
   43     final _ExpandingBottomSheetState result = context.findAncestorStateOfType<_ExpandingBottomSheetState>();
   44     if (isNullOk || result != null) {
   45       return result;
   46     }
   47     throw FlutterError(
   48       'ExpandingBottomSheet.of() called with a context that does not contain a ExpandingBottomSheet.\n');
   49   }
   50 }
   51 
   52 // Emphasized Easing is a motion curve that has an organic, exciting feeling.
   53 // It's very fast to begin with and then very slow to finish. Unlike standard
   54 // curves, like [Curves.fastOutSlowIn], it can't be expressed in a cubic bezier
   55 // curve formula. It's quintic, not cubic. But it _can_ be expressed as one
   56 // curve followed by another, which we do here.
   57 Animation<T> _getEmphasizedEasingAnimation<T>({
   58   @required T begin,
   59   @required T peak,
   60   @required T end,
   61   @required bool isForward,
   62   @required Animation<double> parent,
   63 }) {
   64   Curve firstCurve;
   65   Curve secondCurve;
   66   double firstWeight;
   67   double secondWeight;
   68 
   69   if (isForward) {
   70     firstCurve = _kAccelerateCurve;
   71     secondCurve = _kDecelerateCurve;
   72     firstWeight = _kPeakVelocityTime;
   73     secondWeight = 1.0 - _kPeakVelocityTime;
   74   } else {
   75     firstCurve = _kDecelerateCurve.flipped;
   76     secondCurve = _kAccelerateCurve.flipped;
   77     firstWeight = 1.0 - _kPeakVelocityTime;
   78     secondWeight = _kPeakVelocityTime;
   79   }
   80 
   81   return TweenSequence<T>(
   82     <TweenSequenceItem<T>>[
   83       TweenSequenceItem<T>(
   84         weight: firstWeight,
   85         tween: Tween<T>(
   86           begin: begin,
   87           end: peak,
   88         ).chain(CurveTween(curve: firstCurve)),
   89       ),
   90       TweenSequenceItem<T>(
   91         weight: secondWeight,
   92         tween: Tween<T>(
   93           begin: peak,
   94           end: end,
   95         ).chain(CurveTween(curve: secondCurve)),
   96       ),
   97     ],
   98   ).animate(parent);
   99 }
  100 
  101 // Calculates the value where two double Animations should be joined. Used by
  102 // callers of _getEmphasisedEasing<double>().
  103 double _getPeakPoint({double begin, double end}) {
  104   return begin + (end - begin) * _kPeakVelocityProgress;
  105 }
  106 
  107 class _ExpandingBottomSheetState extends State<ExpandingBottomSheet> with TickerProviderStateMixin {
  108   final GlobalKey _expandingBottomSheetKey = GlobalKey(debugLabel: 'Expanding bottom sheet');
  109 
  110   // The width of the Material, calculated by _widthFor() & based on the number
  111   // of products in the cart. 64.0 is the width when there are 0 products
  112   // (_kWidthForZeroProducts)
  113   double _width = _kWidthForCartIcon;
  114 
  115   // Controller for the opening and closing of the ExpandingBottomSheet
  116   AnimationController _controller;
  117 
  118   // Animations for the opening and closing of the ExpandingBottomSheet
  119   Animation<double> _widthAnimation;
  120   Animation<double> _heightAnimation;
  121   Animation<double> _thumbnailOpacityAnimation;
  122   Animation<double> _cartOpacityAnimation;
  123   Animation<double> _shapeAnimation;
  124   Animation<Offset> _slideAnimation;
  125 
  126   @override
  127   void initState() {
  128     super.initState();
  129     _controller = AnimationController(
  130       duration: const Duration(milliseconds: 500),
  131       vsync: this,
  132     );
  133   }
  134 
  135   @override
  136   void dispose() {
  137     _controller.dispose();
  138     super.dispose();
  139   }
  140 
  141   Animation<double> _getWidthAnimation(double screenWidth) {
  142     if (_controller.status == AnimationStatus.forward) {
  143       // Opening animation
  144       return Tween<double>(begin: _width, end: screenWidth).animate(
  145         CurvedAnimation(
  146           parent: _controller.view,
  147           curve: const Interval(0.0, 0.3, curve: Curves.fastOutSlowIn),
  148         ),
  149       );
  150     } else {
  151       // Closing animation
  152       return _getEmphasizedEasingAnimation(
  153         begin: _width,
  154         peak: _getPeakPoint(begin: _width, end: screenWidth),
  155         end: screenWidth,
  156         isForward: false,
  157         parent: CurvedAnimation(parent: _controller.view, curve: const Interval(0.0, 0.87)),
  158       );
  159     }
  160   }
  161 
  162   Animation<double> _getHeightAnimation(double screenHeight) {
  163     if (_controller.status == AnimationStatus.forward) {
  164       // Opening animation
  165 
  166       return _getEmphasizedEasingAnimation(
  167         begin: _kCartHeight,
  168         peak: _kCartHeight + (screenHeight - _kCartHeight) * _kPeakVelocityProgress,
  169         end: screenHeight,
  170         isForward: true,
  171         parent: _controller.view,
  172       );
  173     } else {
  174       // Closing animation
  175       return Tween<double>(
  176         begin: _kCartHeight,
  177         end: screenHeight,
  178       ).animate(
  179         CurvedAnimation(
  180           parent: _controller.view,
  181           curve: const Interval(0.434, 1.0, curve: Curves.linear), // not used
  182           // only the reverseCurve will be used
  183           reverseCurve: Interval(0.434, 1.0, curve: Curves.fastOutSlowIn.flipped),
  184         ),
  185       );
  186     }
  187   }
  188 
  189   // Animation of the cut corner. It's cut when closed and not cut when open.
  190   Animation<double> _getShapeAnimation() {
  191     if (_controller.status == AnimationStatus.forward) {
  192       return Tween<double>(begin: _kCornerRadius, end: 0.0).animate(
  193         CurvedAnimation(
  194           parent: _controller.view,
  195           curve: const Interval(0.0, 0.3, curve: Curves.fastOutSlowIn),
  196         ),
  197       );
  198     } else {
  199       return _getEmphasizedEasingAnimation(
  200         begin: _kCornerRadius,
  201         peak: _getPeakPoint(begin: _kCornerRadius, end: 0.0),
  202         end: 0.0,
  203         isForward: false,
  204         parent: _controller.view,
  205       );
  206     }
  207   }
  208 
  209   Animation<double> _getThumbnailOpacityAnimation() {
  210     return Tween<double>(begin: 1.0, end: 0.0).animate(
  211       CurvedAnimation(
  212         parent: _controller.view,
  213         curve: _controller.status == AnimationStatus.forward
  214           ? const Interval(0.0, 0.3)
  215           : const Interval(0.532, 0.766),
  216       ),
  217     );
  218   }
  219 
  220   Animation<double> _getCartOpacityAnimation() {
  221     return CurvedAnimation(
  222       parent: _controller.view,
  223       curve: _controller.status == AnimationStatus.forward
  224         ? const Interval(0.3, 0.6)
  225         : const Interval(0.766, 1.0),
  226     );
  227   }
  228 
  229   // Returns the correct width of the ExpandingBottomSheet based on the number of
  230   // products in the cart.
  231   double _widthFor(int numProducts) {
  232     switch (numProducts) {
  233       case 0:
  234         return _kWidthForCartIcon;
  235       case 1:
  236         return 136.0;
  237       case 2:
  238         return 192.0;
  239       case 3:
  240         return 248.0;
  241       default:
  242         return 278.0;
  243     }
  244   }
  245 
  246   // Returns true if the cart is open or opening and false otherwise.
  247   bool get _isOpen {
  248     final AnimationStatus status = _controller.status;
  249     return status == AnimationStatus.completed || status == AnimationStatus.forward;
  250   }
  251 
  252   // Opens the ExpandingBottomSheet if it's closed, otherwise does nothing.
  253   void open() {
  254     if (!_isOpen) {
  255       _controller.forward();
  256     }
  257   }
  258 
  259   // Closes the ExpandingBottomSheet if it's open or opening, otherwise does nothing.
  260   void close() {
  261     if (_isOpen) {
  262       _controller.reverse();
  263     }
  264   }
  265 
  266   // Changes the padding between the start edge of the Material and the cart icon
  267   // based on the number of products in the cart (padding increases when > 0
  268   // products.)
  269   EdgeInsetsDirectional _cartPaddingFor(int numProducts) {
  270     return (numProducts == 0)
  271       ? const EdgeInsetsDirectional.only(start: 20.0, end: 8.0)
  272       : const EdgeInsetsDirectional.only(start: 32.0, end: 8.0);
  273   }
  274 
  275   bool get _cartIsVisible => _thumbnailOpacityAnimation.value == 0.0;
  276 
  277   Widget _buildThumbnails(int numProducts) {
  278     return ExcludeSemantics(
  279       child: Opacity(
  280         opacity: _thumbnailOpacityAnimation.value,
  281         child: Column(
  282           children: <Widget>[
  283             Row(
  284               children: <Widget>[
  285                 AnimatedPadding(
  286                   padding: _cartPaddingFor(numProducts),
  287                   child: const Icon(Icons.shopping_cart),
  288                   duration: const Duration(milliseconds: 225),
  289                 ),
  290                 Container(
  291                   // Accounts for the overflow number
  292                   width: numProducts > 3 ? _width - 94.0 : _width - 64.0,
  293                   height: _kCartHeight,
  294                   padding: const EdgeInsets.symmetric(vertical: 8.0),
  295                   child: ProductThumbnailRow(),
  296                 ),
  297                 ExtraProductsNumber(),
  298               ],
  299             ),
  300           ],
  301         ),
  302       ),
  303     );
  304   }
  305 
  306   Widget _buildShoppingCartPage() {
  307     return Opacity(
  308       opacity: _cartOpacityAnimation.value,
  309       child: ShoppingCartPage(),
  310     );
  311   }
  312 
  313   Widget _buildCart(BuildContext context, Widget child) {
  314     // numProducts is the number of different products in the cart (does not
  315     // include multiples of the same product).
  316     final AppStateModel model = ScopedModel.of<AppStateModel>(context);
  317     final int numProducts = model.productsInCart.keys.length;
  318     final int totalCartQuantity = model.totalCartQuantity;
  319     final Size screenSize = MediaQuery.of(context).size;
  320     final double screenWidth = screenSize.width;
  321     final double screenHeight = screenSize.height;
  322 
  323     _width = _widthFor(numProducts);
  324     _widthAnimation = _getWidthAnimation(screenWidth);
  325     _heightAnimation = _getHeightAnimation(screenHeight);
  326     _shapeAnimation = _getShapeAnimation();
  327     _thumbnailOpacityAnimation = _getThumbnailOpacityAnimation();
  328     _cartOpacityAnimation = _getCartOpacityAnimation();
  329 
  330     return Semantics(
  331       button: true,
  332       value: 'Shopping cart, $totalCartQuantity items',
  333       child: Container(
  334         width: _widthAnimation.value,
  335         height: _heightAnimation.value,
  336         child: Material(
  337           animationDuration: const Duration(milliseconds: 0),
  338           shape: BeveledRectangleBorder(
  339             borderRadius: BorderRadius.only(
  340               topLeft: Radius.circular(_shapeAnimation.value),
  341             ),
  342           ),
  343           elevation: 4.0,
  344           color: kShrinePink50,
  345           child: _cartIsVisible
  346             ? _buildShoppingCartPage()
  347             : _buildThumbnails(numProducts),
  348         ),
  349       ),
  350     );
  351   }
  352 
  353   // Builder for the hide and reveal animation when the backdrop opens and closes
  354   Widget _buildSlideAnimation(BuildContext context, Widget child) {
  355     _slideAnimation = _getEmphasizedEasingAnimation(
  356       begin: const Offset(1.0, 0.0),
  357       peak: const Offset(_kPeakVelocityProgress, 0.0),
  358       end: const Offset(0.0, 0.0),
  359       isForward: widget.hideController.status == AnimationStatus.forward,
  360       parent: widget.hideController,
  361     );
  362 
  363     return SlideTransition(
  364       position: _slideAnimation,
  365       child: child,
  366     );
  367   }
  368 
  369   // Closes the cart if the cart is open, otherwise exits the app (this should
  370   // only be relevant for Android).
  371   Future<bool> _onWillPop() async {
  372     if (!_isOpen) {
  373       await SystemNavigator.pop();
  374       return true;
  375     }
  376 
  377     close();
  378     return true;
  379   }
  380 
  381   @override
  382   Widget build(BuildContext context) {
  383     return AnimatedSize(
  384       key: _expandingBottomSheetKey,
  385       duration: const Duration(milliseconds: 225),
  386       curve: Curves.easeInOut,
  387       vsync: this,
  388       alignment: FractionalOffset.topLeft,
  389       child: WillPopScope(
  390         onWillPop: _onWillPop,
  391         child: AnimatedBuilder(
  392           animation: widget.hideController,
  393           builder: _buildSlideAnimation,
  394           child: GestureDetector(
  395             behavior: HitTestBehavior.opaque,
  396             onTap: open,
  397             child: ScopedModelDescendant<AppStateModel>(
  398               builder: (BuildContext context, Widget child, AppStateModel model) {
  399                 return AnimatedBuilder(
  400                   builder: _buildCart,
  401                   animation: _controller,
  402                 );
  403               },
  404             ),
  405           ),
  406         ),
  407       ),
  408     );
  409   }
  410 }
  411 
  412 class ProductThumbnailRow extends StatefulWidget {
  413   @override
  414   _ProductThumbnailRowState createState() => _ProductThumbnailRowState();
  415 }
  416 
  417 class _ProductThumbnailRowState extends State<ProductThumbnailRow> {
  418   final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
  419 
  420   // _list represents what's currently on screen. If _internalList updates,
  421   // it will need to be updated to match it.
  422   _ListModel _list;
  423 
  424   // _internalList represents the list as it is updated by the AppStateModel.
  425   List<int> _internalList;
  426 
  427   @override
  428   void initState() {
  429     super.initState();
  430     _list = _ListModel(
  431       listKey: _listKey,
  432       initialItems: ScopedModel.of<AppStateModel>(context).productsInCart.keys.toList(),
  433       removedItemBuilder: _buildRemovedThumbnail,
  434     );
  435     _internalList = List<int>.from(_list.list);
  436   }
  437 
  438   Product _productWithId(int productId) {
  439     final AppStateModel model = ScopedModel.of<AppStateModel>(context);
  440     final Product product = model.getProductById(productId);
  441     assert(product != null);
  442     return product;
  443   }
  444 
  445   Widget _buildRemovedThumbnail(int item, BuildContext context, Animation<double> animation) {
  446     return ProductThumbnail(animation, animation, _productWithId(item));
  447   }
  448 
  449   Widget _buildThumbnail(BuildContext context, int index, Animation<double> animation) {
  450     final Animation<double> thumbnailSize = Tween<double>(begin: 0.8, end: 1.0).animate(
  451       CurvedAnimation(
  452         curve: const Interval(0.33, 1.0, curve: Curves.easeIn),
  453         parent: animation,
  454       ),
  455     );
  456 
  457     final Animation<double> opacity = CurvedAnimation(
  458       curve: const Interval(0.33, 1.0, curve: Curves.linear),
  459       parent: animation,
  460     );
  461 
  462     return ProductThumbnail(thumbnailSize, opacity, _productWithId(_list[index]));
  463   }
  464 
  465   // If the lists are the same length, assume nothing has changed.
  466   // If the internalList is shorter than the ListModel, an item has been removed.
  467   // If the internalList is longer, then an item has been added.
  468   void _updateLists() {
  469     // Update _internalList based on the model
  470     _internalList = ScopedModel.of<AppStateModel>(context).productsInCart.keys.toList();
  471     final Set<int> internalSet = Set<int>.from(_internalList);
  472     final Set<int> listSet = Set<int>.from(_list.list);
  473 
  474     final Set<int> difference = internalSet.difference(listSet);
  475     if (difference.isEmpty) {
  476       return;
  477     }
  478 
  479     for (final int product in difference) {
  480       if (_internalList.length < _list.length) {
  481         _list.remove(product);
  482       } else if (_internalList.length > _list.length) {
  483         _list.add(product);
  484       }
  485     }
  486 
  487     while (_internalList.length != _list.length) {
  488       int index = 0;
  489       // Check bounds and that the list elements are the same
  490       while (_internalList.isNotEmpty &&
  491           _list.length > 0 &&
  492           index < _internalList.length &&
  493           index < _list.length &&
  494           _internalList[index] == _list[index]) {
  495         index++;
  496       }
  497     }
  498   }
  499 
  500   Widget _buildAnimatedList() {
  501     return AnimatedList(
  502       key: _listKey,
  503       shrinkWrap: true,
  504       itemBuilder: _buildThumbnail,
  505       initialItemCount: _list.length,
  506       scrollDirection: Axis.horizontal,
  507       physics: const NeverScrollableScrollPhysics(), // Cart shouldn't scroll
  508     );
  509   }
  510 
  511   @override
  512   Widget build(BuildContext context) {
  513     _updateLists();
  514     return ScopedModelDescendant<AppStateModel>(
  515       builder: (BuildContext context, Widget child, AppStateModel model) => _buildAnimatedList(),
  516     );
  517   }
  518 }
  519 
  520 class ExtraProductsNumber extends StatelessWidget {
  521   // Calculates the number to be displayed at the end of the row if there are
  522   // more than three products in the cart. This calculates overflow products,
  523   // including their duplicates (but not duplicates of products shown as
  524   // thumbnails).
  525   int _calculateOverflow(AppStateModel model) {
  526     final Map<int, int> productMap = model.productsInCart;
  527     // List created to be able to access products by index instead of ID.
  528     // Order is guaranteed because productsInCart returns a LinkedHashMap.
  529     final List<int> products = productMap.keys.toList();
  530     int overflow = 0;
  531     final int numProducts = products.length;
  532     if (numProducts > 3) {
  533       for (int i = 3; i < numProducts; i++) {
  534         overflow += productMap[products[i]];
  535       }
  536     }
  537     return overflow;
  538   }
  539 
  540   Widget _buildOverflow(AppStateModel model, BuildContext context) {
  541     if (model.productsInCart.length <= 3)
  542       return Container();
  543 
  544     final int numOverflowProducts = _calculateOverflow(model);
  545     // Maximum of 99 so padding doesn't get messy.
  546     final int displayedOverflowProducts = numOverflowProducts <= 99 ? numOverflowProducts : 99;
  547     return Container(
  548       child: Text(
  549         '+$displayedOverflowProducts',
  550         style: Theme.of(context).primaryTextTheme.button,
  551       ),
  552     );
  553   }
  554 
  555   @override
  556   Widget build(BuildContext context) {
  557     return ScopedModelDescendant<AppStateModel>(
  558       builder: (BuildContext builder, Widget child, AppStateModel model) => _buildOverflow(model, context),
  559     );
  560   }
  561 }
  562 
  563 class ProductThumbnail extends StatelessWidget {
  564   const ProductThumbnail(this.animation, this.opacityAnimation, this.product);
  565 
  566   final Animation<double> animation;
  567   final Animation<double> opacityAnimation;
  568   final Product product;
  569 
  570   @override
  571   Widget build(BuildContext context) {
  572     return FadeTransition(
  573       opacity: opacityAnimation,
  574       child: ScaleTransition(
  575         scale: animation,
  576         child: Container(
  577           width: 40.0,
  578           height: 40.0,
  579           decoration: BoxDecoration(
  580             image: DecorationImage(
  581               image: ExactAssetImage(
  582                 product.assetName, // asset name
  583                 package: product.assetPackage, // asset package
  584               ),
  585               fit: BoxFit.cover,
  586             ),
  587             borderRadius: const BorderRadius.all(Radius.circular(10.0)),
  588           ),
  589           margin: const EdgeInsets.only(left: 16.0),
  590         ),
  591       ),
  592     );
  593   }
  594 }
  595 
  596 // _ListModel manipulates an internal list and an AnimatedList
  597 class _ListModel {
  598   _ListModel({
  599     @required this.listKey,
  600     @required this.removedItemBuilder,
  601     Iterable<int> initialItems,
  602   }) : assert(listKey != null),
  603        assert(removedItemBuilder != null),
  604        _items = initialItems?.toList() ?? <int>[];
  605 
  606   final GlobalKey<AnimatedListState> listKey;
  607   final Widget Function(int item, BuildContext context, Animation<double> animation) removedItemBuilder;
  608   final List<int> _items;
  609 
  610   AnimatedListState get _animatedList => listKey.currentState;
  611 
  612   void add(int product) {
  613     _insert(_items.length, product);
  614   }
  615 
  616   void _insert(int index, int item) {
  617     _items.insert(index, item);
  618     _animatedList.insertItem(index, duration: const Duration(milliseconds: 225));
  619   }
  620 
  621   void remove(int product) {
  622     final int index = _items.indexOf(product);
  623     if (index >= 0) {
  624       _removeAt(index);
  625     }
  626   }
  627 
  628   void _removeAt(int index) {
  629     final int removedItem = _items.removeAt(index);
  630     if (removedItem != null) {
  631       _animatedList.removeItem(index, (BuildContext context, Animation<double> animation) {
  632         return removedItemBuilder(removedItem, context, animation);
  633       });
  634     }
  635   }
  636 
  637   int get length => _items.length;
  638 
  639   int operator [](int index) => _items[index];
  640 
  641   int indexOf(int item) => _items.indexOf(item);
  642 
  643   List<int> get list => _items;
  644 }