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