• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2018 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:math' as math;
6
7import 'package:flutter/material.dart';
8
9// This demo displays one Category at a time. The backdrop show a list
10// of all of the categories and the selected category is displayed
11// (CategoryView) on top of the backdrop.
12
13class Category {
14  const Category({ this.title, this.assets });
15  final String title;
16  final List<String> assets;
17  @override
18  String toString() => '$runtimeType("$title")';
19}
20
21const List<Category> allCategories = <Category>[
22  Category(
23    title: 'Accessories',
24    assets: <String>[
25      'products/belt.png',
26      'products/earrings.png',
27      'products/backpack.png',
28      'products/hat.png',
29      'products/scarf.png',
30      'products/sunnies.png',
31    ],
32  ),
33  Category(
34    title: 'Blue',
35    assets: <String>[
36      'products/backpack.png',
37      'products/cup.png',
38      'products/napkins.png',
39      'products/top.png',
40    ],
41  ),
42  Category(
43    title: 'Cold Weather',
44    assets: <String>[
45      'products/jacket.png',
46      'products/jumper.png',
47      'products/scarf.png',
48      'products/sweater.png',
49      'products/sweats.png',
50    ],
51  ),
52  Category(
53    title: 'Home',
54    assets: <String>[
55      'products/cup.png',
56      'products/napkins.png',
57      'products/planters.png',
58      'products/table.png',
59      'products/teaset.png',
60    ],
61  ),
62  Category(
63    title: 'Tops',
64    assets: <String>[
65      'products/jumper.png',
66      'products/shirt.png',
67      'products/sweater.png',
68      'products/top.png',
69    ],
70  ),
71  Category(
72    title: 'Everything',
73    assets: <String>[
74      'products/backpack.png',
75      'products/belt.png',
76      'products/cup.png',
77      'products/dress.png',
78      'products/earrings.png',
79      'products/flatwear.png',
80      'products/hat.png',
81      'products/jacket.png',
82      'products/jumper.png',
83      'products/napkins.png',
84      'products/planters.png',
85      'products/scarf.png',
86      'products/shirt.png',
87      'products/sunnies.png',
88      'products/sweater.png',
89      'products/sweats.png',
90      'products/table.png',
91      'products/teaset.png',
92      'products/top.png',
93    ],
94  ),
95];
96
97class CategoryView extends StatelessWidget {
98  const CategoryView({ Key key, this.category }) : super(key: key);
99
100  final Category category;
101
102  @override
103  Widget build(BuildContext context) {
104    final ThemeData theme = Theme.of(context);
105    return Scrollbar(
106      child: ListView(
107        key: PageStorageKey<Category>(category),
108        padding: const EdgeInsets.symmetric(
109          vertical: 16.0,
110          horizontal: 64.0,
111        ),
112        children: category.assets.map<Widget>((String asset) {
113          return Column(
114            crossAxisAlignment: CrossAxisAlignment.stretch,
115            children: <Widget>[
116              Card(
117                child: Container(
118                  width: 144.0,
119                  alignment: Alignment.center,
120                  child: Column(
121                    children: <Widget>[
122                      Image.asset(
123                        asset,
124                        package: 'flutter_gallery_assets',
125                        fit: BoxFit.contain,
126                      ),
127                      Container(
128                        padding: const EdgeInsets.only(bottom: 16.0),
129                        alignment: AlignmentDirectional.center,
130                        child: Text(
131                          asset,
132                          style: theme.textTheme.caption,
133                        ),
134                      ),
135                    ],
136                  ),
137                ),
138              ),
139              const SizedBox(height: 24.0),
140            ],
141          );
142        }).toList(),
143      )
144    );
145  }
146}
147
148// One BackdropPanel is visible at a time. It's stacked on top of the
149// the BackdropDemo.
150class BackdropPanel extends StatelessWidget {
151  const BackdropPanel({
152    Key key,
153    this.onTap,
154    this.onVerticalDragUpdate,
155    this.onVerticalDragEnd,
156    this.title,
157    this.child,
158  }) : super(key: key);
159
160  final VoidCallback onTap;
161  final GestureDragUpdateCallback onVerticalDragUpdate;
162  final GestureDragEndCallback onVerticalDragEnd;
163  final Widget title;
164  final Widget child;
165
166  @override
167  Widget build(BuildContext context) {
168    final ThemeData theme = Theme.of(context);
169    return Material(
170      elevation: 2.0,
171      borderRadius: const BorderRadius.only(
172        topLeft: Radius.circular(16.0),
173        topRight: Radius.circular(16.0),
174      ),
175      child: Column(
176        crossAxisAlignment: CrossAxisAlignment.stretch,
177        children: <Widget>[
178          GestureDetector(
179            behavior: HitTestBehavior.opaque,
180            onVerticalDragUpdate: onVerticalDragUpdate,
181            onVerticalDragEnd: onVerticalDragEnd,
182            onTap: onTap,
183            child: Container(
184              height: 48.0,
185              padding: const EdgeInsetsDirectional.only(start: 16.0),
186              alignment: AlignmentDirectional.centerStart,
187              child: DefaultTextStyle(
188                style: theme.textTheme.subhead,
189                child: Tooltip(
190                  message: 'Tap to dismiss',
191                  child: title,
192                ),
193              ),
194            ),
195          ),
196          const Divider(height: 1.0),
197          Expanded(child: child),
198        ],
199      ),
200    );
201  }
202}
203
204// Cross fades between 'Select a Category' and 'Asset Viewer'.
205class BackdropTitle extends AnimatedWidget {
206  const BackdropTitle({
207    Key key,
208    Listenable listenable,
209  }) : super(key: key, listenable: listenable);
210
211  @override
212  Widget build(BuildContext context) {
213    final Animation<double> animation = listenable;
214    return DefaultTextStyle(
215      style: Theme.of(context).primaryTextTheme.title,
216      softWrap: false,
217      overflow: TextOverflow.ellipsis,
218      child: Stack(
219        children: <Widget>[
220          Opacity(
221            opacity: CurvedAnimation(
222              parent: ReverseAnimation(animation),
223              curve: const Interval(0.5, 1.0),
224            ).value,
225            child: const Text('Select a Category'),
226          ),
227          Opacity(
228            opacity: CurvedAnimation(
229              parent: animation,
230              curve: const Interval(0.5, 1.0),
231            ).value,
232            child: const Text('Asset Viewer'),
233          ),
234        ],
235      ),
236    );
237  }
238}
239
240// This widget is essentially the backdrop itself.
241class BackdropDemo extends StatefulWidget {
242  static const String routeName = '/material/backdrop';
243
244  @override
245  _BackdropDemoState createState() => _BackdropDemoState();
246}
247
248class _BackdropDemoState extends State<BackdropDemo> with SingleTickerProviderStateMixin {
249  final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop');
250  AnimationController _controller;
251  Category _category = allCategories[0];
252
253  @override
254  void initState() {
255    super.initState();
256    _controller = AnimationController(
257      duration: const Duration(milliseconds: 300),
258      value: 1.0,
259      vsync: this,
260    );
261  }
262
263  @override
264  void dispose() {
265    _controller.dispose();
266    super.dispose();
267  }
268
269  void _changeCategory(Category category) {
270    setState(() {
271      _category = category;
272      _controller.fling(velocity: 2.0);
273    });
274  }
275
276  bool get _backdropPanelVisible {
277    final AnimationStatus status = _controller.status;
278    return status == AnimationStatus.completed || status == AnimationStatus.forward;
279  }
280
281  void _toggleBackdropPanelVisibility() {
282    _controller.fling(velocity: _backdropPanelVisible ? -2.0 : 2.0);
283  }
284
285  double get _backdropHeight {
286    final RenderBox renderBox = _backdropKey.currentContext.findRenderObject();
287    return renderBox.size.height;
288  }
289
290  // By design: the panel can only be opened with a swipe. To close the panel
291  // the user must either tap its heading or the backdrop's menu icon.
292
293  void _handleDragUpdate(DragUpdateDetails details) {
294    if (_controller.isAnimating || _controller.status == AnimationStatus.completed)
295      return;
296
297    _controller.value -= details.primaryDelta / (_backdropHeight ?? details.primaryDelta);
298  }
299
300  void _handleDragEnd(DragEndDetails details) {
301    if (_controller.isAnimating || _controller.status == AnimationStatus.completed)
302      return;
303
304    final double flingVelocity = details.velocity.pixelsPerSecond.dy / _backdropHeight;
305    if (flingVelocity < 0.0)
306      _controller.fling(velocity: math.max(2.0, -flingVelocity));
307    else if (flingVelocity > 0.0)
308      _controller.fling(velocity: math.min(-2.0, -flingVelocity));
309    else
310      _controller.fling(velocity: _controller.value < 0.5 ? -2.0 : 2.0);
311  }
312
313  // Stacks a BackdropPanel, which displays the selected category, on top
314  // of the backdrop. The categories are displayed with ListTiles. Just one
315  // can be selected at a time. This is a LayoutWidgetBuild function because
316  // we need to know how big the BackdropPanel will be to set up its
317  // animation.
318  Widget _buildStack(BuildContext context, BoxConstraints constraints) {
319    const double panelTitleHeight = 48.0;
320    final Size panelSize = constraints.biggest;
321    final double panelTop = panelSize.height - panelTitleHeight;
322
323    final Animation<RelativeRect> panelAnimation = _controller.drive(
324      RelativeRectTween(
325        begin: RelativeRect.fromLTRB(
326          0.0,
327          panelTop - MediaQuery.of(context).padding.bottom,
328          0.0,
329          panelTop - panelSize.height,
330        ),
331        end: const RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0),
332      ),
333    );
334
335    final ThemeData theme = Theme.of(context);
336    final List<Widget> backdropItems = allCategories.map<Widget>((Category category) {
337      final bool selected = category == _category;
338      return Material(
339        shape: const RoundedRectangleBorder(
340          borderRadius: BorderRadius.all(Radius.circular(4.0)),
341        ),
342        color: selected
343          ? Colors.white.withOpacity(0.25)
344          : Colors.transparent,
345        child: ListTile(
346          title: Text(category.title),
347          selected: selected,
348          onTap: () {
349            _changeCategory(category);
350          },
351        ),
352      );
353    }).toList();
354
355    return Container(
356      key: _backdropKey,
357      color: theme.primaryColor,
358      child: Stack(
359        children: <Widget>[
360          ListTileTheme(
361            iconColor: theme.primaryIconTheme.color,
362            textColor: theme.primaryTextTheme.title.color.withOpacity(0.6),
363            selectedColor: theme.primaryTextTheme.title.color,
364            child: Padding(
365              padding: const EdgeInsets.symmetric(horizontal: 16.0),
366              child: Column(
367                crossAxisAlignment: CrossAxisAlignment.stretch,
368                children: backdropItems,
369              ),
370            ),
371          ),
372          PositionedTransition(
373            rect: panelAnimation,
374            child: BackdropPanel(
375              onTap: _toggleBackdropPanelVisibility,
376              onVerticalDragUpdate: _handleDragUpdate,
377              onVerticalDragEnd: _handleDragEnd,
378              title: Text(_category.title),
379              child: CategoryView(category: _category),
380            ),
381          ),
382        ],
383      ),
384    );
385  }
386
387  @override
388  Widget build(BuildContext context) {
389    return Scaffold(
390      appBar: AppBar(
391        elevation: 0.0,
392        title: BackdropTitle(
393          listenable: _controller.view,
394        ),
395        actions: <Widget>[
396          IconButton(
397            onPressed: _toggleBackdropPanelVisibility,
398            icon: AnimatedIcon(
399              icon: AnimatedIcons.close_menu,
400              semanticLabel: 'close',
401              progress: _controller.view,
402            ),
403          ),
404        ],
405      ),
406      body: LayoutBuilder(
407        builder: _buildStack,
408      ),
409    );
410  }
411}
412