mirror of
https://github.com/flutter/samples.git
synced 2025-11-08 22:09:06 +00:00
Updates detail screen to new, modal design. (#40)
This commit is contained in:
@@ -32,9 +32,9 @@ class AppState extends Model {
|
|||||||
.where((v) => v.name.toLowerCase().contains(terms.toLowerCase()))
|
.where((v) => v.name.toLowerCase().contains(terms.toLowerCase()))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
void toggleFavorite(int id) {
|
void setFavorite(int id, bool isFavorite) {
|
||||||
Veggie veggie = getVeggie(id);
|
Veggie veggie = getVeggie(id);
|
||||||
veggie.isFavorite = !veggie.isFavorite;
|
veggie.isFavorite = isFavorite;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,15 @@ import 'package:scoped_model/scoped_model.dart';
|
|||||||
import 'package:veggieseasons/data/app_state.dart';
|
import 'package:veggieseasons/data/app_state.dart';
|
||||||
import 'package:veggieseasons/data/veggie.dart';
|
import 'package:veggieseasons/data/veggie.dart';
|
||||||
import 'package:veggieseasons/styles.dart';
|
import 'package:veggieseasons/styles.dart';
|
||||||
|
import 'package:veggieseasons/widgets/close_button.dart';
|
||||||
|
|
||||||
/// A circular widget that represents a season of the year.
|
/// 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
|
/// particular veggie cannot be harvested. Bright colors are used in the first
|
||||||
/// case, and grays in the latter.
|
/// case, and grays in the latter.
|
||||||
class SeasonCircle extends StatelessWidget {
|
class SeasonCircle extends StatelessWidget {
|
||||||
SeasonCircle(this.season, this.isHarvestTime);
|
const SeasonCircle(this.season, this.isHarvestTime);
|
||||||
|
|
||||||
/// Season to be displayed by this widget.
|
/// Season to be displayed by this widget.
|
||||||
final Season season;
|
final Season season;
|
||||||
@@ -62,25 +63,7 @@ class DetailsScreen extends StatelessWidget {
|
|||||||
|
|
||||||
DetailsScreen(this.id);
|
DetailsScreen(this.id);
|
||||||
|
|
||||||
Widget _createFavoriteButton(bool isFav, VoidCallback onPressed) {
|
Widget _createHeader(BuildContext context, AppState model) {
|
||||||
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) {
|
|
||||||
final veggie = model.getVeggie(id);
|
final veggie = model.getVeggie(id);
|
||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
@@ -90,11 +73,23 @@ class DetailsScreen extends StatelessWidget {
|
|||||||
Positioned(
|
Positioned(
|
||||||
right: 0.0,
|
right: 0.0,
|
||||||
left: 0.0,
|
left: 0.0,
|
||||||
|
child: Hero(
|
||||||
|
tag: veggie.id,
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
veggie.imageAssetPath,
|
veggie.imageAssetPath,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 0.0,
|
||||||
|
right: 16.0,
|
||||||
|
child: SafeArea(
|
||||||
|
child: CloseButton(() {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 0.0,
|
bottom: 0.0,
|
||||||
left: 0.0,
|
left: 0.0,
|
||||||
@@ -124,34 +119,39 @@ class DetailsScreen extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.all(24.0),
|
padding: const EdgeInsets.all(24.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: [
|
children: [
|
||||||
Wrap(
|
Wrap(
|
||||||
children: Season.values.map((s) {
|
children: Season.values.map((s) {
|
||||||
return SeasonCircle(s, veggie.seasons.contains(s));
|
return SeasonCircle(s, veggie.seasons.contains(s));
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
|
SizedBox(height: 8.0),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
CupertinoSwitch(
|
||||||
|
value: veggie.isFavorite,
|
||||||
|
onChanged: (value) {
|
||||||
|
model.setFavorite(id, value);
|
||||||
|
},
|
||||||
|
),
|
||||||
SizedBox(width: 8.0),
|
SizedBox(width: 8.0),
|
||||||
Expanded(
|
Text('Save to Garden'),
|
||||||
child: Align(
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 24.0),
|
||||||
|
Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: Text(
|
child: Text(
|
||||||
veggieCategoryNames[veggie.category].toUpperCase(),
|
veggieCategoryNames[veggie.category].toUpperCase(),
|
||||||
style: Styles.minorText,
|
style: Styles.minorText,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
SizedBox(width: 8.0),
|
||||||
],
|
|
||||||
),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
||||||
child: Text(veggie.shortDescription),
|
child: Text(veggie.shortDescription),
|
||||||
),
|
),
|
||||||
_createFavoriteButton(veggie.isFavorite, () {
|
|
||||||
model.toggleFavorite(veggie.id);
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -162,13 +162,10 @@ class DetailsScreen extends StatelessWidget {
|
|||||||
final model = ScopedModel.of<AppState>(context, rebuildOnChange: true);
|
final model = ScopedModel.of<AppState>(context, rebuildOnChange: true);
|
||||||
|
|
||||||
return CupertinoPageScaffold(
|
return CupertinoPageScaffold(
|
||||||
navigationBar: CupertinoNavigationBar(
|
|
||||||
middle: Text('Details'),
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
_createHeader(model),
|
_createHeader(context, model),
|
||||||
_createDetails(model),
|
_createDetails(model),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import 'package:veggieseasons/widgets/veggie_headline.dart';
|
|||||||
class FavoritesScreen extends StatelessWidget {
|
class FavoritesScreen extends StatelessWidget {
|
||||||
/// Builds the "content" of the favorites screen: either a list of favorite
|
/// 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.
|
/// 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<AppState>(context, rebuildOnChange: true);
|
final model = ScopedModel.of<AppState>(context, rebuildOnChange: true);
|
||||||
|
|
||||||
if (model.favoriteVeggies.length == 0) {
|
if (model.favoriteVeggies.length == 0) {
|
||||||
@@ -46,14 +46,17 @@ class FavoritesScreen extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return CupertinoPageScaffold(
|
return CupertinoTabView(
|
||||||
navigationBar: CupertinoNavigationBar(
|
builder: (context) {
|
||||||
middle: Text('Your Garden'),
|
return DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Styles.scaffoldBackground,
|
||||||
),
|
),
|
||||||
backgroundColor: Styles.scaffoldBackground,
|
|
||||||
child: Center(
|
child: Center(
|
||||||
child: _buildScaffoldBody(context),
|
child: _buildTabViewBody(context),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,12 +91,14 @@ abstract class Styles {
|
|||||||
|
|
||||||
static const scaffoldBackground = Color(0xfff0f0f0);
|
static const scaffoldBackground = Color(0xfff0f0f0);
|
||||||
|
|
||||||
static const buttonColor = Color(0xff007aff);
|
|
||||||
|
|
||||||
static const buttonIconColor = Color(0xffffffff);
|
|
||||||
|
|
||||||
static const searchBackground = Color(0xffe0e0e0);
|
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(
|
static const TextStyle searchText = TextStyle(
|
||||||
color: Color.fromRGBO(0, 0, 0, 1.0),
|
color: Color.fromRGBO(0, 0, 0, 1.0),
|
||||||
fontFamily: 'NotoSans',
|
fontFamily: 'NotoSans',
|
||||||
|
|||||||
131
veggieseasons/lib/widgets/close_button.dart
Normal file
131
veggieseasons/lib/widgets/close_button.dart
Normal file
@@ -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<ColorChangingIcon> {
|
||||||
|
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<CloseButton> {
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ class SearchBar extends StatelessWidget {
|
|||||||
color: Styles.searchIconColor,
|
color: Styles.searchIconColor,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: EditableText(
|
child: CupertinoTextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
style: Styles.searchText,
|
style: Styles.searchText,
|
||||||
|
|||||||
@@ -35,8 +35,10 @@ class VeggieHeadline extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => Navigator.of(context).push(
|
onTap: () => Navigator.of(context).push(CupertinoPageRoute(
|
||||||
CupertinoPageRoute(builder: (context) => DetailsScreen(veggie.id))),
|
builder: (context) => DetailsScreen(veggie.id),
|
||||||
|
fullscreenDialog: true,
|
||||||
|
)),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
Reference in New Issue
Block a user