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'); }