// 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:async'; import 'dart:developer'; import 'dart:math' as math; import 'package:flutter/material.dart'; import 'backdrop.dart'; import 'demos.dart'; const String _kGalleryAssetsPackage = 'flutter_gallery_assets'; const Color _kFlutterBlue = const Color(0xFF003D75); const double _kDemoItemHeight = 64.0; const Duration _kFrontLayerSwitchDuration = const Duration(milliseconds: 300); class _FlutterLogo extends StatelessWidget { const _FlutterLogo({ Key key }) : super(key: key); @override Widget build(BuildContext context) { return new Center( child: new Container( width: 34.0, height: 34.0, decoration: const BoxDecoration( image: const DecorationImage( image: const AssetImage( 'white_logo/logo.png', package: _kGalleryAssetsPackage, ), ), ), ), ); } } class _CategoryItem extends StatelessWidget { const _CategoryItem({ Key key, this.category, this.onTap, }) : super (key: key); final GalleryDemoCategory category; final VoidCallback onTap; @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); final bool isDark = theme.brightness == Brightness.dark; // This repaint boundary prevents the entire _CategoriesPage from being // repainted when the button's ink splash animates. return new RepaintBoundary( child: new RawMaterialButton( padding: EdgeInsets.zero, splashColor: theme.primaryColor.withOpacity(0.12), highlightColor: Colors.transparent, onPressed: onTap, child: new Column( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.center, children: [ new Padding( padding: const EdgeInsets.all(6.0), child: new Icon( category.icon, size: 60.0, color: isDark ? Colors.white : _kFlutterBlue, ), ), const SizedBox(height: 10.0), new Container( height: 48.0, alignment: Alignment.center, child: new Text( category.name, textAlign: TextAlign.center, style: theme.textTheme.subhead.copyWith( fontFamily: 'GoogleSans', color: isDark ? Colors.white : _kFlutterBlue, ), ), ), ], ), ), ); } } class _CategoriesPage extends StatelessWidget { const _CategoriesPage({ Key key, this.categories, this.onCategoryTap, }) : super(key: key); final Iterable categories; final ValueChanged onCategoryTap; @override Widget build(BuildContext context) { const double aspectRatio = 160.0 / 180.0; final List categoriesList = categories.toList(); final int columnCount = (MediaQuery.of(context).orientation == Orientation.portrait) ? 2 : 3; return new SingleChildScrollView( key: const PageStorageKey('categories'), child: new LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { final double columnWidth = constraints.biggest.width / columnCount.toDouble(); final double rowHeight = columnWidth * aspectRatio; final int rowCount = (categories.length + columnCount - 1) ~/ columnCount; // This repaint boundary prevents the inner contents of the front layer // from repainting when the backdrop toggle triggers a repaint on the // LayoutBuilder. return new RepaintBoundary( child: new Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: new List.generate(rowCount, (int rowIndex) { final int columnCountForRow = rowIndex == rowCount - 1 ? categories.length - columnCount * math.max(0, rowCount - 1) : columnCount; return new Row( children: new List.generate(columnCountForRow, (int columnIndex) { final int index = rowIndex * columnCount + columnIndex; final GalleryDemoCategory category = categoriesList[index]; return new SizedBox( width: columnWidth, height: rowHeight, child: new _CategoryItem( category: category, onTap: () { onCategoryTap(category); }, ), ); }), ); }), ), ); }, ), ); } } class _DemoItem extends StatelessWidget { const _DemoItem({ Key key, this.demo }) : super(key: key); final GalleryDemo demo; void _launchDemo(BuildContext context) { if (demo.routeName != null) { Timeline.instantSync('Start Transition', arguments: { 'from': '/', 'to': demo.routeName, }); Navigator.pushNamed(context, demo.routeName); } } @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); final bool isDark = theme.brightness == Brightness.dark; final double textScaleFactor = MediaQuery.of(context)?.textScaleFactor ?? 1.0; final List titleChildren = [ new Text( demo.title, style: theme.textTheme.subhead.copyWith( color: isDark ? Colors.white : const Color(0xFF202124), ), ), ]; if (demo.subtitle != null) { titleChildren.add( new Text( demo.subtitle, style: theme.textTheme.body1.copyWith( color: isDark ? Colors.white : const Color(0xFF60646B) ), ), ); } return new RawMaterialButton( padding: EdgeInsets.zero, splashColor: theme.primaryColor.withOpacity(0.12), highlightColor: Colors.transparent, onPressed: () { _launchDemo(context); }, child: new Container( constraints: new BoxConstraints(minHeight: _kDemoItemHeight * textScaleFactor), child: new Row( children: [ new Container( width: 56.0, height: 56.0, alignment: Alignment.center, child: new Icon( demo.icon, size: 24.0, color: isDark ? Colors.white : _kFlutterBlue, ), ), new Expanded( child: new Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: titleChildren, ), ), const SizedBox(width: 44.0), ], ), ), ); } } class _DemosPage extends StatelessWidget { const _DemosPage(this.category); final GalleryDemoCategory category; @override Widget build(BuildContext context) { return new KeyedSubtree( key: const ValueKey('GalleryDemoList'), // So the tests can find this ListView child: new ListView( key: new PageStorageKey(category.name), padding: const EdgeInsets.only(top: 8.0), children: kGalleryCategoryToDemos[category].map((GalleryDemo demo) { return new _DemoItem(demo: demo); }).toList(), ), ); } } class GalleryHome extends StatefulWidget { // In checked mode our MaterialApp will show the default "debug" banner. // Otherwise show the "preview" banner. static bool showPreviewBanner = true; const GalleryHome({ Key key, this.optionsPage, }) : super(key: key); final Widget optionsPage; @override _GalleryHomeState createState() => new _GalleryHomeState(); } class _GalleryHomeState extends State with SingleTickerProviderStateMixin { static final GlobalKey _scaffoldKey = new GlobalKey(); AnimationController _controller; GalleryDemoCategory _category; @override void initState() { super.initState(); _controller = new AnimationController( duration: const Duration(milliseconds: 600), debugLabel: 'preview banner', vsync: this, )..forward(); } @override void dispose() { _controller.dispose(); super.dispose(); } static Widget _animatedSwitcherLayoutBuilder(List children) { return new Stack( children: children, alignment: Alignment.topLeft, ); } @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); final bool isDark = theme.brightness == Brightness.dark; const Curve switchOutCurve = const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn); const Curve switchInCurve = const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn); Widget home = new Scaffold( key: _scaffoldKey, backgroundColor: isDark ? _kFlutterBlue : theme.primaryColor, body: new SafeArea( bottom: false, child: new WillPopScope( onWillPop: () { // Pop the category page if Android back button is pressed. if (_category != null) { setState(() => _category = null); return new Future.value(false); } return new Future.value(true); }, child: new Backdrop( backTitle: const Text('Options'), backLayer: widget.optionsPage, frontAction: new AnimatedSwitcher( duration: _kFrontLayerSwitchDuration, switchOutCurve: switchOutCurve, switchInCurve: switchInCurve, layoutBuilder: _animatedSwitcherLayoutBuilder, child: _category == null ? const _FlutterLogo() : new IconButton( icon: const BackButtonIcon(), tooltip: 'Back', onPressed: () => setState(() => _category = null), ), ), frontTitle: new AnimatedSwitcher( duration: _kFrontLayerSwitchDuration, child: _category == null ? const Text('Flutter gallery') : new Text(_category.name), ), frontHeading: new Container(height: 24.0), frontLayer: new AnimatedSwitcher( duration: _kFrontLayerSwitchDuration, switchOutCurve: switchOutCurve, switchInCurve: switchInCurve, layoutBuilder: _animatedSwitcherLayoutBuilder, child: _category != null ? new _DemosPage(_category) : new _CategoriesPage( categories: kAllGalleryDemoCategories, onCategoryTap: (GalleryDemoCategory category) { setState(() => _category = category); }, ), ), ), ), ), ); assert(() { GalleryHome.showPreviewBanner = false; return true; }()); if (GalleryHome.showPreviewBanner) { home = new Stack( fit: StackFit.expand, children: [ home, new FadeTransition( opacity: new CurvedAnimation(parent: _controller, curve: Curves.easeInOut), child: const Banner( message: 'PREVIEW', location: BannerLocation.topEnd, ) ), ] ); } return home; } }