Skip to content

Commit 6256c6c

Browse files
committed
[104] Completed code.
1 parent 0e89f76 commit 6256c6c

File tree

6 files changed

+461
-97
lines changed

6 files changed

+461
-97
lines changed

mdc_100_series/lib/app.dart

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,44 +14,68 @@
1414

1515
import 'package:flutter/material.dart';
1616

17+
import 'backdrop.dart';
1718
import 'colors.dart';
1819
import 'home.dart';
1920
import 'login.dart';
21+
import 'category_menu_page.dart';
22+
import 'model/product.dart';
2023
import 'supplemental/cut_corners_border.dart';
2124

22-
// TODO: Convert ShrineApp to stateful widget (104)
23-
class ShrineApp extends StatelessWidget {
25+
class ShrineApp extends StatefulWidget {
26+
@override
27+
_ShrineAppState createState() => _ShrineAppState();
28+
}
29+
30+
class _ShrineAppState extends State<ShrineApp> {
31+
Category _currentCategory = Category.all;
2432

2533
@override
2634
Widget build(BuildContext context) {
2735
return MaterialApp(
2836
title: 'Shrine',
29-
// TODO: Change home: to a Backdrop with a HomePage frontLayer (104)
30-
home: HomePage(),
31-
// TODO: Make currentCategory field take _currentCategory (104)
32-
// TODO: Pass _currentCategory for frontLayer (104)
33-
// TODO: Change backLayer field value to CategoryMenuPage (104)
37+
home: Backdrop(
38+
currentCategory: _currentCategory,
39+
frontLayer: HomePage(category: _currentCategory),
40+
backLayer: CategoryMenuPage(
41+
currentCategory: _currentCategory,
42+
onCategoryTap: _onCategoryTap,
43+
),
44+
frontTitle: Text('SHRINE'),
45+
backTitle: Text('MENU'),
46+
),
3447
initialRoute: '/login',
3548
onGenerateRoute: _getRoute,
3649
theme: _kShrineTheme,
3750
);
3851
}
3952

40-
Route<dynamic> _getRoute(RouteSettings settings) {
41-
if (settings.name != '/login') {
42-
return null;
43-
}
53+
/// Function to call when a [Category] is tapped.
54+
void _onCategoryTap(Category category) {
55+
setState(() {
56+
_currentCategory = category;
57+
});
58+
}
59+
}
4460

45-
return MaterialPageRoute<void>(
46-
settings: settings,
47-
builder: (BuildContext context) => LoginPage(),
48-
fullscreenDialog: true,
49-
);
61+
Route<dynamic> _getRoute(RouteSettings settings) {
62+
if (settings.name != '/login') {
63+
return null;
5064
}
65+
66+
return MaterialPageRoute<void>(
67+
settings: settings,
68+
builder: (BuildContext context) => LoginPage(),
69+
fullscreenDialog: true,
70+
);
5171
}
5272

5373
final ThemeData _kShrineTheme = _buildShrineTheme();
5474

75+
IconThemeData _customIconTheme(IconThemeData original) {
76+
return original.copyWith(color: kShrineBrown900);
77+
}
78+
5579
ThemeData _buildShrineTheme() {
5680
final ThemeData base = ThemeData.light();
5781
return base.copyWith(
@@ -65,15 +89,14 @@ ThemeData _buildShrineTheme() {
6589
buttonTheme: ButtonThemeData(
6690
textTheme: ButtonTextTheme.accent,
6791
),
68-
primaryIconTheme: base.iconTheme.copyWith(
69-
color: kShrineBrown900
70-
),
92+
primaryIconTheme: base.iconTheme.copyWith(color: kShrineBrown900),
7193
inputDecorationTheme: InputDecorationTheme(
7294
border: CutCornersBorder(),
7395
),
7496
textTheme: _buildShrineTextTheme(base.textTheme),
7597
primaryTextTheme: _buildShrineTextTheme(base.primaryTextTheme),
7698
accentTextTheme: _buildShrineTextTheme(base.accentTextTheme),
99+
iconTheme: _customIconTheme(base.iconTheme),
77100
);
78101
}
79102

mdc_100_series/lib/backdrop.dart

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
// Copyright 2018-present the Flutter authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import 'package:flutter/material.dart';
16+
import 'package:meta/meta.dart';
17+
18+
import 'model/product.dart';
19+
import 'login.dart';
20+
21+
const double _kFlingVelocity = 2.0;
22+
23+
class _FrontLayer extends StatelessWidget {
24+
const _FrontLayer({
25+
Key key,
26+
this.onTap,
27+
this.child,
28+
}) : super(key: key);
29+
30+
final VoidCallback onTap;
31+
final Widget child;
32+
33+
@override
34+
Widget build(BuildContext context) {
35+
return Material(
36+
elevation: 16.0,
37+
shape: BeveledRectangleBorder(
38+
borderRadius: BorderRadius.only(topLeft: Radius.circular(46.0)),
39+
),
40+
child: Column(
41+
crossAxisAlignment: CrossAxisAlignment.stretch,
42+
children: <Widget>[
43+
GestureDetector(
44+
behavior: HitTestBehavior.opaque,
45+
onTap: onTap,
46+
child: Container(
47+
height: 40.0,
48+
alignment: AlignmentDirectional.centerStart,
49+
),
50+
),
51+
Expanded(
52+
child: child,
53+
),
54+
],
55+
),
56+
);
57+
}
58+
}
59+
60+
class _BackdropTitle extends AnimatedWidget {
61+
final Function onPress;
62+
final Widget frontTitle;
63+
final Widget backTitle;
64+
65+
const _BackdropTitle({
66+
Key key,
67+
Listenable listenable,
68+
this.onPress,
69+
@required this.frontTitle,
70+
@required this.backTitle,
71+
}) : assert(frontTitle != null),
72+
assert(backTitle != null),
73+
super(key: key, listenable: listenable);
74+
75+
@override
76+
Widget build(BuildContext context) {
77+
final Animation<double> animation = this.listenable;
78+
79+
return DefaultTextStyle(
80+
style: Theme.of(context).primaryTextTheme.title,
81+
softWrap: false,
82+
overflow: TextOverflow.ellipsis,
83+
child: Row(children: <Widget>[
84+
// branded icon
85+
SizedBox(
86+
width: 72.0,
87+
child: IconButton(
88+
padding: EdgeInsets.only(right: 8.0),
89+
onPressed: this.onPress,
90+
icon: Stack(children: <Widget>[
91+
Opacity(
92+
opacity: animation.value,
93+
child: ImageIcon(AssetImage('assets/slanted_menu.png')),
94+
),
95+
FractionalTranslation(
96+
translation: Tween<Offset>(
97+
begin: Offset.zero,
98+
end: Offset(1.0, 0.0),
99+
).evaluate(animation),
100+
child: ImageIcon(AssetImage('assets/diamond.png')),
101+
)]),
102+
),
103+
),
104+
// Here, we do a custom cross fade between backTitle and frontTitle.
105+
// This makes a smooth animation between the two texts.
106+
Stack(
107+
children: <Widget>[
108+
Opacity(
109+
opacity: CurvedAnimation(
110+
parent: ReverseAnimation(animation),
111+
curve: Interval(0.5, 1.0),
112+
).value,
113+
child: FractionalTranslation(
114+
translation: Tween<Offset>(
115+
begin: Offset.zero,
116+
end: Offset(0.5, 0.0),
117+
).evaluate(animation),
118+
child: Semantics(
119+
label: 'hide categories menu',
120+
child: ExcludeSemantics(child: backTitle)
121+
),
122+
),
123+
),
124+
Opacity(
125+
opacity: CurvedAnimation(
126+
parent: animation,
127+
curve: Interval(0.5, 1.0),
128+
).value,
129+
child: FractionalTranslation(
130+
translation: Tween<Offset>(
131+
begin: Offset(-0.25, 0.0),
132+
end: Offset.zero,
133+
).evaluate(animation),
134+
child: Semantics(
135+
label: 'show categories menu',
136+
child: ExcludeSemantics(child: frontTitle)
137+
),
138+
),
139+
),
140+
],
141+
)
142+
]),
143+
);
144+
}
145+
}
146+
147+
/// Builds a Backdrop.
148+
///
149+
/// A Backdrop widget has two layers, front and back. The front layer is shown
150+
/// by default, and slides down to show the back layer, from which a user
151+
/// can make a selection. The user can also configure the titles for when the
152+
/// front or back layer is showing.
153+
class Backdrop extends StatefulWidget {
154+
final Category currentCategory;
155+
final Widget frontLayer;
156+
final Widget backLayer;
157+
final Widget frontTitle;
158+
final Widget backTitle;
159+
160+
const Backdrop({
161+
@required this.currentCategory,
162+
@required this.frontLayer,
163+
@required this.backLayer,
164+
@required this.frontTitle,
165+
@required this.backTitle,
166+
}) : assert(currentCategory != null),
167+
assert(frontLayer != null),
168+
assert(backLayer != null),
169+
assert(frontTitle != null),
170+
assert(backTitle != null);
171+
172+
@override
173+
_BackdropState createState() => _BackdropState();
174+
}
175+
176+
class _BackdropState extends State<Backdrop>
177+
with SingleTickerProviderStateMixin {
178+
final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop');
179+
AnimationController _controller;
180+
181+
@override
182+
void initState() {
183+
super.initState();
184+
_controller = AnimationController(
185+
duration: Duration(milliseconds: 300),
186+
value: 1.0,
187+
vsync: this,
188+
);
189+
}
190+
191+
@override
192+
void didUpdateWidget(Backdrop old) {
193+
super.didUpdateWidget(old);
194+
195+
if (widget.currentCategory != old.currentCategory) {
196+
_toggleBackdropLayerVisibility();
197+
} else if (!_frontLayerVisible) {
198+
_controller.fling(velocity: _kFlingVelocity);
199+
}
200+
}
201+
202+
@override
203+
void dispose() {
204+
_controller.dispose();
205+
super.dispose();
206+
}
207+
208+
bool get _frontLayerVisible {
209+
final AnimationStatus status = _controller.status;
210+
return status == AnimationStatus.completed ||
211+
status == AnimationStatus.forward;
212+
}
213+
214+
void _toggleBackdropLayerVisibility() {
215+
_controller.fling(
216+
velocity: _frontLayerVisible ? -_kFlingVelocity : _kFlingVelocity);
217+
}
218+
219+
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
220+
const double layerTitleHeight = 48.0;
221+
final Size layerSize = constraints.biggest;
222+
final double layerTop = layerSize.height - layerTitleHeight;
223+
224+
Animation<RelativeRect> layerAnimation = RelativeRectTween(
225+
begin: RelativeRect.fromLTRB(
226+
0.0, layerTop, 0.0, layerTop - layerSize.height),
227+
end: RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0),
228+
).animate(_controller.view);
229+
230+
return Stack(
231+
key: _backdropKey,
232+
children: <Widget>[
233+
ExcludeSemantics(
234+
child: widget.backLayer,
235+
excluding: _frontLayerVisible,
236+
),
237+
PositionedTransition(
238+
rect: layerAnimation,
239+
child: _FrontLayer(
240+
onTap: _toggleBackdropLayerVisibility,
241+
child: widget.frontLayer,
242+
),
243+
),
244+
],
245+
);
246+
}
247+
248+
@override
249+
Widget build(BuildContext context) {
250+
var appBar = AppBar(
251+
brightness: Brightness.light,
252+
elevation: 0.0,
253+
titleSpacing: 0.0,
254+
title: _BackdropTitle(
255+
listenable: _controller.view,
256+
onPress: _toggleBackdropLayerVisibility,
257+
frontTitle: widget.frontTitle,
258+
backTitle: widget.backTitle,
259+
),
260+
actions: <Widget>[
261+
new IconButton(
262+
icon: Icon(
263+
Icons.search,
264+
semanticLabel: 'login',
265+
),
266+
onPressed: () {
267+
Navigator.push(
268+
context,
269+
MaterialPageRoute(builder: (BuildContext context) => LoginPage()),
270+
);
271+
},
272+
),
273+
new IconButton(
274+
icon: Icon(
275+
Icons.tune,
276+
semanticLabel: 'login',
277+
),
278+
onPressed: () {
279+
Navigator.push(
280+
context,
281+
MaterialPageRoute(builder: (BuildContext context) => LoginPage()),
282+
);
283+
},
284+
),
285+
],
286+
);
287+
return Scaffold(
288+
appBar: appBar,
289+
body: LayoutBuilder(
290+
builder: _buildStack,
291+
),
292+
);
293+
}
294+
}

0 commit comments

Comments
 (0)