Snippets

Kevin Armstrong Beer Menu - Hero Animation

Created by Kevin Armstrong last modified
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)),
];
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 onAction;

  BeerDetail({this.beer, this.animation, this.onAction});

  @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(
                isHeader: true,
                beer: beer,
                animation: widget.animation,
                onAction: widget.onAction,
              ),
              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.onAction != null ? widget.onAction() : null;
                },
              ),
            ),
          ),
        ],
      ),
    );
  }
}
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,
              isHeader: false,
              animation: _animation,
              onAction: (){
                _controller.forward();
              },
            );
          },
          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,
          onAction: (){
            Navigator.pop(context);
          },
        );
      }, fullscreenDialog: true)
    ).then((value){
      Future.delayed(Duration(milliseconds: 600)).then((v){
        _controller.reverse();
      });
    });
  }
}
import 'package:flutter/material.dart';
import './beers.dart';

class BeerTile extends AnimatedWidget {
  BeerTile({
    Key key,
    Animation<double> animation,
    this.beer,
    this.onAction,
    this.isHeader: false,
    this.delay: 200,
  }):super(key: key, listenable: animation);

  final Beer beer;
  final VoidCallback onAction;
  final bool isHeader;
  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: isHeader ? 0.0 : 10.0,
      bottom: 0.0,
      right: isHeader ? 0.0 : tween.evaluate(animation),
      width: _width,
      child: Hero(
        tag: beer.image,
        child: new Material(
          color: Colors.transparent,
          child: GestureDetector(
            onTap: (){
              if(!isHeader){
                onAction == null ? null : onAction();
              }
            },
            child: Stack(
              children: <Widget>[
                new Positioned(
                  top: isHeader ? 0.0 : 10.0,
                  bottom: isHeader ? 0.0 : 10.0,
                  left: 0.0,
                  right: isHeader ? 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: isHeader ? 45.0 : 0.0,
                  bottom: isHeader ? -20.0 : 0.0,
                  left: isHeader ? _width - 40 : _width - 62,
                  width: 70.0,
                  child: Container(
                    decoration: BoxDecoration(
                      image: DecorationImage(
                        image: AssetImage(beer.asset),
                        fit: BoxFit.fitHeight,
                      ),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  _makeInfo(BuildContext context) {
    return isHeader ? 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(isHeader) {
      return 275.0;
    } else {
      return 200.0;
    }
  }
}

Comments (3)

  1. Evan Parker

    I'll never drink beer again. The last time I felt so bad I thought I was dying. This is terrible. Alcohol should be banned.

  2. Red Arrow

    The worst thing about alcohol addiction is that you can't understand and appreciate the degree of your addiction in time, especially when it comes to beer. Over time, beer is no longer enough, and you're looking for stronger alcohol. I went through it, and only the fact that I found on Ashley Down rehab recovery help helped me. If it weren't for the specialists of this center, then I'd hardly be able to write this comment now.

HTTPS SSH

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