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 (0)