Snippets

Kevin Armstrong Beer Menu - Hero Animation

Updated by Kevin Armstrong

File detail.dart Modified

  • Ignore whitespace
  • Hide word diff
           Column(
             children: <Widget>[
               BeerTile(
-                active: true,
+                isHeader: true,
                 beer: beer,
                 animation: widget.animation,
                 onAction: widget.onAction,

File main.dart Modified

  • Ignore whitespace
  • Hide word diff
             CurvedAnimation _animation = animationMaps[beer.id];
             return BeerTile(
               beer: beer,
-              active: false,
+              isHeader: false,
               animation: _animation,
               onAction: (){
                 _controller.forward();

File tile.dart Modified

  • Ignore whitespace
  • Hide word diff
     Animation<double> animation,
     this.beer,
     this.onAction,
-    this.active: false,
+    this.isHeader: false,
     this.delay: 200,
   }):super(key: key, listenable: animation);
 
   final Beer beer;
   final VoidCallback onAction;
-  final bool active;
+  final bool isHeader;
   final int delay;
 
   @override
     Tween<double> tween = Tween(begin: _width - 90, end: 0.0);
 
     return new Positioned(
-      top: active ? 0.0 : 10.0,
+      top: isHeader ? 0.0 : 10.0,
       bottom: 0.0,
-      right: active ? 0.0 : tween.evaluate(animation),
+      right: isHeader ? 0.0 : tween.evaluate(animation),
       width: _width,
       child: Hero(
         tag: beer.image,
           color: Colors.transparent,
           child: GestureDetector(
             onTap: (){
-              if(!active){
+              if(!isHeader){
                 onAction == null ? null : onAction();
               }
             },
             child: Stack(
               children: <Widget>[
                 new Positioned(
-                  top: active ? 0.0 : 10.0,
-                  bottom: active ? 0.0 : 10.0,
+                  top: isHeader ? 0.0 : 10.0,
+                  bottom: isHeader ? 0.0 : 10.0,
                   left: 0.0,
-                  right: active ? 0.0 : 20.0,
+                  right: isHeader ? 0.0 : 20.0,
                   child: new Container(
                     width: double.infinity,
                     height: double.infinity,
                   ),
                 ),
                 new Positioned(
-                  top: active ? 45.0 : 0.0,
-                  bottom: active ? -20.0 : 0.0,
-                  left: active ? _width - 40 : _width - 62,
+                  top: isHeader ? 45.0 : 0.0,
+                  bottom: isHeader ? -20.0 : 0.0,
+                  left: isHeader ? _width - 40 : _width - 62,
                   width: 70.0,
                   child: Container(
                     decoration: BoxDecoration(
   }
 
   _makeInfo(BuildContext context) {
-    return active ? Container() : Positioned(
+    return isHeader ? Container() : Positioned(
       top: 0.0,
       bottom: 0.0,
       left: 110.0,
   }
 
   double get _height {
-    if(active) {
+    if(isHeader) {
       return 275.0;
     } else {
       return 200.0;
Updated by Kevin Armstrong

File detail.dart Modified

  • Ignore whitespace
  • Hide word diff
 class BeerDetail extends StatefulWidget {
   final Beer beer;
   final CurvedAnimation animation;
-  final VoidCallback onTap;
+  final VoidCallback onAction;
 
-  BeerDetail({this.beer, this.animation, this.onTap});
+  BeerDetail({this.beer, this.animation, this.onAction});
 
   @override
   _BeerDetailState createState() => new _BeerDetailState();
                 active: true,
                 beer: beer,
                 animation: widget.animation,
-                onTap: widget.onTap,
+                onAction: widget.onAction,
               ),
               new Expanded(
                 child: SingleChildScrollView(
                   setState(() {
                     _visible = false;
                   });
-                  widget.onTap != null ? widget.onTap() : null;
+                  widget.onAction != null ? widget.onAction() : null;
                 },
               ),
             ),

File main.dart Modified

  • Ignore whitespace
  • Hide word diff
               beer: beer,
               active: false,
               animation: _animation,
-              onTap: (){
+              onAction: (){
                 _controller.forward();
               },
             );
         return BeerDetail(
           beer: beer,
           animation: _animation,
-          onTap: (){
+          onAction: (){
             Navigator.pop(context);
           },
         );

File tile.dart Modified

  • Ignore whitespace
  • Hide word diff
     Key key,
     Animation<double> animation,
     this.beer,
-    this.onTap,
+    this.onAction,
     this.active: false,
     this.delay: 200,
   }):super(key: key, listenable: animation);
 
   final Beer beer;
-  final VoidCallback onTap;
+  final VoidCallback onAction;
   final bool active;
   final int delay;
 
           child: GestureDetector(
             onTap: (){
               if(!active){
-                onTap == null ? null : onTap();
+                onAction == null ? null : onAction();
               }
             },
             child: Stack(
Updated by Kevin Armstrong

File main.dart Modified

  • Ignore whitespace
  • Hide word diff
               onTap: (){
                 _controller.forward();
               },
-              onCompleted: (){
-                _handleHero(beer);
-              },
             );
           },
           itemCount: beers.length,

File tile.dart Modified

  • Ignore whitespace
  • Hide word diff
     Animation<double> animation,
     this.beer,
     this.onTap,
-    this.onCompleted,
     this.active: false,
     this.delay: 200,
   }):super(key: key, listenable: animation);
 
   final Beer beer;
   final VoidCallback onTap;
-  final Function onCompleted;
   final bool active;
   final int delay;
 
Created by Kevin Armstrong

File beers.dart Added

  • Ignore whitespace
  • Hide word diff
+import 'dart:ui';
+
+class Beer {
+  final int id;
+  final String title;
+  final String word;
+  final double rating;
+  final double alcohol;
+  final String size;
+  final String note;
+  final String foodMatch;
+  final String image;
+  final Color color;
+
+  Beer(
+      {this.id,
+      this.title,
+      this.word,
+      this.rating,
+      this.size,
+      this.foodMatch,
+      this.note,
+      this.image,
+      this.alcohol,
+      this.color});
+
+  String get asset => 'assets/images/beers/$image.png';
+  bool get isDark => color.computeLuminance() < 0.6;
+}
+
+final List<Beer> beers = [
+  Beer(
+      id: 1,
+      title: 'Hallertau Luxe Kölsch',
+      word: 'Kölsch is the local brew of the city of Cologne in Southern Germany. Our version has all New Zealand ingredients. A great entry level craft beer to quench that lawn mowing thirst.',
+      rating: 3.2,
+      alcohol: 4.5,
+      size: '12x330ml',
+      note: 'Some say she’s the luxe life. Exuberant. Snappy. Bright. Chatty. Sunlit. Lush. Passionfruit. Blueberries. Sparkling. Refined. Considered. Dry. And frankly, refreshing.',
+      foodMatch: 'Ceasar Salad, Mature Cheddar, Fish & Chips',
+      image: 'luxe',
+      color: Color.fromRGBO(234, 188, 48, 1.0)),
+  Beer(
+      id: 2,
+      title: 'Hallertau Statesman Pale Ale',
+      word: 'No.2 is an iconic beer for Hallertau with a cult following. An eminently approachable Pale Ale with a delicious hop profile.',
+      rating: 3.8,
+      alcohol: 5.3,
+      size: '12x330ml',
+      note: 'Yowza! This character’s outspoken. Bursting with opinions. Arrives with a floral bouquet. Hoots. Honks. Hops.Charismatic. Honey. Bombastic. Citrus. Distinguished. Tang. Then closes the deal guaranteeing your thirst thoroughly quenched.',
+      foodMatch: 'Thai Beef Salad, Chicken Vindloo, Sweet & Sour Pork.',
+      image: 'statesman',
+      color: Color.fromRGBO(237, 142, 47, 1.0)),
+  Beer(
+      id: 3,
+      title: 'Hallertau Copper Tart Red Ale',
+      word: 'An Irish style beer given a New Zealand twist. Restrained hopping makes this beer more of a malt showcase. Flys in the autumn.',
+      rating: 3.8,
+      alcohol: 4.2,
+      size: '12x330ml',
+      note: 'This’d surely be a miner’s delight. Substantial. Satisfying. Deserved. Malt.Forged Caramel. Bitter. Chocolate. Rich. Worthwhile. Smooth and dry, earner of a knowing smile.',
+      foodMatch: 'Char grilled Tuna, Steak, Spicy Crabcakes.',
+      image: 'copper',
+      color: Color.fromRGBO(200, 76, 42, 1.0)),
+  Beer(
+      id: 4,
+      title: 'Hallertau Deception Schwarzbier',
+      word: 'None of the dryness associated with those other black beers Stouts and Porters. Dehusked malt delivers tons of flavour whilst remaining light on the palate. A real hit with ladies.',
+      rating: 3.8,
+      alcohol: 5.1,
+      size: '12x330ml',
+      note: 'This number is not what he seems.A bit of a trickster really. Smooth.Firm. Dark. Light. Bitter. Sweet.Coffee. Intricate. Chocolate. Subterfuge. You’ve been warned.',
+      foodMatch: 'Cold cuts, BBQ Blackened lamb, Confit of Duck.',
+      image: 'deception',
+      color: Color.fromRGBO(155, 77, 42, 1.0)),
+  Beer(
+      id: 5,
+      title: 'Hallertau Pilsnah',
+      word: 'This style of Pils is a wonderful thing. Super dry and really rather hopper than you’d expect.',
+      rating: 3.8,
+      alcohol: 5.0,
+      size: '12x330ml',
+      note: 'De-sweat your brow. Lawns mown. Hay bailed. Yoga stretched. Inning had. Here’s the reward. Citrus. Wood chips. Unexpected Hop notes. Ahh.',
+      foodMatch: 'Wok seared cuttlefish w pickled cucumber, herbs & chilli.',
+      image: 'pilsnah',
+      color: Color.fromRGBO(54, 80, 143, 1.0)),
+  Beer(
+      id: 6,
+      title: 'Hallertau Session IPA',
+      word: 'A super hoppy Session India Pale Ale with all the hops but half the alcohol. Perfect for those long afternoon sessions in the sun.',
+      rating: 3.8,
+      alcohol: 3.8,
+      size: '12x330ml',
+      note: 'Double the hops, half the tipsy. Revel in the hop bounty and enjoy the journey.',
+      foodMatch: 'Goan Curry, Eggs Benedict with Smoked Salmon, Chorizo',
+      image: 'session',
+      color: Color.fromRGBO(130, 61, 99, 1.0)),
+  Beer(
+      id: 7,
+      title: 'Hallertau Granny Smith Apple Cider',
+      word: 'Created in an off dry style with pleasing acidity from the Granny Smith apple. A cider for grown-ups.',
+      rating: 3.8,
+      alcohol: 5.1,
+      size: '12x330ml',
+      note: 'Crunch. Clean. Crisp. Refreshing. Bite. Blue skies. Cut grass. Good times. Granny Smith would be rapt with this sublimely cider.',
+      foodMatch: 'Pork Stroganoff, Eggs Benedict with bacon.',
+      image: 'cider',
+      color: Color.fromRGBO(88, 90, 59, 1.0)),
+  Beer(
+      id: 8,
+      title: 'Hallertau Maximus IPA',
+      word: 'Dry hopping is the process of adding hops to the post fermentation. These NZ and US hops give the beer a delicious tropical aroma. Please drink respectfully.',
+      rating: 3.8,
+      alcohol: 5.8,
+      size: '12x330ml',
+      note: 'The fiercely floral and fragrant hop, known as the earth wolf by the ancients, has been cunningly tamed with a soft, rich, maltiness.',
+      foodMatch: 'Thai Green Curry, Chicken Jalfrezi.',
+      image: 'maximus',
+      color: Color.fromRGBO(121, 118, 114, 1.0)),
+  Beer(
+      id: 9,
+      title: 'Hallertau Porter Noir',
+      word: 'Brewing this draws on winemaking traditions for a finely composed arrangement of soft tannins, forest fruits and chocolate, all completed with an elegantly long, dry finish.',
+      rating: 3.8,
+      alcohol: 6.6,
+      size: '12x330ml',
+      note: 'Dusty chocolate with hints of cherry pie and sandalwood.',
+      foodMatch: 'Confit of Duck, Game Casserole, Venison Burger, Blue Cheese.',
+      image: 'porternoir',
+      color: Color.fromRGBO(136, 91, 61, 1.0)),
+  Beer(
+      id: 10,
+      title: 'Hallertau Stuntman IIPA',
+      word: 'Best not attempted by the fainthearted.',
+      rating: 3.8,
+      alcohol: 8.8,
+      size: '12x330ml',
+      note: 'Tropical fruits and flowers, guava, mangoes and citrus are all in the mix.',
+      foodMatch: 'Jalapeno Poppers, Red Jungle Curry.',
+      image: 'stuntman',
+      color: Color.fromRGBO(167, 163, 156, 1.0)),
+];

File detail.dart Added

  • Ignore whitespace
  • Hide word diff
+import 'dart:async';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import './tile.dart';
+import './beers.dart';
+
+class BeerDetail extends StatefulWidget {
+  final Beer beer;
+  final CurvedAnimation animation;
+  final VoidCallback onTap;
+
+  BeerDetail({this.beer, this.animation, this.onTap});
+
+  @override
+  _BeerDetailState createState() => new _BeerDetailState();
+}
+
+class _BeerDetailState extends State<BeerDetail> {
+  bool _visible = false;
+
+  @override
+  void initState() {
+    if(widget.beer.isDark){
+      SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.light);
+    } else {
+      SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.dark);
+    }
+    Future.delayed(Duration(milliseconds: 250)).then((v){
+      setState(() {
+        _visible = true;
+      });
+    });
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    Beer beer = widget.beer;
+    return Scaffold(
+      body: Stack(
+        children: <Widget>[
+          Column(
+            children: <Widget>[
+              BeerTile(
+                active: true,
+                beer: beer,
+                animation: widget.animation,
+                onTap: widget.onTap,
+              ),
+              new Expanded(
+                child: SingleChildScrollView(
+                  child: new Column(
+                    crossAxisAlignment: CrossAxisAlignment.start,
+                    children: <Widget>[
+                      Container(
+                        width: double.infinity,
+                        padding: EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0),
+                        child: Text(
+                          'Tasting Note',
+                          textAlign: TextAlign.left,
+                          style: TextStyle(
+                            color: beer.color,
+                            fontWeight: FontWeight.bold,
+                          ),
+                        ),
+                      ),
+                      Container(
+                        width: double.infinity,
+                        padding: EdgeInsets.only(left: 15.0, right: 15.0, bottom: 15.0),
+                        child: Text(beer.note,),
+                      ),
+                      Divider(height: 10.0, indent: 35.0,),
+                      Container(
+                        width: double.infinity,
+                        padding: EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0),
+                        child: Text(
+                          'Word from the Maker',
+                          textAlign: TextAlign.left,
+                          style: Theme.of(context).textTheme.body2,
+                        ),
+                      ),
+                      Container(
+                        width: double.infinity,
+                        padding: EdgeInsets.only(left: 15.0, right: 15.0, bottom: 15.0),
+                        child: Text(beer.word,),
+                      ),
+                      Container(
+                        color: beer.color.withAlpha(120),
+                        child: Column(
+                          children: <Widget>[
+                            Container(
+                              width: double.infinity,
+                              padding: EdgeInsets.only(right: 15.0, left: 15.0, top: 15.0),
+                              child: Text(
+                                'Food Matches',
+                                textAlign: TextAlign.left,
+                                style: Theme.of(context).textTheme.body2.copyWith(
+                                  color: beer.isDark ? Colors.white : Colors.black
+                                ),
+                              ),
+                            ),
+                            Container(
+                              width: double.infinity,
+                              padding: EdgeInsets.only(left: 15.0, right: 15.0, bottom: 15.0),
+                              child: Text(beer.foodMatch,
+                                style: TextStyle(
+                                  color: beer.isDark ? Colors.white : Colors.black
+                                ),
+                              ),
+                            ),
+                          ],
+                        ),
+                      ),
+                      Container(
+                        width: double.infinity,
+                        padding: EdgeInsets.symmetric(horizontal: 15.0, vertical: 20.0),
+                        child: Column(
+                          crossAxisAlignment: CrossAxisAlignment.start,
+                          children: <Widget>[
+                            Text(beer.size, style: Theme.of(context).textTheme.body1,),
+                            Text('From \$48.00', style: Theme.of(context).textTheme.subhead.copyWith(fontWeight: FontWeight.w500),),
+                          ],
+                        ),
+                      ),
+                    ],
+                  ),
+                ),
+              ),
+              SafeArea(
+                top: false,
+                child: Container(
+                  margin: EdgeInsets.symmetric(vertical: 15.0, horizontal: 20.0),
+                  width: double.infinity,
+                  color: Colors.grey.shade300,
+                  child: FlatButton(
+                    color: Colors.grey.shade300,
+                    onPressed: (){},
+                    child: Text('Order'),
+                  ),
+                ),
+              ),
+            ],
+          ),
+          new AnimatedPositioned(
+            top: _visible ? 35.0 : 0.0,
+            left: 10.0,
+            height: 60.0,
+            width: 50.0,
+            duration: Duration(milliseconds: 150),
+            curve: Curves.bounceInOut,
+            child: AnimatedOpacity(
+              duration: Duration(milliseconds: 200),
+              curve: Curves.linear,
+              opacity: _visible ? 1.0 : 0.0,
+              child: IconButton(
+                icon: Icon(Icons.clear),
+                color: Colors.white,
+                onPressed: (){
+                  setState(() {
+                    _visible = false;
+                  });
+                  widget.onTap != null ? widget.onTap() : null;
+                },
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}

File main.dart Added

  • Ignore whitespace
  • Hide word diff
+import 'dart:async';
+import 'package:flutter/material.dart';
+import './beers.dart';
+import './tile.dart';
+import './detail.dart';
+
+class BeersMenuMain extends StatefulWidget {
+  BeersMenuMain({Key key}) : super(key: key);
+
+  @override
+  _BeersMenuState createState() => new _BeersMenuState();
+}
+
+class _BeersMenuState extends State<BeersMenuMain> with TickerProviderStateMixin {
+  Map<int, AnimationController> controllerMaps = new Map();
+  Map<int, CurvedAnimation> animationMaps = new Map();
+
+  @override
+  void initState() {
+    beers.forEach((Beer beer){
+      AnimationController _controller = AnimationController(duration: Duration(milliseconds: 400), vsync: this,);
+      CurvedAnimation _animation = new CurvedAnimation(parent: _controller, curve: Curves.easeIn);
+
+      controllerMaps[beer.id] = _controller;
+      _controller.addStatusListener((AnimationStatus status){
+        if(status == AnimationStatus.completed){
+          _handleHero(beer);
+        }
+      });
+      animationMaps[beer.id] = _animation;
+    });
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Theme(
+      data: ThemeData(
+        primaryColor: Colors.grey.shade200,
+      ),
+      child: Scaffold(
+        appBar: new AppBar(
+          title: new Text('Beer', style: TextStyle(
+            fontSize: 16.0,
+            color: Colors.grey.shade500
+          ),),
+          elevation: 0.0,
+        ),
+        body: ListView.builder(
+          itemBuilder: (context, index){
+            Beer beer = beers[index];
+            AnimationController _controller = controllerMaps[beer.id];
+            CurvedAnimation _animation = animationMaps[beer.id];
+            return BeerTile(
+              beer: beer,
+              active: false,
+              animation: _animation,
+              onTap: (){
+                _controller.forward();
+              },
+              onCompleted: (){
+                _handleHero(beer);
+              },
+            );
+          },
+          itemCount: beers.length,
+        ),
+      ),
+    );
+  }
+
+  void _handleHero(Beer beer) {
+    AnimationController _controller = controllerMaps[beer.id];
+    CurvedAnimation _animation = animationMaps[beer.id];
+    Navigator.push(context,
+      MaterialPageRoute(builder: (context){
+        return BeerDetail(
+          beer: beer,
+          animation: _animation,
+          onTap: (){
+            Navigator.pop(context);
+          },
+        );
+      }, fullscreenDialog: true)
+    ).then((value){
+      Future.delayed(Duration(milliseconds: 600)).then((v){
+        _controller.reverse();
+      });
+    });
+  }
+}

File tile.dart Added

  • Ignore whitespace
  • Hide word diff
+import 'package:flutter/material.dart';
+import './beers.dart';
+
+class BeerTile extends AnimatedWidget {
+  BeerTile({
+    Key key,
+    Animation<double> animation,
+    this.beer,
+    this.onTap,
+    this.onCompleted,
+    this.active: false,
+    this.delay: 200,
+  }):super(key: key, listenable: animation);
+
+  final Beer beer;
+  final VoidCallback onTap;
+  final Function onCompleted;
+  final bool active;
+  final int delay;
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      width: double.infinity,
+      height: _height,
+      child: Stack(
+        children: <Widget>[
+          _makeInfo(context),
+          _makeBeer(context),
+        ],
+      ),
+    );
+  }
+
+  _makeBeer(BuildContext context){
+    final Animation<double> animation = listenable;
+    final double _width = MediaQuery.of(context).size.width;
+
+    Tween<double> tween = Tween(begin: _width - 90, end: 0.0);
+
+    return new Positioned(
+      top: active ? 0.0 : 10.0,
+      bottom: 0.0,
+      right: active ? 0.0 : tween.evaluate(animation),
+      width: _width,
+      child: Hero(
+        tag: beer.image,
+        child: new Material(
+          color: Colors.transparent,
+          child: GestureDetector(
+            onTap: (){
+              if(!active){
+                onTap == null ? null : onTap();
+              }
+            },
+            child: Stack(
+              children: <Widget>[
+                new Positioned(
+                  top: active ? 0.0 : 10.0,
+                  bottom: active ? 0.0 : 10.0,
+                  left: 0.0,
+                  right: active ? 0.0 : 20.0,
+                  child: new Container(
+                    width: double.infinity,
+                    height: double.infinity,
+                    color: beer.color,
+                    child: Text(
+                      beer.title,
+                      style: TextStyle(
+                        color: Colors.white,
+                        fontFamily: 'Times',
+                        fontWeight: FontWeight.w300,
+                        fontSize: 35.0,
+                      ),
+                    ),
+                    alignment: Alignment.bottomLeft,
+                    padding: EdgeInsets.only(
+                      bottom: 30.0,
+                      left: 15.0,
+                      right: 65.0,
+                    ),
+                  ),
+                ),
+                new Positioned(
+                  top: active ? 45.0 : 0.0,
+                  bottom: active ? -20.0 : 0.0,
+                  left: active ? _width - 40 : _width - 62,
+                  width: 70.0,
+                  child: Container(
+                    decoration: BoxDecoration(
+                      image: DecorationImage(
+                        image: AssetImage(beer.asset),
+                        fit: BoxFit.fitHeight,
+                      ),
+                    ),
+                  ),
+                ),
+              ],
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+
+  _makeInfo(BuildContext context) {
+    return active ? Container() : Positioned(
+      top: 0.0,
+      bottom: 0.0,
+      left: 110.0,
+      right: 20.0,
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        mainAxisAlignment: MainAxisAlignment.center,
+        children: <Widget>[
+          Padding(
+            padding: const EdgeInsets.only(bottom: 8.0),
+            child: Text(beer.title, style: Theme.of(context).textTheme.headline.copyWith(color: beer.color),),
+          ),
+          Text(beer.size, style: Theme.of(context).textTheme.body1,),
+          Text('From \$48.00', style: Theme.of(context).textTheme.subhead.copyWith(fontWeight: FontWeight.w500),),
+          Container(
+            margin: EdgeInsets.symmetric(vertical: 15.0),
+            width: double.infinity,
+            child: FlatButton(
+              color: Colors.grey.shade300,
+              onPressed: (){},
+              child: Text('Order'),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  double get _height {
+    if(active) {
+      return 275.0;
+    } else {
+      return 200.0;
+    }
+  }
+}
HTTPS SSH

You can clone a snippet to your computer for local editing. Learn more.