// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/material.dart'; class PestoDemo extends StatelessWidget { const PestoDemo({ Key? key }) : super(key: key); static const String routeName = '/pesto'; @override Widget build(BuildContext context) => const PestoHome(); } const String _kSmallLogoImage = 'logos/pesto/logo_small.png'; const String _kGalleryAssetsPackage = 'flutter_gallery_assets'; const double _kAppBarHeight = 128.0; const double _kFabHalfSize = 28.0; // TODO(mpcomplete): needs to adapt to screen size const double _kRecipePageMaxWidth = 500.0; final Set<Recipe?> _favoriteRecipes = <Recipe?>{}; final ThemeData _kTheme = ThemeData( brightness: Brightness.light, primarySwatch: Colors.teal, ); class PestoHome extends StatelessWidget { const PestoHome({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return const RecipeGridPage(recipes: kPestoRecipes); } } class PestoFavorites extends StatelessWidget { const PestoFavorites({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return RecipeGridPage(recipes: _favoriteRecipes.toList()); } } class PestoStyle extends TextStyle { const PestoStyle({ double fontSize = 12.0, FontWeight? fontWeight, Color color = Colors.black87, double? letterSpacing, double? height, }) : super( inherit: false, color: color, fontFamily: 'Raleway', fontSize: fontSize, fontWeight: fontWeight, textBaseline: TextBaseline.alphabetic, letterSpacing: letterSpacing, height: height, ); } // Displays a grid of recipe cards. class RecipeGridPage extends StatefulWidget { const RecipeGridPage({ Key? key, this.recipes }) : super(key: key); final List<Recipe?>? recipes; @override State<RecipeGridPage> createState() => _RecipeGridPageState(); } class _RecipeGridPageState extends State<RecipeGridPage> { @override Widget build(BuildContext context) { final double statusBarHeight = MediaQuery.of(context).padding.top; return Theme( data: _kTheme.copyWith(platform: Theme.of(context).platform), child: Scaffold( floatingActionButton: FloatingActionButton( backgroundColor: Colors.redAccent, onPressed: () { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Not supported.'), )); }, child: const Icon(Icons.edit), ), body: CustomScrollView( semanticChildCount: widget.recipes!.length, slivers: <Widget>[ _buildAppBar(context, statusBarHeight), _buildBody(context, statusBarHeight), ], ), ), ); } Widget _buildAppBar(BuildContext context, double statusBarHeight) { return SliverAppBar( pinned: true, expandedHeight: _kAppBarHeight, actions: <Widget>[ IconButton( icon: const Icon(Icons.search), tooltip: 'Search', onPressed: () { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Not supported.'), )); }, ), ], flexibleSpace: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { final Size size = constraints.biggest; final double appBarHeight = size.height - statusBarHeight; final double t = (appBarHeight - kToolbarHeight) / (_kAppBarHeight - kToolbarHeight); final double extraPadding = Tween<double>(begin: 10.0, end: 24.0).transform(t); final double logoHeight = appBarHeight - 1.5 * extraPadding; return Padding( padding: EdgeInsets.only( top: statusBarHeight + 0.5 * extraPadding, bottom: extraPadding, ), child: Center( child: PestoLogo(height: logoHeight, t: t.clamp(0.0, 1.0)), ), ); }, ), ); } Widget _buildBody(BuildContext context, double statusBarHeight) { final EdgeInsets mediaPadding = MediaQuery.of(context).padding; final EdgeInsets padding = EdgeInsets.only( top: 8.0, left: 8.0 + mediaPadding.left, right: 8.0 + mediaPadding.right, bottom: 8.0, ); return SliverPadding( padding: padding, sliver: SliverGrid( gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: _kRecipePageMaxWidth, crossAxisSpacing: 8.0, mainAxisSpacing: 8.0, ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { final Recipe? recipe = widget.recipes![index]; return RecipeCard( recipe: recipe, onTap: () { showRecipePage(context, recipe); }, ); }, childCount: widget.recipes!.length, ), ), ); } void showFavoritesPage(BuildContext context) { Navigator.push(context, MaterialPageRoute<void>( settings: const RouteSettings(name: '/pesto/favorites'), builder: (BuildContext context) => const PestoFavorites(), )); } void showRecipePage(BuildContext context, Recipe? recipe) { Navigator.push(context, MaterialPageRoute<void>( settings: const RouteSettings(name: '/pesto/recipe'), builder: (BuildContext context) { return Theme( data: _kTheme.copyWith(platform: Theme.of(context).platform), child: RecipePage(recipe: recipe), ); }, )); } } class PestoLogo extends StatefulWidget { const PestoLogo({Key? key, this.height, this.t}) : super(key: key); final double? height; final double? t; @override State<PestoLogo> createState() => _PestoLogoState(); } class _PestoLogoState extends State<PestoLogo> { // Native sizes for logo and its image/text components. static const double kLogoHeight = 162.0; static const double kLogoWidth = 220.0; static const double kImageHeight = 108.0; static const double kTextHeight = 48.0; final TextStyle titleStyle = const PestoStyle(fontSize: kTextHeight, fontWeight: FontWeight.w900, color: Colors.white, letterSpacing: 3.0); final RectTween _textRectTween = RectTween( begin: const Rect.fromLTWH(0.0, kLogoHeight, kLogoWidth, kTextHeight), end: const Rect.fromLTWH(0.0, kImageHeight, kLogoWidth, kTextHeight), ); final Curve _textOpacity = const Interval(0.4, 1.0, curve: Curves.easeInOut); final RectTween _imageRectTween = RectTween( begin: const Rect.fromLTWH(0.0, 0.0, kLogoWidth, kLogoHeight), end: const Rect.fromLTWH(0.0, 0.0, kLogoWidth, kImageHeight), ); @override Widget build(BuildContext context) { return Semantics( namesRoute: true, child: Transform( transform: Matrix4.identity()..scale(widget.height! / kLogoHeight), alignment: Alignment.topCenter, child: SizedBox( width: kLogoWidth, child: Stack( clipBehavior: Clip.none, children: <Widget>[ Positioned.fromRect( rect: _imageRectTween.lerp(widget.t!)!, child: Image.asset( _kSmallLogoImage, package: _kGalleryAssetsPackage, fit: BoxFit.contain, ), ), Positioned.fromRect( rect: _textRectTween.lerp(widget.t!)!, child: Opacity( opacity: _textOpacity.transform(widget.t!), child: Text('PESTO', style: titleStyle, textAlign: TextAlign.center), ), ), ], ), ), ), ); } } // A card with the recipe's image, author, and title. class RecipeCard extends StatelessWidget { const RecipeCard({ Key? key, this.recipe, this.onTap }) : super(key: key); final Recipe? recipe; final VoidCallback? onTap; TextStyle get titleStyle => const PestoStyle(fontSize: 24.0, fontWeight: FontWeight.w600); TextStyle get authorStyle => const PestoStyle(fontWeight: FontWeight.w500, color: Colors.black54); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Card( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Hero( tag: 'packages/$_kGalleryAssetsPackage/${recipe!.imagePath}', child: AspectRatio( aspectRatio: 4.0 / 3.0, child: Image.asset( recipe!.imagePath!, package: recipe!.imagePackage, fit: BoxFit.cover, semanticLabel: recipe!.name, ), ), ), Expanded( child: Row( children: <Widget>[ Padding( padding: const EdgeInsets.all(16.0), child: Image.asset( recipe!.ingredientsImagePath!, package: recipe!.ingredientsImagePackage, width: 48.0, height: 48.0, ), ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text(recipe!.name!, style: titleStyle, softWrap: false, overflow: TextOverflow.ellipsis), Text(recipe!.author!, style: authorStyle), ], ), ), ], ), ), ], ), ), ); } } // Displays one recipe. Includes the recipe sheet with a background image. class RecipePage extends StatefulWidget { const RecipePage({ Key? key, this.recipe }) : super(key: key); final Recipe? recipe; @override State<RecipePage> createState() => _RecipePageState(); } class _RecipePageState extends State<RecipePage> { final TextStyle menuItemStyle = const PestoStyle(fontSize: 15.0, color: Colors.black54, height: 24.0/15.0); double _getAppBarHeight(BuildContext context) => MediaQuery.of(context).size.height * 0.3; @override Widget build(BuildContext context) { // The full page content with the recipe's image behind it. This // adjusts based on the size of the screen. If the recipe sheet touches // the edge of the screen, use a slightly different layout. final double appBarHeight = _getAppBarHeight(context); final Size screenSize = MediaQuery.of(context).size; final bool fullWidth = screenSize.width < _kRecipePageMaxWidth; final bool isFavorite = _favoriteRecipes.contains(widget.recipe); return Scaffold( body: Stack( children: <Widget>[ Positioned( top: 0.0, left: 0.0, right: 0.0, height: appBarHeight + _kFabHalfSize, child: Hero( tag: 'packages/$_kGalleryAssetsPackage/${widget.recipe!.imagePath}', child: Image.asset( widget.recipe!.imagePath!, package: widget.recipe!.imagePackage, fit: fullWidth ? BoxFit.fitWidth : BoxFit.cover, ), ), ), CustomScrollView( slivers: <Widget>[ SliverAppBar( expandedHeight: appBarHeight - _kFabHalfSize, backgroundColor: Colors.transparent, actions: <Widget>[ PopupMenuButton<String>( onSelected: (String item) { }, itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[ _buildMenuItem(Icons.share, 'Tweet recipe'), _buildMenuItem(Icons.email, 'Email recipe'), _buildMenuItem(Icons.message, 'Message recipe'), _buildMenuItem(Icons.people, 'Share on Facebook'), ], ), ], flexibleSpace: const FlexibleSpaceBar( background: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment(0.0, -0.2), colors: <Color>[Color(0x60000000), Color(0x00000000)], ), ), ), ), ), SliverToBoxAdapter( child: Stack( children: <Widget>[ Container( padding: const EdgeInsets.only(top: _kFabHalfSize), width: fullWidth ? null : _kRecipePageMaxWidth, child: RecipeSheet(recipe: widget.recipe), ), Positioned( right: 16.0, child: FloatingActionButton( backgroundColor: Colors.redAccent, onPressed: _toggleFavorite, child: Icon(isFavorite ? Icons.favorite : Icons.favorite_border), ), ), ], ), ), ], ), ], ), ); } PopupMenuItem<String> _buildMenuItem(IconData icon, String label) { return PopupMenuItem<String>( child: Row( children: <Widget>[ Padding( padding: const EdgeInsets.only(right: 24.0), child: Icon(icon, color: Colors.black54), ), Text(label, style: menuItemStyle), ], ), ); } void _toggleFavorite() { setState(() { if (_favoriteRecipes.contains(widget.recipe)) _favoriteRecipes.remove(widget.recipe); else _favoriteRecipes.add(widget.recipe); }); } } /// Displays the recipe's name and instructions. class RecipeSheet extends StatelessWidget { RecipeSheet({ Key? key, this.recipe }) : super(key: key); final TextStyle titleStyle = const PestoStyle(fontSize: 34.0); final TextStyle descriptionStyle = const PestoStyle(fontSize: 15.0, color: Colors.black54, height: 24.0/15.0); final TextStyle itemStyle = const PestoStyle(fontSize: 15.0, height: 24.0/15.0); final TextStyle itemAmountStyle = PestoStyle(fontSize: 15.0, color: _kTheme.primaryColor, height: 24.0/15.0); final TextStyle headingStyle = const PestoStyle(fontSize: 16.0, fontWeight: FontWeight.bold, height: 24.0/15.0); final Recipe? recipe; @override Widget build(BuildContext context) { return Material( child: SafeArea( top: false, bottom: false, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 40.0), child: Table( columnWidths: const <int, TableColumnWidth>{ 0: FixedColumnWidth(64.0), }, children: <TableRow>[ TableRow( children: <Widget>[ TableCell( verticalAlignment: TableCellVerticalAlignment.middle, child: Image.asset( recipe!.ingredientsImagePath!, package: recipe!.ingredientsImagePackage, width: 32.0, height: 32.0, alignment: Alignment.centerLeft, fit: BoxFit.scaleDown, ), ), TableCell( verticalAlignment: TableCellVerticalAlignment.middle, child: Text(recipe!.name!, style: titleStyle), ), ] ), TableRow( children: <Widget>[ const SizedBox(), Padding( padding: const EdgeInsets.only(top: 8.0, bottom: 4.0), child: Text(recipe!.description!, style: descriptionStyle), ), ] ), TableRow( children: <Widget>[ const SizedBox(), Padding( padding: const EdgeInsets.only(top: 24.0, bottom: 4.0), child: Text('Ingredients', style: headingStyle), ), ] ), ...recipe!.ingredients!.map<TableRow>((RecipeIngredient ingredient) { return _buildItemRow(ingredient.amount!, ingredient.description!); }), TableRow( children: <Widget>[ const SizedBox(), Padding( padding: const EdgeInsets.only(top: 24.0, bottom: 4.0), child: Text('Steps', style: headingStyle), ), ] ), ...recipe!.steps!.map<TableRow>((RecipeStep step) { return _buildItemRow(step.duration ?? '', step.description!); }), ], ), ), ), ); } TableRow _buildItemRow(String left, String right) { return TableRow( children: <Widget>[ Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: Text(left, style: itemAmountStyle), ), Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: Text(right, style: itemStyle), ), ], ); } } class Recipe { const Recipe({ this.name, this.author, this.description, this.imagePath, this.imagePackage, this.ingredientsImagePath, this.ingredientsImagePackage, this.ingredients, this.steps, }); final String? name; final String? author; final String? description; final String? imagePath; final String? imagePackage; final String? ingredientsImagePath; final String? ingredientsImagePackage; final List<RecipeIngredient>? ingredients; final List<RecipeStep>? steps; } class RecipeIngredient { const RecipeIngredient({this.amount, this.description}); final String? amount; final String? description; } class RecipeStep { const RecipeStep({this.duration, this.description}); final String? duration; final String? description; } const List<Recipe> kPestoRecipes = <Recipe>[ Recipe( name: 'Roasted Chicken', author: 'Peter Carlsson', ingredientsImagePath: 'food/icons/main.png', ingredientsImagePackage: _kGalleryAssetsPackage, description: 'The perfect dish to welcome your family and friends with on a crisp autumn night. Pair with roasted veggies to truly impress them.', imagePath: 'food/roasted_chicken.png', imagePackage: _kGalleryAssetsPackage, ingredients: <RecipeIngredient>[ RecipeIngredient(amount: '1 whole', description: 'Chicken'), RecipeIngredient(amount: '1/2 cup', description: 'Butter'), RecipeIngredient(amount: '1 tbsp', description: 'Onion powder'), RecipeIngredient(amount: '1 tbsp', description: 'Freshly ground pepper'), RecipeIngredient(amount: '1 tsp', description: 'Salt'), ], steps: <RecipeStep>[ RecipeStep(duration: '1 min', description: 'Put in oven'), RecipeStep(duration: '1hr 45 min', description: 'Cook'), ], ), Recipe( name: 'Chopped Beet Leaves', author: 'Trevor Hansen', ingredientsImagePath: 'food/icons/veggie.png', ingredientsImagePackage: _kGalleryAssetsPackage, description: 'This vegetable has more to offer than just its root. Beet greens can be tossed into a salad to add some variety or sauteed on its own with some oil and garlic.', imagePath: 'food/chopped_beet_leaves.png', imagePackage: _kGalleryAssetsPackage, ingredients: <RecipeIngredient>[ RecipeIngredient(amount: '3 cups', description: 'Beet greens'), ], steps: <RecipeStep>[ RecipeStep(duration: '5 min', description: 'Chop'), ], ), Recipe( name: 'Pesto Pasta', author: 'Ali Connors', ingredientsImagePath: 'food/icons/main.png', ingredientsImagePackage: _kGalleryAssetsPackage, description: "With this pesto recipe, you can quickly whip up a meal to satisfy your savory needs. And if you're feeling festive, you can add bacon to taste.", imagePath: 'food/pesto_pasta.png', imagePackage: _kGalleryAssetsPackage, ingredients: <RecipeIngredient>[ RecipeIngredient(amount: '1/4 cup ', description: 'Pasta'), RecipeIngredient(amount: '2 cups', description: 'Fresh basil leaves'), RecipeIngredient(amount: '1/2 cup', description: 'Parmesan cheese'), RecipeIngredient(amount: '1/2 cup', description: 'Extra virgin olive oil'), RecipeIngredient(amount: '1/3 cup', description: 'Pine nuts'), RecipeIngredient(amount: '1/4 cup', description: 'Lemon juice'), RecipeIngredient(amount: '3 cloves', description: 'Garlic'), RecipeIngredient(amount: '1/4 tsp', description: 'Salt'), RecipeIngredient(amount: '1/8 tsp', description: 'Pepper'), RecipeIngredient(amount: '3 lbs', description: 'Bacon'), ], steps: <RecipeStep>[ RecipeStep(duration: '15 min', description: 'Blend'), ], ), Recipe( name: 'Cherry Pie', author: 'Sandra Adams', ingredientsImagePath: 'food/icons/main.png', ingredientsImagePackage: _kGalleryAssetsPackage, description: "Sometimes when you're craving some cheer in your life you can jumpstart your day with some cherry pie. Dessert for breakfast is perfectly acceptable.", imagePath: 'food/cherry_pie.png', imagePackage: _kGalleryAssetsPackage, ingredients: <RecipeIngredient>[ RecipeIngredient(amount: '1', description: 'Pie crust'), RecipeIngredient(amount: '4 cups', description: 'Fresh or frozen cherries'), RecipeIngredient(amount: '1 cup', description: 'Granulated sugar'), RecipeIngredient(amount: '4 tbsp', description: 'Cornstarch'), RecipeIngredient(amount: '1½ tbsp', description: 'Butter'), ], steps: <RecipeStep>[ RecipeStep(duration: '15 min', description: 'Mix'), RecipeStep(duration: '1hr 30 min', description: 'Bake'), ], ), Recipe( name: 'Spinach Salad', author: 'Peter Carlsson', ingredientsImagePath: 'food/icons/spicy.png', ingredientsImagePackage: _kGalleryAssetsPackage, description: "Everyone's favorite leafy green is back. Paired with fresh sliced onion, it's ready to tackle any dish, whether it be a salad or an egg scramble.", imagePath: 'food/spinach_onion_salad.png', imagePackage: _kGalleryAssetsPackage, ingredients: <RecipeIngredient>[ RecipeIngredient(amount: '4 cups', description: 'Spinach'), RecipeIngredient(amount: '1 cup', description: 'Sliced onion'), ], steps: <RecipeStep>[ RecipeStep(duration: '5 min', description: 'Mix'), ], ), Recipe( name: 'Butternut Squash Soup', author: 'Ali Connors', ingredientsImagePath: 'food/icons/healthy.png', ingredientsImagePackage: _kGalleryAssetsPackage, description: 'This creamy butternut squash soup will warm you on the chilliest of winter nights and bring a delightful pop of orange to the dinner table.', imagePath: 'food/butternut_squash_soup.png', imagePackage: _kGalleryAssetsPackage, ingredients: <RecipeIngredient>[ RecipeIngredient(amount: '1', description: 'Butternut squash'), RecipeIngredient(amount: '4 cups', description: 'Chicken stock'), RecipeIngredient(amount: '2', description: 'Potatoes'), RecipeIngredient(amount: '1', description: 'Onion'), RecipeIngredient(amount: '1', description: 'Carrot'), RecipeIngredient(amount: '1', description: 'Celery'), RecipeIngredient(amount: '1 tsp', description: 'Salt'), RecipeIngredient(amount: '1 tsp', description: 'Pepper'), ], steps: <RecipeStep>[ RecipeStep(duration: '10 min', description: 'Prep vegetables'), RecipeStep(duration: '5 min', description: 'Stir'), RecipeStep(duration: '1 hr 10 min', description: 'Cook'), ], ), Recipe( name: 'Spanakopita', author: 'Trevor Hansen', ingredientsImagePath: 'food/icons/quick.png', ingredientsImagePackage: _kGalleryAssetsPackage, description: "You 'feta' believe this is a crowd-pleaser! Flaky phyllo pastry surrounds a delicious mixture of spinach and cheeses to create the perfect appetizer.", imagePath: 'food/spanakopita.png', imagePackage: _kGalleryAssetsPackage, ingredients: <RecipeIngredient>[ RecipeIngredient(amount: '1 lb', description: 'Spinach'), RecipeIngredient(amount: '½ cup', description: 'Feta cheese'), RecipeIngredient(amount: '½ cup', description: 'Cottage cheese'), RecipeIngredient(amount: '2', description: 'Eggs'), RecipeIngredient(amount: '1', description: 'Onion'), RecipeIngredient(amount: '½ lb', description: 'Phyllo dough'), ], steps: <RecipeStep>[ RecipeStep(duration: '5 min', description: 'Sauté vegetables'), RecipeStep(duration: '3 min', description: 'Stir vegetables and other filling ingredients'), RecipeStep(duration: '10 min', description: 'Fill phyllo squares half-full with filling and fold.'), RecipeStep(duration: '40 min', description: 'Bake'), ], ), ];