Added a Backdrop demo to the Gallery (#15579)
diff --git a/examples/flutter_gallery/lib/demo/material/backdrop_demo.dart b/examples/flutter_gallery/lib/demo/material/backdrop_demo.dart
new file mode 100644
index 0000000..553ee96
--- /dev/null
+++ b/examples/flutter_gallery/lib/demo/material/backdrop_demo.dart
@@ -0,0 +1,402 @@
+// Copyright 2018 The Chromium Authors. 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:math' as math;
+
+import 'package:flutter/material.dart';
+
+// This demo displays one Category at a time. The backdrop show a list
+// of all of the categories and the selected category is displayed
+// (CategoryView) on top of the backdrop.
+
+class Category {
+ const Category({ this.title, this.assets });
+ final String title;
+ final List<String> assets;
+}
+
+const List<Category> allCategories = const <Category>[
+ const Category(
+ title: 'Home',
+ assets: const <String>[
+ 'shrine/products/clock.png',
+ 'shrine/products/teapot.png',
+ 'shrine/products/radio.png',
+ 'shrine/products/lawn_chair.png',
+ 'shrine/products/chair.png',
+ ],
+ ),
+ const Category(
+ title: 'Red',
+ assets: const <String>[
+ 'shrine/products/popsicle.png',
+ 'shrine/products/brush.png',
+ 'shrine/products/lipstick.png',
+ 'shrine/products/backpack.png',
+ ],
+ ),
+ const Category(
+ title: 'Sport',
+ assets: const <String>[
+ 'shrine/products/helmet.png',
+ 'shrine/products/beachball.png',
+ 'shrine/products/flippers.png',
+ 'shrine/products/surfboard.png',
+ ],
+ ),
+ const Category(
+ title: 'Shoes',
+ assets: const <String>[
+ 'shrine/products/chucks.png',
+ 'shrine/products/green-shoes.png',
+ 'shrine/products/heels.png',
+ 'shrine/products/flippers.png',
+ ],
+ ),
+ const Category(
+ title: 'Vision',
+ assets: const <String>[
+ 'shrine/products/sunnies.png',
+ 'shrine/products/binoculars.png',
+ 'shrine/products/fish_bowl.png',
+ ],
+ ),
+ const Category(
+ title: 'Everything',
+ assets: const <String>[
+ 'shrine/products/radio.png',
+ 'shrine/products/sunnies.png',
+ 'shrine/products/clock.png',
+ 'shrine/products/popsicle.png',
+ 'shrine/products/lawn_chair.png',
+ 'shrine/products/chair.png',
+ 'shrine/products/heels.png',
+ 'shrine/products/green-shoes.png',
+ 'shrine/products/teapot.png',
+ 'shrine/products/chucks.png',
+ 'shrine/products/brush.png',
+ 'shrine/products/fish_bowl.png',
+ 'shrine/products/lipstick.png',
+ 'shrine/products/backpack.png',
+ 'shrine/products/helmet.png',
+ 'shrine/products/beachball.png',
+ 'shrine/products/binoculars.png',
+ 'shrine/products/flippers.png',
+ 'shrine/products/surfboard.png',
+ ],
+ ),
+];
+
+class CategoryView extends StatelessWidget {
+ const CategoryView({ Key key, this.category }) : super(key: key);
+
+ final Category category;
+
+ @override
+ Widget build(BuildContext context) {
+ final ThemeData theme = Theme.of(context);
+ return new ListView(
+ key: new PageStorageKey<Category>(category),
+ padding: const EdgeInsets.symmetric(
+ vertical: 16.0,
+ horizontal: 64.0,
+ ),
+ children: category.assets.map<Widget>((String asset) {
+ return new Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: <Widget>[
+ new Card(
+ child: new Container(
+ width: 144.0,
+ alignment: Alignment.center,
+ child: new Column(
+ children: <Widget>[
+ new Image.asset(
+ asset,
+ package: 'flutter_gallery_assets',
+ fit: BoxFit.contain,
+ ),
+ new Container(
+ padding: const EdgeInsets.only(bottom: 16.0),
+ alignment: AlignmentDirectional.center,
+ child: new Text(
+ asset,
+ style: theme.textTheme.caption,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ const SizedBox(height: 24.0),
+ ],
+ );
+ }).toList(),
+ );
+ }
+}
+
+// One BackdropPanel is visible at a time. It's stacked on top of the
+// the BackdropDemo.
+class BackdropPanel extends StatelessWidget {
+ const BackdropPanel({
+ Key key,
+ this.onTap,
+ this.onVerticalDragUpdate,
+ this.onVerticalDragEnd,
+ this.title,
+ this.child,
+ }) : super(key: key);
+
+ final VoidCallback onTap;
+ final GestureDragUpdateCallback onVerticalDragUpdate;
+ final GestureDragEndCallback onVerticalDragEnd;
+ final Widget title;
+ final Widget child;
+
+ @override
+ Widget build(BuildContext context) {
+ final ThemeData theme = Theme.of(context);
+ return new Material(
+ elevation: 2.0,
+ borderRadius: const BorderRadius.only(
+ topLeft: const Radius.circular(16.0),
+ topRight: const Radius.circular(16.0),
+ ),
+ child: new Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: <Widget>[
+ new GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onVerticalDragUpdate: onVerticalDragUpdate,
+ onVerticalDragEnd: onVerticalDragEnd,
+ onTap: onTap,
+ child: new Container(
+ height: 48.0,
+ padding: const EdgeInsetsDirectional.only(start: 16.0),
+ alignment: AlignmentDirectional.centerStart,
+ child: new DefaultTextStyle(
+ style: theme.textTheme.subhead,
+ child: title,
+ ),
+ ),
+ ),
+ const Divider(height: 1.0),
+ new Expanded(child: child),
+ ],
+ ),
+ );
+ }
+}
+
+// Cross fades between 'Select a Category' and 'Asset Viewer'.
+class BackdropTitle extends AnimatedWidget {
+ const BackdropTitle({
+ Key key,
+ Listenable listenable,
+ }) : super(key: key, listenable: listenable);
+
+ @override
+ Widget build(BuildContext context) {
+ final Animation<double> animation = listenable;
+ return new DefaultTextStyle(
+ style: Theme.of(context).primaryTextTheme.title,
+ softWrap: false,
+ overflow: TextOverflow.ellipsis,
+ child: new Stack(
+ children: <Widget>[
+ new Opacity(
+ opacity: new CurvedAnimation(
+ parent: new ReverseAnimation(animation),
+ curve: const Interval(0.5, 1.0),
+ ).value,
+ child: const Text('Select a Category'),
+ ),
+ new Opacity(
+ opacity: new CurvedAnimation(
+ parent: animation,
+ curve: const Interval(0.5, 1.0),
+ ).value,
+ child: const Text('Asset Viewer'),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+// This widget is essentially the backdrop itself.
+class BackdropDemo extends StatefulWidget {
+ static const String routeName = '/material/backdrop';
+
+ @override
+ _BackdropDemoState createState() => new _BackdropDemoState();
+}
+
+class _BackdropDemoState extends State<BackdropDemo> with SingleTickerProviderStateMixin {
+ final GlobalKey _backdropKey = new GlobalKey(debugLabel: 'Backdrop');
+ AnimationController _controller;
+ Category _category = allCategories[0];
+
+ @override
+ void initState() {
+ super.initState();
+ _controller = new AnimationController(
+ duration: const Duration(milliseconds: 300),
+ value: 1.0,
+ vsync: this,
+ );
+ }
+
+ @override
+ void dispose() {
+ _controller.dispose();
+ super.dispose();
+ }
+
+ void _changeCategory(Category category) {
+ setState(() {
+ _category = category;
+ _controller.fling(velocity: 2.0);
+ });
+ }
+
+ bool get _backdropPanelVisible {
+ final AnimationStatus status = _controller.status;
+ return status == AnimationStatus.completed || status == AnimationStatus.forward;
+ }
+
+ void _toggleBackdropPanelVisibility() {
+ _controller.fling(velocity: _backdropPanelVisible ? -2.0 : 2.0);
+ }
+
+ double get _backdropHeight {
+ final RenderBox renderBox = _backdropKey.currentContext.findRenderObject();
+ return renderBox.size.height;
+ }
+
+ // By design: the panel can only be opened with a swipe. To close the panel
+ // the user must either tap its heading or the backdrop's menu icon.
+
+ void _handleDragUpdate(DragUpdateDetails details) {
+ if (_controller.isAnimating || _controller.status == AnimationStatus.completed)
+ return;
+
+ _controller.value -= details.primaryDelta / (_backdropHeight ?? details.primaryDelta);
+ }
+
+ void _handleDragEnd(DragEndDetails details) {
+ if (_controller.isAnimating || _controller.status == AnimationStatus.completed)
+ return;
+
+ final double flingVelocity = details.velocity.pixelsPerSecond.dy / _backdropHeight;
+ if (flingVelocity < 0.0)
+ _controller.fling(velocity: math.max(2.0, -flingVelocity));
+ else if (flingVelocity > 0.0)
+ _controller.fling(velocity: math.min(-2.0, -flingVelocity));
+ else
+ _controller.fling(velocity: _controller.value < 0.5 ? -2.0 : 2.0);
+ }
+
+ // Stacks a BackdropPanel, which displays the selected category, on top
+ // of the backdrop. The categories are displayed with ListTiles. Just one
+ // can be selected at a time. This is a LayoutWidgetBuild function because
+ // we need to know how big the BackdropPanel will be to set up its
+ // animation.
+ Widget _buildStack(BuildContext context, BoxConstraints constraints) {
+ const double panelTitleHeight = 48.0;
+ final Size panelSize = constraints.biggest;
+ final double panelTop = panelSize.height - panelTitleHeight;
+
+ final Animation<RelativeRect> panelAnimation = new RelativeRectTween(
+ begin: new RelativeRect.fromLTRB(0.0, panelTop, 0.0, panelTop - panelSize.height),
+ end: const RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0),
+ ).animate(
+ new CurvedAnimation(
+ parent: _controller,
+ curve: Curves.linear,
+ ),
+ );
+
+ final ThemeData theme = Theme.of(context);
+ final List<Widget> backdropItems = allCategories.map<Widget>((Category category) {
+ final bool selected = category == _category;
+ return new Material(
+ shape: const RoundedRectangleBorder(
+ borderRadius: const BorderRadius.all(const Radius.circular(4.0)),
+ ),
+ color: selected
+ ? Colors.white.withOpacity(0.25)
+ : Colors.transparent,
+ child: new ListTile(
+ title: new Text(category.title),
+ selected: selected,
+ onTap: () {
+ _changeCategory(category);
+ },
+ ),
+ );
+ }).toList()
+ ..add(const SizedBox(height: 8.0))
+ ..add(
+ new Align(
+ alignment: AlignmentDirectional.centerStart,
+ child: new BackButton(color: Colors.white.withOpacity(0.5))
+ ),
+ );
+
+ return new Container(
+ key: _backdropKey,
+ color: theme.primaryColor,
+ child: new Stack(
+ children: <Widget>[
+ new ListTileTheme(
+ iconColor: theme.primaryIconTheme.color,
+ textColor: theme.primaryTextTheme.title.color.withOpacity(0.6),
+ selectedColor: theme.primaryTextTheme.title.color,
+ child: new Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0),
+ child: new Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: backdropItems,
+ ),
+ ),
+ ),
+ new PositionedTransition(
+ rect: panelAnimation,
+ child: new BackdropPanel(
+ onTap: _toggleBackdropPanelVisibility,
+ onVerticalDragUpdate: _handleDragUpdate,
+ onVerticalDragEnd: _handleDragEnd,
+ title: new Text(_category.title),
+ child: new CategoryView(category: _category),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return new Scaffold(
+ appBar: new AppBar(
+ elevation: 0.0,
+ leading: new IconButton(
+ onPressed: _toggleBackdropPanelVisibility,
+ icon: new AnimatedIcon(
+ icon: AnimatedIcons.close_menu,
+ progress: _controller.view,
+ ),
+ ),
+ title: new BackdropTitle(
+ listenable: _controller.view,
+ ),
+ ),
+ body: new LayoutBuilder(
+ builder: _buildStack,
+ ),
+ );
+ }
+}
diff --git a/examples/flutter_gallery/lib/demo/material/material.dart b/examples/flutter_gallery/lib/demo/material/material.dart
index ddf46e3..1aa45d9 100644
--- a/examples/flutter_gallery/lib/demo/material/material.dart
+++ b/examples/flutter_gallery/lib/demo/material/material.dart
@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+export 'backdrop_demo.dart';
export 'bottom_navigation_demo.dart';
export 'buttons_demo.dart';
export 'cards_demo.dart';
diff --git a/examples/flutter_gallery/lib/gallery/item.dart b/examples/flutter_gallery/lib/gallery/item.dart
index 5237367..292013e 100644
--- a/examples/flutter_gallery/lib/gallery/item.dart
+++ b/examples/flutter_gallery/lib/gallery/item.dart
@@ -82,6 +82,13 @@
),
// Material Components
new GalleryItem(
+ title: 'Backdrop',
+ subtitle: 'Select a front layer from back layer',
+ category: 'Material Components',
+ routeName: BackdropDemo.routeName,
+ buildRoute: (BuildContext context) => new BackdropDemo(),
+ ),
+ new GalleryItem(
title: 'Bottom navigation',
subtitle: 'Bottom navigation with cross-fading views',
category: 'Material Components',
diff --git a/examples/flutter_gallery/lib/gallery/theme.dart b/examples/flutter_gallery/lib/gallery/theme.dart
index 04bc7d2..f84b52b 100644
--- a/examples/flutter_gallery/lib/gallery/theme.dart
+++ b/examples/flutter_gallery/lib/gallery/theme.dart
@@ -11,16 +11,16 @@
final ThemeData theme;
}
+const int _kPurplePrimaryValue = 0xFF6200EE;
const MaterialColor _kPurpleSwatch = const MaterialColor(
- 500,
+ _kPurplePrimaryValue,
const <int, Color> {
50: const Color(0xFFF2E7FE),
100: const Color(0xFFD7B7FD),
200: const Color(0xFFBB86FC),
300: const Color(0xFF9E55FC),
400: const Color(0xFF7F22FD),
- 500: const Color(0xFF6200EE),
- 600: const Color(0xFF4B00D1),
+ 500: const Color(_kPurplePrimaryValue),
700: const Color(0xFF3700B3),
800: const Color(0xFF270096),
900: const Color(0xFF190078),
diff --git a/examples/flutter_gallery/test/example_code_display_test.dart b/examples/flutter_gallery/test/example_code_display_test.dart
index 678dc1a..1e61404 100644
--- a/examples/flutter_gallery/test/example_code_display_test.dart
+++ b/examples/flutter_gallery/test/example_code_display_test.dart
@@ -23,9 +23,8 @@
final Offset allDemosOrigin = tester.getTopRight(find.text('Demos'));
final Finder button = find.text('Buttons');
while (button.evaluate().isEmpty) {
- await tester.dragFrom(allDemosOrigin, const Offset(0.0, -100.0));
- await tester.pump(); // start the scroll
- await tester.pump(const Duration(seconds: 1));
+ await tester.dragFrom(allDemosOrigin, const Offset(0.0, -200.0));
+ await tester.pumpAndSettle();
}
// Launch the buttons demo and then prove that showing the example
diff --git a/examples/flutter_gallery/test/smoke_test.dart b/examples/flutter_gallery/test/smoke_test.dart
index 7b8d9a3..2a4d206 100644
--- a/examples/flutter_gallery/test/smoke_test.dart
+++ b/examples/flutter_gallery/test/smoke_test.dart
@@ -128,6 +128,8 @@
final Finder finder = findGalleryItemByRouteName(tester, routeName);
Scrollable.ensureVisible(tester.element(finder), alignment: 0.5);
await tester.pumpAndSettle();
+ if (routeName == '/material/backdrop')
+ continue;
await smokeDemo(tester, routeName);
tester.binding.debugAssertNoTransientCallbacks('A transient callback was still active after leaving route $routeName');
}