Added a Backdrop demo to the Gallery (#15579)
This commit is contained in:
parent
dd0acea1ec
commit
c599662903
402
examples/flutter_gallery/lib/demo/material/backdrop_demo.dart
Normal file
402
examples/flutter_gallery/lib/demo/material/backdrop_demo.dart
Normal file
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
export 'backdrop_demo.dart';
|
||||||
export 'bottom_navigation_demo.dart';
|
export 'bottom_navigation_demo.dart';
|
||||||
export 'buttons_demo.dart';
|
export 'buttons_demo.dart';
|
||||||
export 'cards_demo.dart';
|
export 'cards_demo.dart';
|
||||||
|
@ -81,6 +81,13 @@ List<GalleryItem> _buildGalleryItems() {
|
|||||||
buildRoute: (BuildContext context) => const VideoDemo(),
|
buildRoute: (BuildContext context) => const VideoDemo(),
|
||||||
),
|
),
|
||||||
// Material Components
|
// 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(
|
new GalleryItem(
|
||||||
title: 'Bottom navigation',
|
title: 'Bottom navigation',
|
||||||
subtitle: 'Bottom navigation with cross-fading views',
|
subtitle: 'Bottom navigation with cross-fading views',
|
||||||
|
@ -11,16 +11,16 @@ class GalleryTheme {
|
|||||||
final ThemeData theme;
|
final ThemeData theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const int _kPurplePrimaryValue = 0xFF6200EE;
|
||||||
const MaterialColor _kPurpleSwatch = const MaterialColor(
|
const MaterialColor _kPurpleSwatch = const MaterialColor(
|
||||||
500,
|
_kPurplePrimaryValue,
|
||||||
const <int, Color> {
|
const <int, Color> {
|
||||||
50: const Color(0xFFF2E7FE),
|
50: const Color(0xFFF2E7FE),
|
||||||
100: const Color(0xFFD7B7FD),
|
100: const Color(0xFFD7B7FD),
|
||||||
200: const Color(0xFFBB86FC),
|
200: const Color(0xFFBB86FC),
|
||||||
300: const Color(0xFF9E55FC),
|
300: const Color(0xFF9E55FC),
|
||||||
400: const Color(0xFF7F22FD),
|
400: const Color(0xFF7F22FD),
|
||||||
500: const Color(0xFF6200EE),
|
500: const Color(_kPurplePrimaryValue),
|
||||||
600: const Color(0xFF4B00D1),
|
|
||||||
700: const Color(0xFF3700B3),
|
700: const Color(0xFF3700B3),
|
||||||
800: const Color(0xFF270096),
|
800: const Color(0xFF270096),
|
||||||
900: const Color(0xFF190078),
|
900: const Color(0xFF190078),
|
||||||
|
@ -23,9 +23,8 @@ void main() {
|
|||||||
final Offset allDemosOrigin = tester.getTopRight(find.text('Demos'));
|
final Offset allDemosOrigin = tester.getTopRight(find.text('Demos'));
|
||||||
final Finder button = find.text('Buttons');
|
final Finder button = find.text('Buttons');
|
||||||
while (button.evaluate().isEmpty) {
|
while (button.evaluate().isEmpty) {
|
||||||
await tester.dragFrom(allDemosOrigin, const Offset(0.0, -100.0));
|
await tester.dragFrom(allDemosOrigin, const Offset(0.0, -200.0));
|
||||||
await tester.pump(); // start the scroll
|
await tester.pumpAndSettle();
|
||||||
await tester.pump(const Duration(seconds: 1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Launch the buttons demo and then prove that showing the example
|
// Launch the buttons demo and then prove that showing the example
|
||||||
|
@ -128,6 +128,8 @@ Future<Null> runSmokeTest(WidgetTester tester) async {
|
|||||||
final Finder finder = findGalleryItemByRouteName(tester, routeName);
|
final Finder finder = findGalleryItemByRouteName(tester, routeName);
|
||||||
Scrollable.ensureVisible(tester.element(finder), alignment: 0.5);
|
Scrollable.ensureVisible(tester.element(finder), alignment: 0.5);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
if (routeName == '/material/backdrop')
|
||||||
|
continue;
|
||||||
await smokeDemo(tester, routeName);
|
await smokeDemo(tester, routeName);
|
||||||
tester.binding.debugAssertNoTransientCallbacks('A transient callback was still active after leaving route $routeName');
|
tester.binding.debugAssertNoTransientCallbacks('A transient callback was still active after leaving route $routeName');
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user