mirror of
https://github.com/flutter/samples.git
synced 2025-11-11 07:18:15 +00:00
State Restoration support for Veggie Seasons app (#433)
This commit is contained in:
committed by
GitHub
parent
d30bfd59ec
commit
ed1503143e
132
experimental/veggieseasons/lib/widgets/close_button.dart
Normal file
132
experimental/veggieseasons/lib/widgets/close_button.dart
Normal file
@@ -0,0 +1,132 @@
|
||||
// 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's background.
|
||||
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, sigmaY: 10),
|
||||
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,
|
||||
semanticLabel: 'Close button',
|
||||
size: widget.size,
|
||||
color: _colorTween?.evaluate(animation),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void forEachTween(TweenVisitor<dynamic> visitor) {
|
||||
_colorTween = visitor(
|
||||
_colorTween,
|
||||
widget.color,
|
||||
(dynamic value) => ColorTween(begin: value as Color),
|
||||
) as ColorTween;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
72
experimental/veggieseasons/lib/widgets/search_bar.dart
Normal file
72
experimental/veggieseasons/lib/widgets/search_bar.dart
Normal file
@@ -0,0 +1,72 @@
|
||||
// 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';
|
||||
|
||||
class SearchBar extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
final FocusNode focusNode;
|
||||
|
||||
SearchBar({
|
||||
@required this.controller,
|
||||
@required this.focusNode,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final themeData = CupertinoTheme.of(context);
|
||||
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: BackdropFilter(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Styles.searchBackground(themeData),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
ExcludeSemantics(
|
||||
child: Icon(
|
||||
CupertinoIcons.search,
|
||||
color: Styles.searchIconColor,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: CupertinoTextField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
decoration: null,
|
||||
style: Styles.searchText(themeData),
|
||||
cursorColor: Styles.searchCursorColor,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
controller.clear();
|
||||
},
|
||||
child: Icon(
|
||||
CupertinoIcons.clear_thick_circled,
|
||||
semanticLabel: 'Clear search terms',
|
||||
color: Styles.searchIconColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
filter: ImageFilter.blur(sigmaY: 5, sigmaX: 5),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
121
experimental/veggieseasons/lib/widgets/settings_group.dart
Normal file
121
experimental/veggieseasons/lib/widgets/settings_group.dart
Normal file
@@ -0,0 +1,121 @@
|
||||
// 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 'package:flutter/cupertino.dart';
|
||||
import 'package:veggieseasons/styles.dart';
|
||||
|
||||
import 'settings_item.dart';
|
||||
|
||||
// The widgets in this file present a group of Cupertino-style settings items to
|
||||
// the user. In the future, the Cupertino package in the Flutter SDK will
|
||||
// include dedicated widgets for this purpose, but for now they're done here.
|
||||
//
|
||||
// See https://github.com/flutter/flutter/projects/29 for more info.
|
||||
|
||||
class SettingsGroupHeader extends StatelessWidget {
|
||||
const SettingsGroupHeader(this.title);
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 15,
|
||||
right: 15,
|
||||
bottom: 6,
|
||||
),
|
||||
child: Text(
|
||||
title.toUpperCase(),
|
||||
style: TextStyle(
|
||||
color: CupertinoColors.inactiveGray,
|
||||
fontSize: 13.5,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsGroupFooter extends StatelessWidget {
|
||||
const SettingsGroupFooter(this.title);
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 15,
|
||||
right: 15,
|
||||
top: 7.5,
|
||||
),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: Styles.settingsGroupSubtitle,
|
||||
fontSize: 13,
|
||||
letterSpacing: -0.08,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsGroup extends StatelessWidget {
|
||||
SettingsGroup({
|
||||
@required this.items,
|
||||
this.header,
|
||||
this.footer,
|
||||
}) : assert(items != null),
|
||||
assert(items.isNotEmpty);
|
||||
|
||||
final List<SettingsItem> items;
|
||||
final Widget header;
|
||||
final Widget footer;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var brightness = CupertinoTheme.brightnessOf(context);
|
||||
final dividedItems = <Widget>[items[0]];
|
||||
for (var i = 1; i < items.length; i++) {
|
||||
dividedItems.add(Container(
|
||||
color: Styles.settingsLineation(brightness),
|
||||
height: 0.3,
|
||||
));
|
||||
dividedItems.add(items[i]);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: header == null ? 35 : 22,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (header != null) header,
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoColors.white,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: Styles.settingsLineation(brightness),
|
||||
width: 0,
|
||||
),
|
||||
bottom: BorderSide(
|
||||
color: Styles.settingsLineation(brightness),
|
||||
width: 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: dividedItems,
|
||||
),
|
||||
),
|
||||
if (footer != null) footer,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
167
experimental/veggieseasons/lib/widgets/settings_item.dart
Normal file
167
experimental/veggieseasons/lib/widgets/settings_item.dart
Normal file
@@ -0,0 +1,167 @@
|
||||
// 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:async';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:veggieseasons/styles.dart';
|
||||
|
||||
// The widgets in this file present a Cupertino-style settings item to the user.
|
||||
// In the future, the Cupertino package in the Flutter SDK will include
|
||||
// dedicated widgets for this purpose, but for now they're done here.
|
||||
//
|
||||
// See https://github.com/flutter/flutter/projects/29 for more info.
|
||||
|
||||
typedef SettingsItemCallback = FutureOr<void> Function();
|
||||
|
||||
class SettingsNavigationIndicator extends StatelessWidget {
|
||||
const SettingsNavigationIndicator({Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Icon(
|
||||
CupertinoIcons.forward,
|
||||
color: Styles.settingsMediumGray,
|
||||
size: 21,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsIcon extends StatelessWidget {
|
||||
const SettingsIcon({
|
||||
@required this.icon,
|
||||
this.foregroundColor = CupertinoColors.white,
|
||||
this.backgroundColor = CupertinoColors.black,
|
||||
Key key,
|
||||
}) : assert(icon != null),
|
||||
super(key: key);
|
||||
|
||||
final Color backgroundColor;
|
||||
final Color foregroundColor;
|
||||
final IconData icon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
color: backgroundColor,
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
icon,
|
||||
color: foregroundColor,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsItem extends StatefulWidget {
|
||||
const SettingsItem({
|
||||
@required this.label,
|
||||
this.icon,
|
||||
this.content,
|
||||
this.subtitle,
|
||||
this.onPress,
|
||||
Key key,
|
||||
}) : assert(label != null),
|
||||
super(key: key);
|
||||
|
||||
final String label;
|
||||
final Widget icon;
|
||||
final Widget content;
|
||||
final String subtitle;
|
||||
final SettingsItemCallback onPress;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => SettingsItemState();
|
||||
}
|
||||
|
||||
class SettingsItemState extends State<SettingsItem> {
|
||||
bool pressed = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var themeData = CupertinoTheme.of(context);
|
||||
var brightness = CupertinoTheme.brightnessOf(context);
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
color: Styles.settingsItemColor(brightness),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () async {
|
||||
if (widget.onPress != null) {
|
||||
setState(() {
|
||||
pressed = true;
|
||||
});
|
||||
await widget.onPress();
|
||||
Future.delayed(
|
||||
Duration(milliseconds: 150),
|
||||
() {
|
||||
setState(() {
|
||||
pressed = false;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
child: SizedBox(
|
||||
height: widget.subtitle == null ? 44 : 57,
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.icon != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 15,
|
||||
bottom: 2,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 29,
|
||||
width: 29,
|
||||
child: widget.icon,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 15,
|
||||
),
|
||||
child: widget.subtitle != null
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
SizedBox(height: 8.5),
|
||||
Text(
|
||||
widget.label,
|
||||
style: Styles.settingsItemText(themeData),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
widget.subtitle,
|
||||
style: Styles.settingsItemSubtitleText(themeData),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Padding(
|
||||
padding: EdgeInsets.only(top: 1.5),
|
||||
child: Text(
|
||||
widget.label,
|
||||
style: Styles.settingsItemText(themeData),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 11),
|
||||
child: widget.content ?? Container(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
258
experimental/veggieseasons/lib/widgets/trivia.dart
Normal file
258
experimental/veggieseasons/lib/widgets/trivia.dart
Normal file
@@ -0,0 +1,258 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:veggieseasons/data/app_state.dart';
|
||||
import 'package:veggieseasons/data/veggie.dart';
|
||||
import 'package:veggieseasons/styles.dart';
|
||||
|
||||
/// Presents a series of trivia questions about a particular widget, and tracks
|
||||
/// the user's score.
|
||||
class TriviaView extends StatefulWidget {
|
||||
final int id;
|
||||
final String restorationId;
|
||||
|
||||
const TriviaView({this.id, this.restorationId});
|
||||
|
||||
@override
|
||||
_TriviaViewState createState() => _TriviaViewState();
|
||||
}
|
||||
|
||||
/// Possible states of the game.
|
||||
enum PlayerStatus {
|
||||
readyToAnswer,
|
||||
wasCorrect,
|
||||
wasIncorrect,
|
||||
}
|
||||
|
||||
class _TriviaViewState extends State<TriviaView> with RestorationMixin {
|
||||
/// Current app state. This is used to fetch veggie data.
|
||||
AppState appState;
|
||||
|
||||
/// The veggie trivia about which to show.
|
||||
Veggie veggie;
|
||||
|
||||
/// Index of the current trivia question.
|
||||
RestorableInt triviaIndex = RestorableInt(0);
|
||||
|
||||
/// User's score on the current veggie.
|
||||
RestorableInt score = RestorableInt(0);
|
||||
|
||||
/// Trivia question currently being displayed.
|
||||
Trivia get currentTrivia => veggie.trivia[triviaIndex.value];
|
||||
|
||||
/// The current state of the game.
|
||||
_RestorablePlayerStatus status = _RestorablePlayerStatus(PlayerStatus.readyToAnswer);
|
||||
|
||||
@override
|
||||
String get restorationId => widget.restorationId;
|
||||
|
||||
@override
|
||||
void restoreState(RestorationBucket oldBucket, bool initialRestore) {
|
||||
registerForRestoration(triviaIndex, 'index');
|
||||
registerForRestoration(score, 'score');
|
||||
registerForRestoration(status, 'status');
|
||||
}
|
||||
|
||||
// Called at init and again if any dependencies (read: InheritedWidgets) on
|
||||
// on which this object relies are changed.
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
||||
final newAppState = Provider.of<AppState>(context);
|
||||
|
||||
setState(() {
|
||||
appState = newAppState;
|
||||
veggie = appState.getVeggie(widget.id);
|
||||
});
|
||||
}
|
||||
|
||||
// Called when the widget associated with this object is swapped out for a new
|
||||
// one. If the new widget has a different Veggie ID value, the state object
|
||||
// needs to do a little work to reset itself for the new Veggie.
|
||||
@override
|
||||
void didUpdateWidget(TriviaView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (oldWidget.id != widget.id) {
|
||||
setState(() {
|
||||
veggie = appState.getVeggie(widget.id);
|
||||
});
|
||||
|
||||
_resetGame();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
triviaIndex.dispose();
|
||||
score.dispose();
|
||||
status.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (triviaIndex.value >= veggie.trivia.length) {
|
||||
return _buildFinishedView();
|
||||
} else if (status.value == PlayerStatus.readyToAnswer) {
|
||||
return _buildQuestionView();
|
||||
} else {
|
||||
return _buildResultView();
|
||||
}
|
||||
}
|
||||
|
||||
void _resetGame() {
|
||||
setState(() {
|
||||
triviaIndex.value = 0;
|
||||
score.value = 0;
|
||||
status.value = PlayerStatus.readyToAnswer;
|
||||
});
|
||||
}
|
||||
|
||||
void _processAnswer(int answerIndex) {
|
||||
setState(() {
|
||||
if (answerIndex == currentTrivia.correctAnswerIndex) {
|
||||
status.value = PlayerStatus.wasCorrect;
|
||||
score.value++;
|
||||
} else {
|
||||
status.value = PlayerStatus.wasIncorrect;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Widget shown when the game is over. It includes the score and a button to
|
||||
// restart everything.
|
||||
Widget _buildFinishedView() {
|
||||
final themeData = CupertinoTheme.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'All done!',
|
||||
style: Styles.triviaFinishedTitleText(themeData),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'You answered',
|
||||
style: Styles.triviaFinishedText(themeData),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text(
|
||||
'$score',
|
||||
style: Styles.triviaFinishedBigText(themeData),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
' of ',
|
||||
style: Styles.triviaFinishedText(themeData),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${veggie.trivia.length}',
|
||||
style: Styles.triviaFinishedBigText(themeData),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
'questions correctly!',
|
||||
style: Styles.triviaFinishedText(themeData),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
CupertinoButton(
|
||||
child: Text('Try Again'),
|
||||
onPressed: () => _resetGame(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Presents the current trivia's question and answer choices.
|
||||
Widget _buildQuestionView() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
currentTrivia.question,
|
||||
style: CupertinoTheme.of(context).textTheme.textStyle,
|
||||
),
|
||||
SizedBox(height: 32),
|
||||
for (int i = 0; i < currentTrivia.answers.length; i++)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: CupertinoButton(
|
||||
color: CupertinoColors.activeBlue,
|
||||
child: Text(
|
||||
currentTrivia.answers[i],
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
onPressed: () => _processAnswer(i),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Shows whether the last answer was right or wrong and prompts the user to
|
||||
// continue through the game.
|
||||
Widget _buildResultView() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
status.value == PlayerStatus.wasCorrect
|
||||
? 'That\'s right!'
|
||||
: 'Sorry, that wasn\'t the right answer.',
|
||||
style: CupertinoTheme.of(context).textTheme.textStyle,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
CupertinoButton(
|
||||
child: Text('Next Question'),
|
||||
onPressed: () => setState(() {
|
||||
triviaIndex.value++;
|
||||
status.value = PlayerStatus.readyToAnswer;
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RestorablePlayerStatus extends RestorableValue<PlayerStatus> {
|
||||
_RestorablePlayerStatus(this._defaultValue);
|
||||
|
||||
final PlayerStatus _defaultValue;
|
||||
|
||||
@override
|
||||
PlayerStatus createDefaultValue() {
|
||||
return _defaultValue;
|
||||
}
|
||||
|
||||
@override
|
||||
PlayerStatus fromPrimitives(Object data) {
|
||||
return PlayerStatus.values[data as int];
|
||||
}
|
||||
|
||||
@override
|
||||
Object toPrimitives() {
|
||||
return value.index;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateValue(PlayerStatus oldValue) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
174
experimental/veggieseasons/lib/widgets/veggie_card.dart
Normal file
174
experimental/veggieseasons/lib/widgets/veggie_card.dart
Normal file
@@ -0,0 +1,174 @@
|
||||
// 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:veggieseasons/data/veggie.dart';
|
||||
import 'package:veggieseasons/screens/details.dart';
|
||||
import 'package:veggieseasons/styles.dart';
|
||||
|
||||
class FrostyBackground extends StatelessWidget {
|
||||
const FrostyBackground({
|
||||
this.color,
|
||||
this.intensity = 25,
|
||||
this.child,
|
||||
});
|
||||
|
||||
final Color color;
|
||||
final double intensity;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: intensity, sigmaY: intensity),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A Card-like Widget that responds to tap events by animating changes to its
|
||||
/// elevation and invoking an optional [onPressed] callback.
|
||||
class PressableCard extends StatefulWidget {
|
||||
const PressableCard({
|
||||
@required this.child,
|
||||
this.borderRadius = const BorderRadius.all(Radius.circular(5)),
|
||||
this.upElevation = 2,
|
||||
this.downElevation = 0,
|
||||
this.shadowColor = CupertinoColors.black,
|
||||
this.duration = const Duration(milliseconds: 100),
|
||||
this.onPressed,
|
||||
Key key,
|
||||
}) : assert(child != null),
|
||||
assert(borderRadius != null),
|
||||
assert(upElevation != null),
|
||||
assert(downElevation != null),
|
||||
assert(shadowColor != null),
|
||||
assert(duration != null),
|
||||
super(key: key);
|
||||
|
||||
final VoidCallback onPressed;
|
||||
|
||||
final Widget child;
|
||||
|
||||
final BorderRadius borderRadius;
|
||||
|
||||
final double upElevation;
|
||||
|
||||
final double downElevation;
|
||||
|
||||
final Color shadowColor;
|
||||
|
||||
final Duration duration;
|
||||
|
||||
@override
|
||||
_PressableCardState createState() => _PressableCardState();
|
||||
}
|
||||
|
||||
class _PressableCardState extends State<PressableCard> {
|
||||
bool cardIsDown = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
setState(() => cardIsDown = false);
|
||||
if (widget.onPressed != null) {
|
||||
widget.onPressed();
|
||||
}
|
||||
},
|
||||
onTapDown: (details) => setState(() => cardIsDown = true),
|
||||
onTapCancel: () => setState(() => cardIsDown = false),
|
||||
child: AnimatedPhysicalModel(
|
||||
elevation: cardIsDown ? widget.downElevation : widget.upElevation,
|
||||
borderRadius: widget.borderRadius,
|
||||
shape: BoxShape.rectangle,
|
||||
shadowColor: widget.shadowColor,
|
||||
duration: widget.duration,
|
||||
color: CupertinoColors.lightBackgroundGray,
|
||||
child: ClipRRect(
|
||||
borderRadius: widget.borderRadius,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VeggieCard extends StatelessWidget {
|
||||
VeggieCard(this.veggie, this.isInSeason, this.isPreferredCategory);
|
||||
|
||||
/// Veggie to be displayed by the card.
|
||||
final Veggie veggie;
|
||||
|
||||
/// If the veggie is in season, it's displayed more prominently and the
|
||||
/// image is fully saturated. Otherwise, it's reduced and de-saturated.
|
||||
final bool isInSeason;
|
||||
|
||||
/// Whether [veggie] falls into one of user's preferred [VeggieCategory]s
|
||||
final bool isPreferredCategory;
|
||||
|
||||
Widget _buildDetails() {
|
||||
return FrostyBackground(
|
||||
color: Color(0x90ffffff),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
veggie.name,
|
||||
style: Styles.cardTitleText,
|
||||
),
|
||||
Text(
|
||||
veggie.shortDescription,
|
||||
style: Styles.cardDescriptionText,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PressableCard(
|
||||
onPressed: () => DetailsScreen.show(Navigator.of(context), veggie.id),
|
||||
child: Stack(
|
||||
children: [
|
||||
Semantics(
|
||||
label: 'A card background featuring ${veggie.name}',
|
||||
child: Container(
|
||||
height: isInSeason ? 300 : 150,
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
colorFilter:
|
||||
isInSeason ? null : Styles.desaturatedColorFilter,
|
||||
image: AssetImage(
|
||||
veggie.imageAssetPath,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: _buildDetails(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
109
experimental/veggieseasons/lib/widgets/veggie_headline.dart
Normal file
109
experimental/veggieseasons/lib/widgets/veggie_headline.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
// 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 'package:flutter/cupertino.dart';
|
||||
import 'package:veggieseasons/data/veggie.dart';
|
||||
import 'package:veggieseasons/screens/details.dart';
|
||||
import 'package:veggieseasons/styles.dart';
|
||||
|
||||
class ZoomClipAssetImage extends StatelessWidget {
|
||||
const ZoomClipAssetImage(
|
||||
{@required this.zoom,
|
||||
this.height,
|
||||
this.width,
|
||||
@required this.imageAsset});
|
||||
|
||||
final double zoom;
|
||||
final double height;
|
||||
final double width;
|
||||
final String imageAsset;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: height,
|
||||
width: width,
|
||||
alignment: Alignment.center,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: OverflowBox(
|
||||
maxHeight: height * zoom,
|
||||
maxWidth: width * zoom,
|
||||
child: Image.asset(
|
||||
imageAsset,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VeggieHeadline extends StatelessWidget {
|
||||
final Veggie veggie;
|
||||
|
||||
const VeggieHeadline(this.veggie);
|
||||
|
||||
List<Widget> _buildSeasonDots(List<Season> seasons) {
|
||||
var widgets = <Widget>[];
|
||||
|
||||
for (var season in seasons) {
|
||||
widgets.add(SizedBox(width: 4));
|
||||
widgets.add(
|
||||
Container(
|
||||
height: 10,
|
||||
width: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: Styles.seasonColors[season],
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return widgets;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final themeData = CupertinoTheme.of(context);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => DetailsScreen.show(Navigator.of(context), veggie.id),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ZoomClipAssetImage(
|
||||
imageAsset: veggie.imageAssetPath,
|
||||
zoom: 2.4,
|
||||
height: 72,
|
||||
width: 72,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
veggie.name,
|
||||
style: Styles.headlineName(themeData),
|
||||
),
|
||||
..._buildSeasonDots(veggie.seasons),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
veggie.shortDescription,
|
||||
style: Styles.headlineDescription(themeData),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user