// 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 assets; @override String toString() => '$runtimeType("$title")'; } const List allCategories = [ Category( title: 'Accessories', assets: [ 'products/belt.png', 'products/earrings.png', 'products/backpack.png', 'products/hat.png', 'products/scarf.png', 'products/sunnies.png', ], ), Category( title: 'Blue', assets: [ 'products/backpack.png', 'products/cup.png', 'products/napkins.png', 'products/top.png', ], ), Category( title: 'Cold Weather', assets: [ 'products/jacket.png', 'products/jumper.png', 'products/scarf.png', 'products/sweater.png', 'products/sweats.png', ], ), Category( title: 'Home', assets: [ 'products/cup.png', 'products/napkins.png', 'products/planters.png', 'products/table.png', 'products/teaset.png', ], ), Category( title: 'Tops', assets: [ 'products/jumper.png', 'products/shirt.png', 'products/sweater.png', 'products/top.png', ], ), Category( title: 'Everything', assets: [ 'products/backpack.png', 'products/belt.png', 'products/cup.png', 'products/dress.png', 'products/earrings.png', 'products/flatwear.png', 'products/hat.png', 'products/jacket.png', 'products/jumper.png', 'products/napkins.png', 'products/planters.png', 'products/scarf.png', 'products/shirt.png', 'products/sunnies.png', 'products/sweater.png', 'products/sweats.png', 'products/table.png', 'products/teaset.png', 'products/top.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 Scrollbar( child: ListView( key: PageStorageKey(category), padding: const EdgeInsets.symmetric( vertical: 16.0, horizontal: 64.0, ), children: category.assets.map((String asset) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Card( child: Container( width: 144.0, alignment: Alignment.center, child: Column( children: [ Image.asset( asset, package: 'flutter_gallery_assets', fit: BoxFit.contain, ), Container( padding: const EdgeInsets.only(bottom: 16.0), alignment: AlignmentDirectional.center, child: 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 Material( elevation: 2.0, borderRadius: const BorderRadius.only( topLeft: Radius.circular(16.0), topRight: Radius.circular(16.0), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ GestureDetector( behavior: HitTestBehavior.opaque, onVerticalDragUpdate: onVerticalDragUpdate, onVerticalDragEnd: onVerticalDragEnd, onTap: onTap, child: Container( height: 48.0, padding: const EdgeInsetsDirectional.only(start: 16.0), alignment: AlignmentDirectional.centerStart, child: DefaultTextStyle( style: theme.textTheme.subhead, child: Tooltip( message: 'Tap to dismiss', child: title, ), ), ), ), const Divider(height: 1.0), 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 animation = listenable; return DefaultTextStyle( style: Theme.of(context).primaryTextTheme.title, softWrap: false, overflow: TextOverflow.ellipsis, child: Stack( children: [ Opacity( opacity: CurvedAnimation( parent: ReverseAnimation(animation), curve: const Interval(0.5, 1.0), ).value, child: const Text('Select a Category'), ), Opacity( opacity: 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() => _BackdropDemoState(); } class _BackdropDemoState extends State with SingleTickerProviderStateMixin { final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop'); AnimationController _controller; Category _category = allCategories[0]; @override void initState() { super.initState(); _controller = 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 panelAnimation = _controller.drive( RelativeRectTween( begin: RelativeRect.fromLTRB( 0.0, panelTop - MediaQuery.of(context).padding.bottom, 0.0, panelTop - panelSize.height, ), end: const RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0), ), ); final ThemeData theme = Theme.of(context); final List backdropItems = allCategories.map((Category category) { final bool selected = category == _category; return Material( shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(4.0)), ), color: selected ? Colors.white.withOpacity(0.25) : Colors.transparent, child: ListTile( title: Text(category.title), selected: selected, onTap: () { _changeCategory(category); }, ), ); }).toList(); return Container( key: _backdropKey, color: theme.primaryColor, child: Stack( children: [ ListTileTheme( iconColor: theme.primaryIconTheme.color, textColor: theme.primaryTextTheme.title.color.withOpacity(0.6), selectedColor: theme.primaryTextTheme.title.color, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: backdropItems, ), ), ), PositionedTransition( rect: panelAnimation, child: BackdropPanel( onTap: _toggleBackdropPanelVisibility, onVerticalDragUpdate: _handleDragUpdate, onVerticalDragEnd: _handleDragEnd, title: Text(_category.title), child: CategoryView(category: _category), ), ), ], ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( elevation: 0.0, title: BackdropTitle( listenable: _controller.view, ), actions: [ IconButton( onPressed: _toggleBackdropPanelVisibility, icon: AnimatedIcon( icon: AnimatedIcons.close_menu, semanticLabel: 'close', progress: _controller.view, ), ), ], ), body: LayoutBuilder( builder: _buildStack, ), ); } }