diff --git a/veggieseasons/lib/data/app_state.dart b/veggieseasons/lib/data/app_state.dart index 9fd67c5b5..27e239508 100644 --- a/veggieseasons/lib/data/app_state.dart +++ b/veggieseasons/lib/data/app_state.dart @@ -32,9 +32,9 @@ class AppState extends Model { .where((v) => v.name.toLowerCase().contains(terms.toLowerCase())) .toList(); - void toggleFavorite(int id) { + void setFavorite(int id, bool isFavorite) { Veggie veggie = getVeggie(id); - veggie.isFavorite = !veggie.isFavorite; + veggie.isFavorite = isFavorite; notifyListeners(); } diff --git a/veggieseasons/lib/screens/details.dart b/veggieseasons/lib/screens/details.dart index 112dda4f6..80740cb64 100644 --- a/veggieseasons/lib/screens/details.dart +++ b/veggieseasons/lib/screens/details.dart @@ -8,14 +8,15 @@ import 'package:scoped_model/scoped_model.dart'; import 'package:veggieseasons/data/app_state.dart'; import 'package:veggieseasons/data/veggie.dart'; import 'package:veggieseasons/styles.dart'; +import 'package:veggieseasons/widgets/close_button.dart'; /// A circular widget that represents a season of the year. /// -/// The season can be displayed as a valid harvest season or one during which a +/// The season can be displayed as a valid harvest time or one during which a /// particular veggie cannot be harvested. Bright colors are used in the first /// case, and grays in the latter. class SeasonCircle extends StatelessWidget { - SeasonCircle(this.season, this.isHarvestTime); + const SeasonCircle(this.season, this.isHarvestTime); /// Season to be displayed by this widget. final Season season; @@ -62,25 +63,7 @@ class DetailsScreen extends StatelessWidget { DetailsScreen(this.id); - Widget _createFavoriteButton(bool isFav, VoidCallback onPressed) { - return CupertinoButton( - color: Styles.buttonColor, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - isFav ? Styles.checkedIcon : Styles.uncheckedIcon, - color: Styles.buttonIconColor, - ), - SizedBox(width: 4.0), - Text(isFav ? 'Saved to Garden' : 'Save to Garden'), - ], - ), - onPressed: onPressed, - ); - } - - Widget _createHeader(AppState model) { + Widget _createHeader(BuildContext context, AppState model) { final veggie = model.getVeggie(id); return SizedBox( @@ -90,9 +73,21 @@ class DetailsScreen extends StatelessWidget { Positioned( right: 0.0, left: 0.0, - child: Image.asset( - veggie.imageAssetPath, - fit: BoxFit.cover, + child: Hero( + tag: veggie.id, + child: Image.asset( + veggie.imageAssetPath, + fit: BoxFit.cover, + ), + ), + ), + Positioned( + top: 0.0, + right: 16.0, + child: SafeArea( + child: CloseButton(() { + Navigator.of(context).pop(); + }), ), ), Positioned( @@ -125,33 +120,38 @@ class DetailsScreen extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + Wrap( + children: Season.values.map((s) { + return SeasonCircle(s, veggie.seasons.contains(s)); + }).toList(), + ), + SizedBox(height: 8.0), Row( - mainAxisSize: MainAxisSize.max, + mainAxisSize: MainAxisSize.min, children: [ - Wrap( - children: Season.values.map((s) { - return SeasonCircle(s, veggie.seasons.contains(s)); - }).toList(), + CupertinoSwitch( + value: veggie.isFavorite, + onChanged: (value) { + model.setFavorite(id, value); + }, ), SizedBox(width: 8.0), - Expanded( - child: Align( - alignment: Alignment.centerRight, - child: Text( - veggieCategoryNames[veggie.category].toUpperCase(), - style: Styles.minorText, - ), - ), - ), + Text('Save to Garden'), ], ), + SizedBox(height: 24.0), + Align( + alignment: Alignment.centerRight, + child: Text( + veggieCategoryNames[veggie.category].toUpperCase(), + style: Styles.minorText, + ), + ), + SizedBox(width: 8.0), Padding( padding: const EdgeInsets.symmetric(vertical: 10.0), child: Text(veggie.shortDescription), ), - _createFavoriteButton(veggie.isFavorite, () { - model.toggleFavorite(veggie.id); - }), ], ), ); @@ -162,13 +162,10 @@ class DetailsScreen extends StatelessWidget { final model = ScopedModel.of(context, rebuildOnChange: true); return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Text('Details'), - ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _createHeader(model), + _createHeader(context, model), _createDetails(model), ], ), diff --git a/veggieseasons/lib/screens/favorites.dart b/veggieseasons/lib/screens/favorites.dart index 55afce8f2..be40f733a 100644 --- a/veggieseasons/lib/screens/favorites.dart +++ b/veggieseasons/lib/screens/favorites.dart @@ -13,7 +13,7 @@ import 'package:veggieseasons/widgets/veggie_headline.dart'; class FavoritesScreen extends StatelessWidget { /// Builds the "content" of the favorites screen: either a list of favorite /// veggies or a note that says the user hasn't favorited any yet. - Widget _buildScaffoldBody(BuildContext context) { + Widget _buildTabViewBody(BuildContext context) { final model = ScopedModel.of(context, rebuildOnChange: true); if (model.favoriteVeggies.length == 0) { @@ -46,14 +46,17 @@ class FavoritesScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Text('Your Garden'), - ), - backgroundColor: Styles.scaffoldBackground, - child: Center( - child: _buildScaffoldBody(context), - ), + return CupertinoTabView( + builder: (context) { + return DecoratedBox( + decoration: BoxDecoration( + color: Styles.scaffoldBackground, + ), + child: Center( + child: _buildTabViewBody(context), + ), + ); + }, ); } } diff --git a/veggieseasons/lib/styles.dart b/veggieseasons/lib/styles.dart index 62d36472f..46082da83 100644 --- a/veggieseasons/lib/styles.dart +++ b/veggieseasons/lib/styles.dart @@ -91,12 +91,14 @@ abstract class Styles { static const scaffoldBackground = Color(0xfff0f0f0); - static const buttonColor = Color(0xff007aff); - - static const buttonIconColor = Color(0xffffffff); - static const searchBackground = Color(0xffe0e0e0); + static const frostedBackground = Color(0xccf8f8f8); + + static const closeButtonUnpressed = Color(0xff101010); + + static const closeButtonPressed = Color(0xff808080); + static const TextStyle searchText = TextStyle( color: Color.fromRGBO(0, 0, 0, 1.0), fontFamily: 'NotoSans', diff --git a/veggieseasons/lib/widgets/close_button.dart b/veggieseasons/lib/widgets/close_button.dart new file mode 100644 index 000000000..1f77c6061 --- /dev/null +++ b/veggieseasons/lib/widgets/close_button.dart @@ -0,0 +1,131 @@ +// Copyright 2018 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/widgets.dart'; +import 'package:veggieseasons/styles.dart'; + +/// Partially overlays and then blurs its child. +class FrostedBox extends StatelessWidget { + const FrostedBox({ + this.child, + Key key, + }) : super(key: key); + + final Widget child; + + @override + Widget build(BuildContext context) { + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), + child: DecoratedBox( + decoration: BoxDecoration( + color: Styles.frostedBackground, + ), + child: child, + ), + ); + } +} + +/// An Icon that implicitly animates changes to its color. +class ColorChangingIcon extends ImplicitlyAnimatedWidget { + const ColorChangingIcon( + this.icon, { + this.color = CupertinoColors.black, + this.size, + @required Duration duration, + Key key, + }) : assert(icon != null), + assert(color != null), + assert(duration != null), + super(key: key, duration: duration); + + final Color color; + + final IconData icon; + + final double size; + + @override + _ColorChangingIconState createState() => _ColorChangingIconState(); +} + +class _ColorChangingIconState + extends AnimatedWidgetBaseState { + ColorTween _colorTween; + + @override + Widget build(BuildContext context) { + return Icon( + widget.icon, + size: widget.size, + color: _colorTween?.evaluate(animation), + ); + } + + @override + void forEachTween(visitor) { + _colorTween = visitor( + _colorTween, + widget.color, + (dynamic value) => ColorTween(begin: value), + ); + } +} + +/// A simple "close this modal" button that invokes a callback when pressed. +class CloseButton extends StatefulWidget { + const CloseButton(this.onPressed); + + final VoidCallback onPressed; + + @override + CloseButtonState createState() { + return new CloseButtonState(); + } +} + +class CloseButtonState extends State { + bool tapInProgress = false; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTapDown: (details) { + setState(() => tapInProgress = true); + }, + onTapUp: (details) { + setState(() => tapInProgress = false); + widget.onPressed(); + }, + onTapCancel: () { + setState(() => tapInProgress = false); + }, + child: ClipOval( + child: FrostedBox( + child: Container( + width: 30, + height: 30, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + ), + child: Center( + child: ColorChangingIcon( + CupertinoIcons.clear_thick, + duration: Duration(milliseconds: 300), + color: tapInProgress + ? Styles.closeButtonPressed + : Styles.closeButtonUnpressed, + size: 20, + ), + ), + ), + ), + ), + ); + } +} diff --git a/veggieseasons/lib/widgets/search_bar.dart b/veggieseasons/lib/widgets/search_bar.dart index 5eebe6d57..55def0d8e 100644 --- a/veggieseasons/lib/widgets/search_bar.dart +++ b/veggieseasons/lib/widgets/search_bar.dart @@ -34,7 +34,7 @@ class SearchBar extends StatelessWidget { color: Styles.searchIconColor, ), Expanded( - child: EditableText( + child: CupertinoTextField( controller: controller, focusNode: focusNode, style: Styles.searchText, diff --git a/veggieseasons/lib/widgets/veggie_headline.dart b/veggieseasons/lib/widgets/veggie_headline.dart index 110a5bf47..ea0f5a9db 100644 --- a/veggieseasons/lib/widgets/veggie_headline.dart +++ b/veggieseasons/lib/widgets/veggie_headline.dart @@ -35,8 +35,10 @@ class VeggieHeadline extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onTap: () => Navigator.of(context).push( - CupertinoPageRoute(builder: (context) => DetailsScreen(veggie.id))), + onTap: () => Navigator.of(context).push(CupertinoPageRoute( + builder: (context) => DetailsScreen(veggie.id), + fullscreenDialog: true, + )), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [