diff --git a/dev/benchmarks/complex_layout/pubspec.yaml b/dev/benchmarks/complex_layout/pubspec.yaml index 64ae2fa4981..20d15ea655e 100644 --- a/dev/benchmarks/complex_layout/pubspec.yaml +++ b/dev/benchmarks/complex_layout/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: flutter_gallery_assets: git: url: https://flutter.googlesource.com/gallery-assets - ref: d318485f208376e06d7e330d9f191141d14722b8 + ref: 43590e625ab1b07f6a5809287ce16f7e61d9e165 async: 2.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" charcode: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" diff --git a/examples/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_background.png b/examples/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_background.png new file mode 100644 index 00000000000..c5ccd079eff Binary files /dev/null and b/examples/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_background.png differ diff --git a/examples/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_foreground.png b/examples/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_foreground.png new file mode 100644 index 00000000000..e3c6fb0d528 Binary files /dev/null and b/examples/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_foreground.png differ diff --git a/examples/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index db77bb4b7b0..b8f67f1933d 100644 Binary files a/examples/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/examples/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/examples/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_background.png b/examples/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_background.png new file mode 100644 index 00000000000..d05e1b6799b Binary files /dev/null and b/examples/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_background.png differ diff --git a/examples/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_foreground.png b/examples/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_foreground.png new file mode 100644 index 00000000000..052b9828671 Binary files /dev/null and b/examples/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_foreground.png differ diff --git a/examples/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 09d4391482b..6b188646750 100644 Binary files a/examples/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/examples/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/examples/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_background.png b/examples/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_background.png new file mode 100644 index 00000000000..455f042a62e Binary files /dev/null and b/examples/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_background.png differ diff --git a/examples/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_foreground.png b/examples/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_foreground.png new file mode 100644 index 00000000000..fa40533725a Binary files /dev/null and b/examples/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_foreground.png differ diff --git a/examples/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index d5f1c8d34e7..fcdb89bade5 100644 Binary files a/examples/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/examples/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/examples/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_background.png b/examples/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_background.png new file mode 100644 index 00000000000..3fd2f4fa22a Binary files /dev/null and b/examples/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_background.png differ diff --git a/examples/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_foreground.png b/examples/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_foreground.png new file mode 100644 index 00000000000..c5df8264954 Binary files /dev/null and b/examples/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_foreground.png differ diff --git a/examples/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/examples/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4d6372eebdb..87050b46b09 100644 Binary files a/examples/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/examples/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/examples/flutter_gallery/lib/gallery/about.dart b/examples/flutter_gallery/lib/gallery/about.dart new file mode 100644 index 00000000000..141d7b3064e --- /dev/null +++ b/examples/flutter_gallery/lib/gallery/about.dart @@ -0,0 +1,83 @@ +// 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 'package:flutter/gestures.dart'; +import 'package:flutter/foundation.dart' show defaultTargetPlatform; +import 'package:flutter/material.dart'; + +import 'package:url_launcher/url_launcher.dart'; + +class _LinkTextSpan extends TextSpan { + + // Beware! + // + // This class is only safe because the TapGestureRecognizer is not + // given a deadline and therefore never allocates any resources. + // + // In any other situation -- setting a deadline, using any of the less trivial + // recognizers, etc -- you would have to manage the gesture recognizer's + // lifetime and call dispose() when the TextSpan was no longer being rendered. + // + // Since TextSpan itself is @immutable, this means that you would have to + // manage the recognizer from outside the TextSpan, e.g. in the State of a + // stateful widget that then hands the recognizer to the TextSpan. + + _LinkTextSpan({ TextStyle style, String url, String text }) : super( + style: style, + text: text ?? url, + recognizer: new TapGestureRecognizer()..onTap = () { + launch(url, forceSafariVC: false); + } + ); +} + +void showGalleryAboutDialog(BuildContext context) { + final ThemeData themeData = Theme.of(context); + final TextStyle aboutTextStyle = themeData.textTheme.body2; + final TextStyle linkStyle = themeData.textTheme.body2.copyWith(color: themeData.accentColor); + + showAboutDialog( + context: context, + applicationVersion: 'April 2018 Preview', + applicationIcon: const FlutterLogo(), + applicationLegalese: '© 2017 The Chromium Authors', + children: [ + new Padding( + padding: const EdgeInsets.only(top: 24.0), + child: new RichText( + text: new TextSpan( + children: [ + new TextSpan( + style: aboutTextStyle, + text: 'Flutter is an early-stage, open-source project to help developers ' + 'build high-performance, high-fidelity, mobile apps for ' + '${defaultTargetPlatform == TargetPlatform.iOS ? 'multiple platforms' : 'iOS and Android'} ' + 'from a single codebase. This gallery is a preview of ' + "Flutter's many widgets, behaviors, animations, layouts, " + 'and more. Learn more about Flutter at ' + ), + new _LinkTextSpan( + style: linkStyle, + url: 'https://flutter.io', + ), + new TextSpan( + style: aboutTextStyle, + text: '.\n\nTo see the source code for this app, please visit the ', + ), + new _LinkTextSpan( + style: linkStyle, + url: 'https://goo.gl/iv1p4G', + text: 'flutter github repo', + ), + new TextSpan( + style: aboutTextStyle, + text: '.', + ), + ], + ), + ), + ), + ], + ); +} diff --git a/examples/flutter_gallery/lib/gallery/app.dart b/examples/flutter_gallery/lib/gallery/app.dart index d3af4df0c0b..962ab047cef 100644 --- a/examples/flutter_gallery/lib/gallery/app.dart +++ b/examples/flutter_gallery/lib/gallery/app.dart @@ -8,53 +8,79 @@ import 'package:flutter/foundation.dart' show defaultTargetPlatform; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart' show timeDilation; +import 'package:url_launcher/url_launcher.dart'; + +import 'demos.dart'; import 'home.dart'; -import 'item.dart'; -import 'theme.dart'; -import 'updates.dart'; +import 'options.dart'; +import 'scales.dart'; +import 'themes.dart'; +import 'updater.dart'; class GalleryApp extends StatefulWidget { const GalleryApp({ + Key key, this.updateUrlFetcher, this.enablePerformanceOverlay: true, - this.checkerboardRasterCacheImages: true, - this.checkerboardOffscreenLayers: true, + this.enableRasterCacheImagesCheckerboard: true, + this.enableOffscreenLayersCheckerboard: true, this.onSendFeedback, - Key key} - ) : super(key: key); + }) : super(key: key); final UpdateUrlFetcher updateUrlFetcher; - final bool enablePerformanceOverlay; - - final bool checkerboardRasterCacheImages; - - final bool checkerboardOffscreenLayers; - + final bool enableRasterCacheImagesCheckerboard; + final bool enableOffscreenLayersCheckerboard; final VoidCallback onSendFeedback; @override - GalleryAppState createState() => new GalleryAppState(); + _GalleryAppState createState() => new _GalleryAppState(); } -class GalleryAppState extends State { - GalleryTheme _galleryTheme = kAllGalleryThemes[0]; - bool _showPerformanceOverlay = false; - bool _checkerboardRasterCacheImages = false; - bool _checkerboardOffscreenLayers = false; - TextDirection _overrideDirection = TextDirection.ltr; - double _timeDilation = 1.0; - TargetPlatform _platform; - - // A null value indicates "use system default". - double _textScaleFactor; - +class _GalleryAppState extends State { + GalleryOptions _options; Timer _timeDilationTimer; + Map _buildRoutes() { + // For a different example of how to set up an application routing table + // using named routes, consider the example in the Navigator class documentation: + // https://docs.flutter.io/flutter/widgets/Navigator-class.html + + return new Map.fromIterable( + kAllGalleryDemos, + key: (dynamic demo) => '${demo.routeName}', + value: (dynamic demo) => demo.buildRoute, + )..addAll( + new Map.fromIterable( + kAllGalleryDemoCategories, + key: (dynamic category) => '/${category.name}', + value: (dynamic category) { + return (BuildContext context) { + return new DemosPage( + category: category, + optionsPage: new GalleryOptionsPage( + options: _options, + onOptionsChanged: _handleOptionsChanged, + onSendFeedback: widget.onSendFeedback ?? () { + launch('https://github.com/flutter/flutter/issues/new', forceSafariVC: false); + }, + ), + ); + }; + }, + ), + ); + } + @override void initState() { - _timeDilation = timeDilation; super.initState(); + _options = new GalleryOptions( + theme: kLightGalleryTheme, + textScaleFactor: kAllGalleryTextScaleValues[0], + timeDilation: timeDilation, + platform: defaultTargetPlatform, + ); } @override @@ -64,80 +90,50 @@ class GalleryAppState extends State { super.dispose(); } - Widget _applyScaleFactor(Widget child) { + void _handleOptionsChanged(GalleryOptions newOptions) { + setState(() { + if (_options.timeDilation != newOptions.timeDilation) { + _timeDilationTimer?.cancel(); + _timeDilationTimer = null; + if (newOptions.timeDilation > 1.0) { + // We delay the time dilation change long enough that the user can see + // that UI has started reacting and then we slam on the brakes so that + // they see that the time is in fact now dilated. + _timeDilationTimer = new Timer(const Duration(milliseconds: 150), () { + timeDilation = newOptions.timeDilation; + }); + } else { + timeDilation = newOptions.timeDilation; + } + } + + _options = newOptions; + }); + } + + Widget _applyTextScaleFactor(Widget child) { return new Builder( - builder: (BuildContext context) => new MediaQuery( - data: MediaQuery.of(context).copyWith( - textScaleFactor: _textScaleFactor, - ), - child: child, - ), + builder: (BuildContext context) { + return new MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaleFactor: _options.textScaleFactor.scale, + ), + child: child, + ); + }, ); } @override Widget build(BuildContext context) { Widget home = new GalleryHome( - galleryTheme: _galleryTheme, - onThemeChanged: (GalleryTheme value) { - setState(() { - _galleryTheme = value; - }); - }, - showPerformanceOverlay: _showPerformanceOverlay, - onShowPerformanceOverlayChanged: widget.enablePerformanceOverlay ? (bool value) { - setState(() { - _showPerformanceOverlay = value; - }); - } : null, - checkerboardRasterCacheImages: _checkerboardRasterCacheImages, - onCheckerboardRasterCacheImagesChanged: widget.checkerboardRasterCacheImages ? (bool value) { - setState(() { - _checkerboardRasterCacheImages = value; - }); - } : null, - checkerboardOffscreenLayers: _checkerboardOffscreenLayers, - onCheckerboardOffscreenLayersChanged: widget.checkerboardOffscreenLayers ? (bool value) { - setState(() { - _checkerboardOffscreenLayers = value; - }); - } : null, - onPlatformChanged: (TargetPlatform value) { - setState(() { - _platform = value == defaultTargetPlatform ? null : value; - }); - }, - timeDilation: _timeDilation, - onTimeDilationChanged: (double value) { - setState(() { - _timeDilationTimer?.cancel(); - _timeDilationTimer = null; - _timeDilation = value; - if (_timeDilation > 1.0) { - // We delay the time dilation change long enough that the user can see - // that the checkbox in the drawer has started reacting, then we slam - // on the brakes so that they see that the time is in fact now dilated. - _timeDilationTimer = new Timer(const Duration(milliseconds: 150), () { - timeDilation = _timeDilation; - }); - } else { - timeDilation = _timeDilation; - } - }); - }, - textScaleFactor: _textScaleFactor, - onTextScaleFactorChanged: (double value) { - setState(() { - _textScaleFactor = value; - }); - }, - overrideDirection: _overrideDirection, - onOverrideDirectionChanged: (TextDirection value) { - setState(() { - _overrideDirection = value; - }); - }, - onSendFeedback: widget.onSendFeedback, + optionsPage: new GalleryOptionsPage( + options: _options, + onOptionsChanged: _handleOptionsChanged, + onSendFeedback: widget.onSendFeedback ?? () { + launch('https://github.com/flutter/flutter/issues/new'); + }, + ), ); if (widget.updateUrlFetcher != null) { @@ -147,31 +143,21 @@ class GalleryAppState extends State { ); } - final Map _kRoutes = {}; - for (GalleryItem item in kAllGalleryItems) { - // For a different example of how to set up an application routing table - // using named routes, consider the example in the Navigator class documentation: - // https://docs.flutter.io/flutter/widgets/Navigator-class.html - _kRoutes[item.routeName] = (BuildContext context) { - return item.buildRoute(context); - }; - } - return new MaterialApp( + theme: _options.theme.data.copyWith(platform: _options.platform), title: 'Flutter Gallery', color: Colors.grey, - theme: _galleryTheme.theme.copyWith(platform: _platform ?? defaultTargetPlatform), - showPerformanceOverlay: _showPerformanceOverlay, - checkerboardRasterCacheImages: _checkerboardRasterCacheImages, - checkerboardOffscreenLayers: _checkerboardOffscreenLayers, - routes: _kRoutes, - home: home, + showPerformanceOverlay: _options.showPerformanceOverlay, + checkerboardOffscreenLayers: _options.showOffscreenLayersCheckerboard, + checkerboardRasterCacheImages: _options.showRasterCacheImagesCheckerboard, + routes: _buildRoutes(), builder: (BuildContext context, Widget child) { return new Directionality( - textDirection: _overrideDirection, - child: _applyScaleFactor(child), + textDirection: _options.textDirection, + child: _applyTextScaleFactor(child), ); }, + home: home, ); } } diff --git a/examples/flutter_gallery/lib/gallery/backdrop.dart b/examples/flutter_gallery/lib/gallery/backdrop.dart new file mode 100644 index 00000000000..8de2d9b1cda --- /dev/null +++ b/examples/flutter_gallery/lib/gallery/backdrop.dart @@ -0,0 +1,330 @@ +// 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/foundation.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/material.dart'; + +const double _kFrontHeadingHeight = 32.0; // front layer beveled rectangle +const double _kFrontClosedHeight = 72.0; // front layer height when closed +const double _kBackAppBarHeight = 56.0; // back layer (options) appbar height + +// The size of the front layer heading's left and right beveled corners. +final Tween _kFrontHeadingBevelRadius = new BorderRadiusTween( + begin: const BorderRadius.only( + topLeft: const Radius.circular(12.0), + topRight: const Radius.circular(12.0), + ), + end: const BorderRadius.only( + topLeft: const Radius.circular(_kFrontHeadingHeight), + topRight: const Radius.circular(_kFrontHeadingHeight), + ), +); + +class _IgnorePointerWhileStatusIsNot extends StatefulWidget { + const _IgnorePointerWhileStatusIsNot(this.status, { + Key key, + this.controller, + this.child, + }) : super(key: key); + + final AnimationController controller; + final AnimationStatus status; + final Widget child; + + @override + _IgnorePointerWhileStatusIsNotState createState() => new _IgnorePointerWhileStatusIsNotState(); +} + +class _IgnorePointerWhileStatusIsNotState extends State<_IgnorePointerWhileStatusIsNot> { + bool _ignoring; + + @override + void initState() { + super.initState(); + widget.controller.addStatusListener(_handleStatusChange); + _ignoring = widget.controller.status != AnimationStatus.completed; + } + + @override + void dispose() { + widget.controller.removeStatusListener(_handleStatusChange); + super.dispose(); + } + + void _handleStatusChange(AnimationStatus _) { + final bool value = widget.controller.status != widget.status; + if (_ignoring != value) { + setState(() { + _ignoring = value; + }); + } + } + + @override + Widget build(BuildContext context) { + return new IgnorePointer( + ignoring: _ignoring, + child: widget.child, + ); + } +} + +class _CrossFadeTransition extends AnimatedWidget { + const _CrossFadeTransition({ + Key key, + this.alignment: Alignment.center, + Animation progress, + this.child0, + this.child1, + }) : super(key: key, listenable: progress); + + final AlignmentGeometry alignment; + final Widget child0; + final Widget child1; + + @override + Widget build(BuildContext context) { + final Animation progress = listenable; + + final double opacity1 = new CurvedAnimation( + parent: new ReverseAnimation(progress), + curve: const Interval(0.5, 1.0), + ).value; + + final double opacity2 = new CurvedAnimation( + parent: progress, + curve: const Interval(0.5, 1.0), + ).value; + + return new Stack( + alignment: alignment, + children: [ + new IgnorePointer( + ignoring: opacity1 < 1.0, + child: new Opacity( + opacity: opacity1, + child: child1, + ), + ), + new IgnorePointer( + ignoring: opacity2 <1.0, + child: new Opacity( + opacity: opacity2, + child: child0, + ), + ), + ], + ); + } +} + +class _BackAppBar extends StatelessWidget { + const _BackAppBar({ + Key key, + this.leading: const SizedBox(width: 56.0), + @required this.title, + this.trailing, + }) : assert(leading != null), assert(title != null), super(key: key); + + final Widget leading; + final Widget title; + final Widget trailing; + + @override + Widget build(BuildContext context) { + final List children = [ + new Container( + alignment: Alignment.center, + width: 56.0, + child: leading, + ), + new Expanded( + child: title, + ), + ]; + + if (trailing != null) { + children.add( + new Container( + alignment: Alignment.center, + width: 56.0, + child: trailing, + ), + ); + } + + final ThemeData theme = Theme.of(context); + + return IconTheme.merge( + data: theme.primaryIconTheme, + child: new DefaultTextStyle( + style: theme.primaryTextTheme.title, + child: new SizedBox( + height: _kBackAppBarHeight, + child: new Row(children: children), + ), + ), + ); + } +} + +class Backdrop extends StatefulWidget { + const Backdrop({ + this.frontAction, + this.frontTitle, + this.frontHeading, + this.frontLayer, + this.backTitle, + this.backLayer, + }); + + final Widget frontAction; + final Widget frontTitle; + final Widget frontLayer; + final Widget frontHeading; + final Widget backTitle; + final Widget backLayer; + + @override + _BackdropState createState() => new _BackdropState(); +} + +class _BackdropState extends State with SingleTickerProviderStateMixin { + final GlobalKey _backdropKey = new GlobalKey(debugLabel: 'Backdrop'); + AnimationController _controller; + + @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(); + } + + double get _backdropHeight { + // Warning: this can be safely called from the event handlers but it may + // not be called at build time. + final RenderBox renderBox = _backdropKey.currentContext.findRenderObject(); + return math.max(0.0, renderBox.size.height - _kBackAppBarHeight - _kFrontClosedHeight); + } + + void _handleDragUpdate(DragUpdateDetails details) { + _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); + } + + void _toggleFrontLayer() { + final AnimationStatus status = _controller.status; + final bool isOpen = status == AnimationStatus.completed || status == AnimationStatus.forward; + _controller.fling(velocity: isOpen ? -2.0 : 2.0); + } + + Widget _buildStack(BuildContext context, BoxConstraints constraints) { + final Animation frontRelativeRect = new RelativeRectTween( + begin: new RelativeRect.fromLTRB(0.0, constraints.biggest.height - _kFrontClosedHeight, 0.0, 0.0), + end: const RelativeRect.fromLTRB(0.0, _kBackAppBarHeight, 0.0, 0.0), + ).animate(_controller); + + return new Stack( + key: _backdropKey, + children: [ + new Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Back layer + new _BackAppBar( + leading: widget.frontAction, + title: new _CrossFadeTransition( + progress: _controller, + alignment: AlignmentDirectional.centerStart, + child0: widget.frontTitle, + child1: widget.backTitle, + ), + trailing: new IconButton( + onPressed: _toggleFrontLayer, + tooltip: 'Show options page', + icon: new AnimatedIcon( + icon: AnimatedIcons.close_menu, + progress: _controller, + ), + ), + ), + new Expanded( + child: new _IgnorePointerWhileStatusIsNot( + AnimationStatus.dismissed, + controller: _controller, + child: widget.backLayer, + ), + ), + ], + ), + // Front layer + new PositionedTransition( + rect: frontRelativeRect, + child: new AnimatedBuilder( + animation: _controller, + builder: (BuildContext context, Widget child) { + return new PhysicalShape( + elevation: 12.0, + color: Theme.of(context).canvasColor, + clipper: new ShapeBorderClipper( + shape: new BeveledRectangleBorder( + borderRadius: _kFrontHeadingBevelRadius.lerp(_controller.value), + ), + ), + child: child, + ); + }, + child: new _IgnorePointerWhileStatusIsNot( + AnimationStatus.completed, + controller: _controller, + child: widget.frontLayer, + ), + ), + ), + new PositionedTransition( + rect: frontRelativeRect, + child: new Container( + alignment: Alignment.topLeft, + child: new GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: _toggleFrontLayer, + onVerticalDragUpdate: _handleDragUpdate, + onVerticalDragEnd: _handleDragEnd, + child: widget.frontHeading, + ), + ), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + return new LayoutBuilder(builder: _buildStack); + } +} diff --git a/examples/flutter_gallery/lib/gallery/item.dart b/examples/flutter_gallery/lib/gallery/demos.dart similarity index 60% rename from examples/flutter_gallery/lib/gallery/item.dart rename to examples/flutter_gallery/lib/gallery/demos.dart index ada4c4c7670..c6a14e18e19 100644 --- a/examples/flutter_gallery/lib/gallery/item.dart +++ b/examples/flutter_gallery/lib/gallery/demos.dart @@ -1,19 +1,66 @@ -// Copyright 2016 The Chromium Authors. All rights reserved. +// 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:developer'; - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import '../demo/all.dart'; +import 'icons.dart'; -typedef Widget GalleryDemoBuilder(); +class GalleryDemoCategory { + const GalleryDemoCategory._({ this.name, this.icon }); + @required final String name; + @required final IconData icon; -class GalleryItem extends StatelessWidget { - const GalleryItem({ + @override + bool operator ==(dynamic other) { + if (identical(this, other)) + return true; + if (runtimeType != other.runtimeType) + return false; + final GalleryDemoCategory typedOther = other; + return typedOther.name == name && typedOther.icon == icon; + } + + @override + int get hashCode => hashValues(name, icon); + + @override + String toString() { + return '$runtimeType($name)'; + } +} + +const GalleryDemoCategory _kDemos = const GalleryDemoCategory._( + name: 'Vignettes', + icon: GalleryIcons.animation, +); + +const GalleryDemoCategory _kStyle = const GalleryDemoCategory._( + name: 'Style', + icon: GalleryIcons.custom_typography, +); + +const GalleryDemoCategory _kMaterialComponents = const GalleryDemoCategory._( + name: 'Material', + icon: GalleryIcons.category_mdc, +); + +const GalleryDemoCategory _kCupertinoComponents = const GalleryDemoCategory._( + name: 'Cupertino', + icon: GalleryIcons.phone_iphone, +); + +const GalleryDemoCategory _kMedia = const GalleryDemoCategory._( + name: 'Media', + icon: GalleryIcons.drive_video, +); + +class GalleryDemo { + const GalleryDemo({ @required this.title, + @required this.icon, this.subtitle, @required this.category, @required this.routeName, @@ -24,363 +71,400 @@ class GalleryItem extends StatelessWidget { assert(buildRoute != null); final String title; + final IconData icon; final String subtitle; - final String category; + final GalleryDemoCategory category; final String routeName; final WidgetBuilder buildRoute; @override - Widget build(BuildContext context) { - return new ListTile( - title: new Text(title), - subtitle: new Text(subtitle), - onTap: () { - if (routeName != null) { - Timeline.instantSync('Start Transition', arguments: { - 'from': '/', - 'to': routeName - }); - Navigator.pushNamed(context, routeName); - } - } - ); + String toString() { + return '$runtimeType($title $routeName)'; } } -List _buildGalleryItems() { - // When editing this list, make sure you keep it in sync with - // the list in ../../test_driver/transitions_perf_test.dart - final List galleryItems = [ +List _buildGalleryDemos() { + final List galleryDemos = [ // Demos - new GalleryItem( + new GalleryDemo( title: 'Shrine', subtitle: 'Basic shopping app', - category: 'Vignettes', + icon: GalleryIcons.shrine, + category: _kDemos, routeName: ShrineDemo.routeName, buildRoute: (BuildContext context) => new ShrineDemo(), ), - new GalleryItem( + new GalleryDemo( title: 'Contact profile', subtitle: 'Address book entry with a flexible appbar', - category: 'Vignettes', + icon: GalleryIcons.account_box, + category: _kDemos, routeName: ContactsDemo.routeName, buildRoute: (BuildContext context) => new ContactsDemo(), ), - new GalleryItem( + new GalleryDemo( title: 'Animation', subtitle: 'Section organizer', - category: 'Vignettes', + icon: GalleryIcons.animation, + category: _kDemos, routeName: AnimationDemo.routeName, buildRoute: (BuildContext context) => const AnimationDemo(), ), - new GalleryItem( - title: 'Video', - subtitle: 'Video playback', - category: 'Vignettes', - routeName: VideoDemo.routeName, - buildRoute: (BuildContext context) => const VideoDemo(), - ), - // 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 app bar', - subtitle: 'With repositionable floating action button', - category: 'Material Components', - routeName: BottomAppBarDemo.routeName, - buildRoute: (BuildContext context) => new BottomAppBarDemo(), - ), - new GalleryItem( - title: 'Bottom navigation', - subtitle: 'Bottom navigation with cross-fading views', - category: 'Material Components', - routeName: BottomNavigationDemo.routeName, - buildRoute: (BuildContext context) => new BottomNavigationDemo(), - ), - new GalleryItem( - title: 'Buttons', - subtitle: 'All kinds: flat, raised, dropdown, icon, etc', - category: 'Material Components', - routeName: ButtonsDemo.routeName, - buildRoute: (BuildContext context) => new ButtonsDemo(), - ), - new GalleryItem( - title: 'Cards', - subtitle: 'Material with rounded corners and a drop shadow', - category: 'Material Components', - routeName: CardsDemo.routeName, - buildRoute: (BuildContext context) => new CardsDemo(), - ), - new GalleryItem( - title: 'Chips', - subtitle: 'Label with an optional delete button and avatar', - category: 'Material Components', - routeName: ChipDemo.routeName, - buildRoute: (BuildContext context) => new ChipDemo(), - ), - new GalleryItem( - title: 'Data tables', - subtitle: 'Rows and columns', - category: 'Material Components', - routeName: DataTableDemo.routeName, - buildRoute: (BuildContext context) => new DataTableDemo(), - ), - new GalleryItem( - title: 'Date and time pickers', - subtitle: 'Date and time selection widgets', - category: 'Material Components', - routeName: DateAndTimePickerDemo.routeName, - buildRoute: (BuildContext context) => new DateAndTimePickerDemo(), - ), - new GalleryItem( - title: 'Dialog', - subtitle: 'All kinds: simple, alert, fullscreen, etc', - category: 'Material Components', - routeName: DialogDemo.routeName, - buildRoute: (BuildContext context) => new DialogDemo(), - ), - new GalleryItem( - title: 'Drawer', - subtitle: 'Navigation drawer with a standard header', - category: 'Material Components', - routeName: DrawerDemo.routeName, - buildRoute: (BuildContext context) => new DrawerDemo(), - ), - new GalleryItem( - title: 'Expand/collapse list control', - subtitle: 'List with one level of sublists', - category: 'Material Components', - routeName: TwoLevelListDemo.routeName, - buildRoute: (BuildContext context) => new TwoLevelListDemo(), - ), - new GalleryItem( - title: 'Expansion panels', - subtitle: 'List of expanding panels', - category: 'Material Components', - routeName: ExpansionPanelsDemo.routeName, - buildRoute: (BuildContext context) => new ExpansionPanelsDemo(), - ), - new GalleryItem( - title: 'Floating action button', - subtitle: 'Action buttons with transitions', - category: 'Material Components', - routeName: TabsFabDemo.routeName, - buildRoute: (BuildContext context) => new TabsFabDemo(), - ), - new GalleryItem( - title: 'Grid', - subtitle: 'Row and column layout', - category: 'Material Components', - routeName: GridListDemo.routeName, - buildRoute: (BuildContext context) => const GridListDemo(), - ), - new GalleryItem( - title: 'Icons', - subtitle: 'Enabled and disabled icons with varying opacity', - category: 'Material Components', - routeName: IconsDemo.routeName, - buildRoute: (BuildContext context) => new IconsDemo(), - ), - new GalleryItem( - title: 'Leave-behind list items', - subtitle: 'List items with hidden actions', - category: 'Material Components', - routeName: LeaveBehindDemo.routeName, - buildRoute: (BuildContext context) => const LeaveBehindDemo(), - ), - new GalleryItem( - title: 'List', - subtitle: 'Layout variations for scrollable lists', - category: 'Material Components', - routeName: ListDemo.routeName, - buildRoute: (BuildContext context) => const ListDemo(), - ), - new GalleryItem( - title: 'Menus', - subtitle: 'Menu buttons and simple menus', - category: 'Material Components', - routeName: MenuDemo.routeName, - buildRoute: (BuildContext context) => const MenuDemo(), - ), - new GalleryItem( - title: 'Modal bottom sheet', - subtitle: 'Modal sheet that slides up from the bottom', - category: 'Material Components', - routeName: ModalBottomSheetDemo.routeName, - buildRoute: (BuildContext context) => new ModalBottomSheetDemo(), - ), - new GalleryItem( - title: 'Page selector', - subtitle: 'PageView with indicator', - category: 'Material Components', - routeName: PageSelectorDemo.routeName, - buildRoute: (BuildContext context) => new PageSelectorDemo(), - ), - new GalleryItem( - title: 'Persistent bottom sheet', - subtitle: 'Sheet that slides up from the bottom', - category: 'Material Components', - routeName: PersistentBottomSheetDemo.routeName, - buildRoute: (BuildContext context) => new PersistentBottomSheetDemo(), - ), - new GalleryItem( - title: 'Progress indicators', - subtitle: 'All kinds: linear, circular, indeterminate, etc', - category: 'Material Components', - routeName: ProgressIndicatorDemo.routeName, - buildRoute: (BuildContext context) => new ProgressIndicatorDemo(), - ), - new GalleryItem( - title: 'Pull to refresh', - subtitle: 'Refresh indicators', - category: 'Material Components', - routeName: OverscrollDemo.routeName, - buildRoute: (BuildContext context) => const OverscrollDemo(), - ), - new GalleryItem( - title: 'Scrollable tabs', - subtitle: 'Tab bar that scrolls', - category: 'Material Components', - routeName: ScrollableTabsDemo.routeName, - buildRoute: (BuildContext context) => new ScrollableTabsDemo(), - ), - new GalleryItem( - title: 'Selection controls', - subtitle: 'Checkboxes, radio buttons, and switches', - category: 'Material Components', - routeName: SelectionControlsDemo.routeName, - buildRoute: (BuildContext context) => new SelectionControlsDemo(), - ), - new GalleryItem( - title: 'Sliders', - subtitle: 'Widgets that select a value by dragging the slider thumb', - category: 'Material Components', - routeName: SliderDemo.routeName, - buildRoute: (BuildContext context) => new SliderDemo(), - ), - new GalleryItem( - title: 'Snackbar', - subtitle: 'Temporary message that appears at the bottom', - category: 'Material Components', - routeName: SnackBarDemo.routeName, - buildRoute: (BuildContext context) => const SnackBarDemo(), - ), - new GalleryItem( - title: 'Tabs', - subtitle: 'Tabs with independently scrollable views', - category: 'Material Components', - routeName: TabsDemo.routeName, - buildRoute: (BuildContext context) => new TabsDemo(), - ), - new GalleryItem( - title: 'Text fields', - subtitle: 'Single line of editable text and numbers', - category: 'Material Components', - routeName: TextFormFieldDemo.routeName, - buildRoute: (BuildContext context) => const TextFormFieldDemo(), - ), - new GalleryItem( - title: 'Tooltips', - subtitle: 'Short message displayed after a long-press', - category: 'Material Components', - routeName: TooltipDemo.routeName, - buildRoute: (BuildContext context) => new TooltipDemo(), - ), - // Cupertino Components - new GalleryItem( - title: 'Activity Indicator', - subtitle: 'Cupertino styled activity indicator', - category: 'Cupertino Components', - routeName: CupertinoProgressIndicatorDemo.routeName, - buildRoute: (BuildContext context) => new CupertinoProgressIndicatorDemo(), - ), - new GalleryItem( - title: 'Buttons', - subtitle: 'Cupertino styled buttons', - category: 'Cupertino Components', - routeName: CupertinoButtonsDemo.routeName, - buildRoute: (BuildContext context) => new CupertinoButtonsDemo(), - ), - new GalleryItem( - title: 'Dialogs', - subtitle: 'Cupertino styled dialogs', - category: 'Cupertino Components', - routeName: CupertinoDialogDemo.routeName, - buildRoute: (BuildContext context) => new CupertinoDialogDemo(), - ), - new GalleryItem( - title: 'Navigation', - subtitle: 'Cupertino styled navigation patterns', - category: 'Cupertino Components', - routeName: CupertinoNavigationDemo.routeName, - buildRoute: (BuildContext context) => new CupertinoNavigationDemo(), - ), - new GalleryItem( - title: 'Pickers', - subtitle: 'Cupertino styled pickers', - category: 'Cupertino Components', - routeName: CupertinoPickerDemo.routeName, - buildRoute: (BuildContext context) => new CupertinoPickerDemo(), - ), - new GalleryItem( - title: 'Pull to refresh', - subtitle: 'Cupertino styled refresh controls', - category: 'Cupertino Components', - routeName: CupertinoRefreshControlDemo.routeName, - buildRoute: (BuildContext context) => new CupertinoRefreshControlDemo(), - ), - new GalleryItem( - title: 'Sliders', - subtitle: 'Cupertino styled sliders', - category: 'Cupertino Components', - routeName: CupertinoSliderDemo.routeName, - buildRoute: (BuildContext context) => new CupertinoSliderDemo(), - ), - new GalleryItem( - title: 'Switches', - subtitle: 'Cupertino styled switches', - category: 'Cupertino Components', - routeName: CupertinoSwitchDemo.routeName, - buildRoute: (BuildContext context) => new CupertinoSwitchDemo(), - ), - // Media - new GalleryItem( - title: 'Animated images', - subtitle: 'GIF and WebP animations', - category: 'Media', - routeName: ImagesDemo.routeName, - buildRoute: (BuildContext context) => new ImagesDemo(), - ), - // Styles - new GalleryItem( + + // Style + new GalleryDemo( title: 'Colors', subtitle: 'All of the predefined colors', - category: 'Style', + icon: GalleryIcons.colors, + category: _kStyle, routeName: ColorsDemo.routeName, buildRoute: (BuildContext context) => new ColorsDemo(), ), - new GalleryItem( + new GalleryDemo( title: 'Typography', subtitle: 'All of the predefined text styles', - category: 'Style', + icon: GalleryIcons.custom_typography, + category: _kStyle, routeName: TypographyDemo.routeName, buildRoute: (BuildContext context) => new TypographyDemo(), - ) + ), + + // Material Components + new GalleryDemo( + title: 'Backdrop', + subtitle: 'Select a front layer from back layer', + icon: GalleryIcons.backdrop, + category: _kMaterialComponents, + routeName: BackdropDemo.routeName, + buildRoute: (BuildContext context) => new BackdropDemo(), + ), + new GalleryDemo( + title: 'Bottom app bar', + subtitle: 'With repositionable floating action button', + icon: GalleryIcons.bottom_app_bar, + category: _kMaterialComponents, + routeName: BottomAppBarDemo.routeName, + buildRoute: (BuildContext context) => new BottomAppBarDemo(), + ), + new GalleryDemo( + title: 'Bottom navigation', + subtitle: 'Bottom navigation with cross-fading views', + icon: GalleryIcons.bottom_navigation, + category: _kMaterialComponents, + routeName: BottomNavigationDemo.routeName, + buildRoute: (BuildContext context) => new BottomNavigationDemo(), + ), + new GalleryDemo( + title: 'Buttons', + subtitle: 'All kinds: flat, raised, dropdown, icon, etc', + icon: GalleryIcons.generic_buttons, + category: _kMaterialComponents, + routeName: ButtonsDemo.routeName, + buildRoute: (BuildContext context) => new ButtonsDemo(), + ), + new GalleryDemo( + title: 'Cards', + subtitle: 'Material with rounded corners and a drop shadow', + icon: GalleryIcons.cards, + category: _kMaterialComponents, + routeName: CardsDemo.routeName, + buildRoute: (BuildContext context) => new CardsDemo(), + ), + new GalleryDemo( + title: 'Chips', + subtitle: 'Label with an optional delete button and avatar', + icon: GalleryIcons.chips, + category: _kMaterialComponents, + routeName: ChipDemo.routeName, + buildRoute: (BuildContext context) => new ChipDemo(), + ), + new GalleryDemo( + title: 'Data tables', + subtitle: 'Rows and columns', + icon: GalleryIcons.data_table, + category: _kMaterialComponents, + routeName: DataTableDemo.routeName, + buildRoute: (BuildContext context) => new DataTableDemo(), + ), + new GalleryDemo( + title: 'Date and time pickers', + subtitle: 'Date and time selection widgets', + icon: GalleryIcons.event, + category: _kMaterialComponents, + routeName: DateAndTimePickerDemo.routeName, + buildRoute: (BuildContext context) => new DateAndTimePickerDemo(), + ), + new GalleryDemo( + title: 'Dialog', + subtitle: 'All kinds: simple, alert, fullscreen, etc', + icon: GalleryIcons.dialogs, + category: _kMaterialComponents, + routeName: DialogDemo.routeName, + buildRoute: (BuildContext context) => new DialogDemo(), + ), + new GalleryDemo( + title: 'Drawer', + subtitle: 'Navigation drawer with a standard header', + icon: GalleryIcons.menu, + category: _kMaterialComponents, + routeName: DrawerDemo.routeName, + buildRoute: (BuildContext context) => new DrawerDemo(), + ), + new GalleryDemo( + title: 'Expand/collapse list control', + subtitle: 'List with one level of sublists', + icon: GalleryIcons.expand_all, + category: _kMaterialComponents, + routeName: TwoLevelListDemo.routeName, + buildRoute: (BuildContext context) => new TwoLevelListDemo(), + ), + new GalleryDemo( + title: 'Expansion panels', + subtitle: 'List of expanding panels', + icon: GalleryIcons.expand_all, + category: _kMaterialComponents, + routeName: ExpansionPanelsDemo.routeName, + buildRoute: (BuildContext context) => new ExpansionPanelsDemo(), + ), + new GalleryDemo( + title: 'Floating action button', + subtitle: 'Action buttons with transitions', + icon: GalleryIcons.buttons, + category: _kMaterialComponents, + routeName: TabsFabDemo.routeName, + buildRoute: (BuildContext context) => new TabsFabDemo(), + ), + new GalleryDemo( + title: 'Grid', + subtitle: 'Row and column layout', + icon: GalleryIcons.grid_on, + category: _kMaterialComponents, + routeName: GridListDemo.routeName, + buildRoute: (BuildContext context) => const GridListDemo(), + ), + new GalleryDemo( + title: 'Icons', + subtitle: 'Enabled and disabled icons with varying opacity', + icon: GalleryIcons.sentiment_very_satisfied, + category: _kMaterialComponents, + routeName: IconsDemo.routeName, + buildRoute: (BuildContext context) => new IconsDemo(), + ), + new GalleryDemo( + title: 'Leave-behind list items', + subtitle: 'List items with hidden actions', + icon: GalleryIcons.lists_leave_behind, + category: _kMaterialComponents, + routeName: LeaveBehindDemo.routeName, + buildRoute: (BuildContext context) => const LeaveBehindDemo(), + ), + new GalleryDemo( + title: 'List', + subtitle: 'Layout variations for scrollable lists', + icon: GalleryIcons.list_alt, + category: _kMaterialComponents, + routeName: ListDemo.routeName, + buildRoute: (BuildContext context) => const ListDemo(), + ), + new GalleryDemo( + title: 'Menus', + subtitle: 'Menu buttons and simple menus', + icon: GalleryIcons.more_vert, + category: _kMaterialComponents, + routeName: MenuDemo.routeName, + buildRoute: (BuildContext context) => const MenuDemo(), + ), + new GalleryDemo( + title: 'Modal bottom sheet', + subtitle: 'Modal sheet that slides up from the bottom', + icon: GalleryIcons.bottom_sheets, + category: _kMaterialComponents, + routeName: ModalBottomSheetDemo.routeName, + buildRoute: (BuildContext context) => new ModalBottomSheetDemo(), + ), + new GalleryDemo( + title: 'Page selector', + subtitle: 'PageView with indicator', + icon: GalleryIcons.page_control, + category: _kMaterialComponents, + routeName: PageSelectorDemo.routeName, + buildRoute: (BuildContext context) => new PageSelectorDemo(), + ), + new GalleryDemo( + title: 'Persistent bottom sheet', + subtitle: 'Sheet that slides up from the bottom', + icon: GalleryIcons.bottom_sheet_persistent, + category: _kMaterialComponents, + routeName: PersistentBottomSheetDemo.routeName, + buildRoute: (BuildContext context) => new PersistentBottomSheetDemo(), + ), + new GalleryDemo( + title: 'Progress indicators', + subtitle: 'All kinds: linear, circular, indeterminate, etc', + icon: GalleryIcons.progress_activity, + category: _kMaterialComponents, + routeName: ProgressIndicatorDemo.routeName, + buildRoute: (BuildContext context) => new ProgressIndicatorDemo(), + ), + new GalleryDemo( + title: 'Pull to refresh', + subtitle: 'Refresh indicators', + icon: GalleryIcons.refresh, + category: _kMaterialComponents, + routeName: OverscrollDemo.routeName, + buildRoute: (BuildContext context) => const OverscrollDemo(), + ), + new GalleryDemo( + title: 'Scrollable tabs', + subtitle: 'Tab bar that scrolls', + category: _kMaterialComponents, + icon: GalleryIcons.tabs, + routeName: ScrollableTabsDemo.routeName, + buildRoute: (BuildContext context) => new ScrollableTabsDemo(), + ), + new GalleryDemo( + title: 'Selection controls', + subtitle: 'Checkboxes, radio buttons, and switches', + icon: GalleryIcons.check_box, + category: _kMaterialComponents, + routeName: SelectionControlsDemo.routeName, + buildRoute: (BuildContext context) => new SelectionControlsDemo(), + ), + new GalleryDemo( + title: 'Sliders', + subtitle: 'Widgets that select a value by dragging the slider thumb', + icon: GalleryIcons.sliders, + category: _kMaterialComponents, + routeName: SliderDemo.routeName, + buildRoute: (BuildContext context) => new SliderDemo(), + ), + new GalleryDemo( + title: 'Snackbar', + subtitle: 'Temporary message that appears at the bottom', + icon: GalleryIcons.snackbar, + category: _kMaterialComponents, + routeName: SnackBarDemo.routeName, + buildRoute: (BuildContext context) => const SnackBarDemo(), + ), + new GalleryDemo( + title: 'Tabs', + subtitle: 'Tabs with independently scrollable views', + icon: GalleryIcons.tabs, + category: _kMaterialComponents, + routeName: TabsDemo.routeName, + buildRoute: (BuildContext context) => new TabsDemo(), + ), + new GalleryDemo( + title: 'Text fields', + subtitle: 'Single line of editable text and numbers', + icon: GalleryIcons.text_fields_alt, + category: _kMaterialComponents, + routeName: TextFormFieldDemo.routeName, + buildRoute: (BuildContext context) => const TextFormFieldDemo(), + ), + new GalleryDemo( + title: 'Tooltips', + subtitle: 'Short message displayed after a long-press', + icon: GalleryIcons.tooltip, + category: _kMaterialComponents, + routeName: TooltipDemo.routeName, + buildRoute: (BuildContext context) => new TooltipDemo(), + ), + + // Cupertino Components + new GalleryDemo( + title: 'Activity Indicator', + subtitle: 'Cupertino styled activity indicator', + icon: GalleryIcons.cupertino_progress, + category: _kCupertinoComponents, + routeName: CupertinoProgressIndicatorDemo.routeName, + buildRoute: (BuildContext context) => new CupertinoProgressIndicatorDemo(), + ), + new GalleryDemo( + title: 'Buttons', + subtitle: 'Cupertino styled buttons', + icon: GalleryIcons.generic_buttons, + category: _kCupertinoComponents, + routeName: CupertinoButtonsDemo.routeName, + buildRoute: (BuildContext context) => new CupertinoButtonsDemo(), + ), + new GalleryDemo( + title: 'Dialogs', + subtitle: 'Cupertino styled dialogs', + icon: GalleryIcons.dialogs, + category: _kCupertinoComponents, + routeName: CupertinoDialogDemo.routeName, + buildRoute: (BuildContext context) => new CupertinoDialogDemo(), + ), + new GalleryDemo( + title: 'Navigation', + subtitle: 'Cupertino styled navigation patterns', + icon: GalleryIcons.bottom_navigation, + category: _kCupertinoComponents, + routeName: CupertinoNavigationDemo.routeName, + buildRoute: (BuildContext context) => new CupertinoNavigationDemo(), + ), + new GalleryDemo( + title: 'Pickers', + subtitle: 'Cupertino styled pickers', + icon: GalleryIcons.event, + category: _kCupertinoComponents, + routeName: CupertinoPickerDemo.routeName, + buildRoute: (BuildContext context) => new CupertinoPickerDemo(), + ), + new GalleryDemo( + title: 'Pull to refresh', + subtitle: 'Cupertino styled refresh controls', + icon: GalleryIcons.cupertino_pull_to_refresh, + category: _kCupertinoComponents, + routeName: CupertinoRefreshControlDemo.routeName, + buildRoute: (BuildContext context) => new CupertinoRefreshControlDemo(), + ), + new GalleryDemo( + title: 'Sliders', + subtitle: 'Cupertino styled sliders', + icon: GalleryIcons.sliders, + category: _kCupertinoComponents, + routeName: CupertinoSliderDemo.routeName, + buildRoute: (BuildContext context) => new CupertinoSliderDemo(), + ), + new GalleryDemo( + title: 'Switches', + subtitle: 'Cupertino styled switches', + icon: GalleryIcons.cupertino_switch, + category: _kCupertinoComponents, + routeName: CupertinoSwitchDemo.routeName, + buildRoute: (BuildContext context) => new CupertinoSwitchDemo(), + ), + + // Media + new GalleryDemo( + title: 'Animated images', + subtitle: 'GIF and WebP animations', + icon: GalleryIcons.animation, + category: _kMedia, + routeName: ImagesDemo.routeName, + buildRoute: (BuildContext context) => new ImagesDemo(), + ), + new GalleryDemo( + title: 'Video', + subtitle: 'Video playback', + icon: GalleryIcons.drive_video, + category: _kMedia, + routeName: VideoDemo.routeName, + buildRoute: (BuildContext context) => const VideoDemo(), + ), ]; // Keep Pesto around for its regression test value. It is not included // in (release builds) the performance tests. assert(() { - galleryItems.insert(0, - new GalleryItem( + galleryDemos.insert(0, + new GalleryDemo( title: 'Pesto', subtitle: 'Simple recipe browser', - category: 'Vignettes', + icon: Icons.adjust, + category: _kDemos, routeName: PestoDemo.routeName, buildRoute: (BuildContext context) => const PestoDemo(), ), @@ -388,7 +472,18 @@ List _buildGalleryItems() { return true; }()); - return galleryItems; + return galleryDemos; } -final List kAllGalleryItems = _buildGalleryItems(); +final List kAllGalleryDemos = _buildGalleryDemos(); + +final Set kAllGalleryDemoCategories = + kAllGalleryDemos.map((GalleryDemo demo) => demo.category).toSet(); + +final Map> kGalleryCategoryToDemos = + new Map>.fromIterable( + kAllGalleryDemoCategories, + value: (dynamic category) { + return kAllGalleryDemos.where((GalleryDemo demo) => demo.category == category).toList(); + }, + ); diff --git a/examples/flutter_gallery/lib/gallery/drawer.dart b/examples/flutter_gallery/lib/gallery/drawer.dart deleted file mode 100644 index 4dfe73269f5..00000000000 --- a/examples/flutter_gallery/lib/gallery/drawer.dart +++ /dev/null @@ -1,349 +0,0 @@ -// Copyright 2016 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/foundation.dart' show defaultTargetPlatform, required; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; - -import 'package:url_launcher/url_launcher.dart'; - -import 'theme.dart'; - -class LinkTextSpan extends TextSpan { - - // Beware! - // - // This class is only safe because the TapGestureRecognizer is not - // given a deadline and therefore never allocates any resources. - // - // In any other situation -- setting a deadline, using any of the less trivial - // recognizers, etc -- you would have to manage the gesture recognizer's - // lifetime and call dispose() when the TextSpan was no longer being rendered. - // - // Since TextSpan itself is @immutable, this means that you would have to - // manage the recognizer from outside the TextSpan, e.g. in the State of a - // stateful widget that then hands the recognizer to the TextSpan. - - LinkTextSpan({ TextStyle style, String url, String text }) : super( - style: style, - text: text ?? url, - recognizer: new TapGestureRecognizer()..onTap = () { - launch(url, forceSafariVC: false); - } - ); -} - -class GalleryDrawerHeader extends StatefulWidget { - const GalleryDrawerHeader({ Key key, this.light }) : super(key: key); - - final bool light; - - @override - _GalleryDrawerHeaderState createState() => new _GalleryDrawerHeaderState(); -} - -class _GalleryDrawerHeaderState extends State { - bool _logoHasName = true; - bool _logoHorizontal = true; - MaterialColor _logoColor = Colors.blue; - - @override - Widget build(BuildContext context) { - final double systemTopPadding = MediaQuery.of(context).padding.top; - - return new Semantics( - label: 'Flutter', - child: new DrawerHeader( - decoration: new FlutterLogoDecoration( - margin: new EdgeInsets.fromLTRB(12.0, 12.0 + systemTopPadding, 12.0, 12.0), - style: _logoHasName ? _logoHorizontal ? FlutterLogoStyle.horizontal - : FlutterLogoStyle.stacked - : FlutterLogoStyle.markOnly, - lightColor: _logoColor.shade400, - darkColor: _logoColor.shade900, - textColor: widget.light ? const Color(0xFF616161) : const Color(0xFF9E9E9E), - ), - duration: const Duration(milliseconds: 750), - child: new GestureDetector( - onLongPress: () { - setState(() { - _logoHorizontal = !_logoHorizontal; - if (!_logoHasName) - _logoHasName = true; - }); - }, - onTap: () { - setState(() { - _logoHasName = !_logoHasName; - }); - }, - onDoubleTap: () { - setState(() { - final List options = []; - if (_logoColor != Colors.blue) - options.addAll([Colors.blue, Colors.blue, Colors.blue, Colors.blue, Colors.blue, Colors.blue, Colors.blue]); - if (_logoColor != Colors.amber) - options.addAll([Colors.amber, Colors.amber, Colors.amber]); - if (_logoColor != Colors.red) - options.addAll([Colors.red, Colors.red, Colors.red]); - if (_logoColor != Colors.indigo) - options.addAll([Colors.indigo, Colors.indigo, Colors.indigo]); - if (_logoColor != Colors.pink) - options.addAll([Colors.pink]); - if (_logoColor != Colors.purple) - options.addAll([Colors.purple]); - if (_logoColor != Colors.cyan) - options.addAll([Colors.cyan]); - _logoColor = options[new math.Random().nextInt(options.length)]; - }); - } - ), - ), - ); - } -} - -class GalleryDrawer extends StatelessWidget { - const GalleryDrawer({ - Key key, - this.galleryTheme, - @required this.onThemeChanged, - this.timeDilation, - @required this.onTimeDilationChanged, - this.textScaleFactor, - this.onTextScaleFactorChanged, - this.showPerformanceOverlay, - this.onShowPerformanceOverlayChanged, - this.checkerboardRasterCacheImages, - this.onCheckerboardRasterCacheImagesChanged, - this.checkerboardOffscreenLayers, - this.onCheckerboardOffscreenLayersChanged, - this.onPlatformChanged, - this.overrideDirection: TextDirection.ltr, - this.onOverrideDirectionChanged, - this.onSendFeedback, - }) : assert(onThemeChanged != null), - assert(onTimeDilationChanged != null), - super(key: key); - - final GalleryTheme galleryTheme; - final ValueChanged onThemeChanged; - - final double timeDilation; - final ValueChanged onTimeDilationChanged; - - final double textScaleFactor; - final ValueChanged onTextScaleFactorChanged; - - final bool showPerformanceOverlay; - final ValueChanged onShowPerformanceOverlayChanged; - - final bool checkerboardRasterCacheImages; - final ValueChanged onCheckerboardRasterCacheImagesChanged; - - final bool checkerboardOffscreenLayers; - final ValueChanged onCheckerboardOffscreenLayersChanged; - - final ValueChanged onPlatformChanged; - - final TextDirection overrideDirection; - final ValueChanged onOverrideDirectionChanged; - - final VoidCallback onSendFeedback; - - @override - Widget build(BuildContext context) { - final ThemeData themeData = Theme.of(context); - final TextStyle aboutTextStyle = themeData.textTheme.body2; - final TextStyle linkStyle = themeData.textTheme.body2.copyWith(color: themeData.accentColor); - - final List themeItems = kAllGalleryThemes.map((GalleryTheme theme) { - return new RadioListTile( - title: new Text(theme.name), - secondary: new Icon(theme.icon), - value: theme, - groupValue: galleryTheme, - onChanged: onThemeChanged, - selected: galleryTheme == theme, - ); - }).toList(); - - final Widget mountainViewItem = new RadioListTile( - // on iOS, we don't want to show an Android phone icon - secondary: new Icon(defaultTargetPlatform == TargetPlatform.iOS ? Icons.star : Icons.phone_android), - title: new Text(defaultTargetPlatform == TargetPlatform.iOS ? 'Mountain View' : 'Android'), - value: TargetPlatform.android, - groupValue: Theme.of(context).platform, - onChanged: onPlatformChanged, - selected: Theme.of(context).platform == TargetPlatform.android, - ); - - final Widget cupertinoItem = new RadioListTile( - // on iOS, we don't want to show the iPhone icon - secondary: new Icon(defaultTargetPlatform == TargetPlatform.iOS ? Icons.star_border : Icons.phone_iphone), - title: new Text(defaultTargetPlatform == TargetPlatform.iOS ? 'Cupertino' : 'iOS'), - value: TargetPlatform.iOS, - groupValue: Theme.of(context).platform, - onChanged: onPlatformChanged, - selected: Theme.of(context).platform == TargetPlatform.iOS, - ); - - final List textSizeItems = []; - final Map textSizes = { - null: 'System Default', - 0.8: 'Small', - 1.0: 'Normal', - 1.3: 'Large', - 2.0: 'Huge', - }; - for (double size in textSizes.keys) { - textSizeItems.add(new RadioListTile( - secondary: const Icon(Icons.text_fields), - title: new Text(textSizes[size]), - value: size, - groupValue: textScaleFactor, - onChanged: onTextScaleFactorChanged, - selected: textScaleFactor == size, - )); - } - - final Widget animateSlowlyItem = new CheckboxListTile( - title: const Text('Animate Slowly'), - value: timeDilation != 1.0, - onChanged: (bool value) { - onTimeDilationChanged(value ? 20.0 : 1.0); - }, - secondary: const Icon(Icons.hourglass_empty), - selected: timeDilation != 1.0, - ); - - final Widget overrideDirectionItem = new CheckboxListTile( - title: const Text('Force RTL'), - value: overrideDirection == TextDirection.rtl, - onChanged: (bool value) { - onOverrideDirectionChanged(value ? TextDirection.rtl : TextDirection.ltr); - }, - secondary: const Icon(Icons.format_textdirection_r_to_l), - selected: overrideDirection == TextDirection.rtl, - ); - - final Widget sendFeedbackItem = new ListTile( - leading: const Icon(Icons.report), - title: const Text('Send feedback'), - onTap: onSendFeedback ?? () { - launch('https://github.com/flutter/flutter/issues/new'); - }, - ); - - final Widget aboutItem = new AboutListTile( - icon: const FlutterLogo(), - applicationVersion: 'April 2018 Preview', - applicationIcon: const FlutterLogo(), - applicationLegalese: '© 2017 The Chromium Authors', - aboutBoxChildren: [ - new Padding( - padding: const EdgeInsets.only(top: 24.0), - child: new RichText( - text: new TextSpan( - children: [ - new TextSpan( - style: aboutTextStyle, - text: 'Flutter is an early-stage, open-source project to help developers ' - 'build high-performance, high-fidelity, mobile apps for ' - '${defaultTargetPlatform == TargetPlatform.iOS ? 'multiple platforms' : 'iOS and Android'} ' - 'from a single codebase. This gallery is a preview of ' - "Flutter's many widgets, behaviors, animations, layouts, " - 'and more. Learn more about Flutter at ' - ), - new LinkTextSpan( - style: linkStyle, - url: 'https://flutter.io' - ), - new TextSpan( - style: aboutTextStyle, - text: '.\n\nTo see the source code for this app, please visit the ' - ), - new LinkTextSpan( - style: linkStyle, - url: 'https://goo.gl/iv1p4G', - text: 'flutter github repo' - ), - new TextSpan( - style: aboutTextStyle, - text: '.' - ) - ] - ) - ) - ) - ] - ); - - final List allDrawerItems = [ - new GalleryDrawerHeader( - light: galleryTheme.theme.brightness == Brightness.light, - ), - ] - ..addAll(themeItems) - ..addAll([ - const Divider(), - mountainViewItem, - cupertinoItem, - const Divider(), - ]) - ..addAll(textSizeItems) - ..addAll([ - overrideDirectionItem, - const Divider(), - animateSlowlyItem, - const Divider(), - ]); - - bool addedOptionalItem = false; - if (onCheckerboardOffscreenLayersChanged != null) { - allDrawerItems.add(new CheckboxListTile( - title: const Text('Checkerboard Offscreen Layers'), - value: checkerboardOffscreenLayers, - onChanged: onCheckerboardOffscreenLayersChanged, - secondary: const Icon(Icons.assessment), - selected: checkerboardOffscreenLayers, - )); - addedOptionalItem = true; - } - - if (onCheckerboardRasterCacheImagesChanged != null) { - allDrawerItems.add(new CheckboxListTile( - title: const Text('Checkerboard Raster Cache Images'), - value: checkerboardRasterCacheImages, - onChanged: onCheckerboardRasterCacheImagesChanged, - secondary: const Icon(Icons.assessment), - selected: checkerboardRasterCacheImages, - )); - addedOptionalItem = true; - } - - if (onShowPerformanceOverlayChanged != null) { - allDrawerItems.add(new CheckboxListTile( - title: const Text('Performance Overlay'), - value: showPerformanceOverlay, - onChanged: onShowPerformanceOverlayChanged, - secondary: const Icon(Icons.assessment), - selected: showPerformanceOverlay, - )); - addedOptionalItem = true; - } - - if (addedOptionalItem) - allDrawerItems.add(const Divider()); - - allDrawerItems.addAll([ - sendFeedbackItem, - aboutItem, - ]); - - return new Drawer(child: new ListView(primary: false, children: allDrawerItems)); - } -} diff --git a/examples/flutter_gallery/lib/gallery/home.dart b/examples/flutter_gallery/lib/gallery/home.dart index 754ebdbbab2..b81d8bf7085 100644 --- a/examples/flutter_gallery/lib/gallery/home.dart +++ b/examples/flutter_gallery/lib/gallery/home.dart @@ -1,126 +1,286 @@ -// Copyright 2016 The Chromium Authors. All rights reserved. +// 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 'package:flutter/foundation.dart'; +import 'dart:developer'; +import 'dart:math' as math; + import 'package:flutter/material.dart'; -import 'drawer.dart'; -import 'item.dart'; -import 'theme.dart'; +import 'backdrop.dart'; +import 'demos.dart'; -const double _kFlexibleSpaceMaxHeight = 256.0; const String _kGalleryAssetsPackage = 'flutter_gallery_assets'; +const Color _kFlutterBlue = const Color(0xFF003D75); +const double _kDemoItemHeight = 64.0; -class _BackgroundLayer { - _BackgroundLayer({ int level, double parallax }) - : assetName = 'appbar/appbar_background_layer$level.png', - assetPackage = _kGalleryAssetsPackage, - parallaxTween = new Tween(begin: 0.0, end: parallax); - final String assetName; - final String assetPackage; - final Tween parallaxTween; -} - -final List<_BackgroundLayer> _kBackgroundLayers = <_BackgroundLayer>[ - new _BackgroundLayer(level: 0, parallax: _kFlexibleSpaceMaxHeight), - new _BackgroundLayer(level: 1, parallax: _kFlexibleSpaceMaxHeight), - new _BackgroundLayer(level: 2, parallax: _kFlexibleSpaceMaxHeight / 2.0), - new _BackgroundLayer(level: 3, parallax: _kFlexibleSpaceMaxHeight / 4.0), - new _BackgroundLayer(level: 4, parallax: _kFlexibleSpaceMaxHeight / 2.0), - new _BackgroundLayer(level: 5, parallax: _kFlexibleSpaceMaxHeight) -]; - -class _AppBarBackground extends StatelessWidget { - const _AppBarBackground({ Key key, this.animation }) : super(key: key); - - final Animation animation; +class _FlutterLogo extends StatelessWidget { + const _FlutterLogo({ Key key }) : super(key: key); @override Widget build(BuildContext context) { - return new AnimatedBuilder( - animation: animation, - builder: (BuildContext context, Widget child) { - return new Stack( - children: _kBackgroundLayers.map((_BackgroundLayer layer) { - return new Positioned( - top: -layer.parallaxTween.evaluate(animation), - left: 0.0, - right: 0.0, - bottom: 0.0, - child: new Image.asset( - layer.assetName, - package: layer.assetPackage, - fit: BoxFit.cover, - height: _kFlexibleSpaceMaxHeight - ) - ); - }).toList() - ); - } + 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; + + return 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( + 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; + + return new Column( + mainAxisAlignment: MainAxisAlignment.center, + 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: () { + Navigator.pushNamed(context, '/${category.name}'); + }, + ), + ); + }), + ); + }), + ); + }, + ), + ); + } +} + +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; + + 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: [ + new Text( + demo.title, + style: theme.textTheme.subhead.copyWith( + color: isDark ? Colors.white : const Color(0xFF202124), + ), + ), + new Text( + demo.subtitle, + style: theme.textTheme.body1.copyWith( + color: isDark ? Colors.white : const Color(0xFF60646B)), + ), + ], + ), + ), + const SizedBox(width: 44.0), + ], + ), + ), + ); + } +} + +class DemosPage extends StatelessWidget { + const DemosPage({ + Key key, + this.category, + this.optionsPage, + }) : super(key: key); + + final GalleryDemoCategory category; + final Widget optionsPage; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final bool isDark = theme.brightness == Brightness.dark; + + return new Scaffold( + backgroundColor: isDark ? _kFlutterBlue : theme.primaryColor, + body: new SafeArea( + child: new SizedBox.expand( + child: new Backdrop( + backTitle: const Text('Options'), + backLayer: optionsPage, + frontAction: const BackButton(), + frontTitle: new Text(category.name), + frontHeading: new Container( + height: 40.0, + alignment: Alignment.bottomCenter, + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: const Divider( + color: const Color(0xFFD5D7DA), + height: 1.0 + ), + ), + frontLayer: new Padding( + padding: const EdgeInsets.only(top: 40.0), + child: new ListView( + key: const ValueKey('GalleryDemoList'), // So tests can find it. + padding: const EdgeInsets.only(top: 8.0), + children: kGalleryCategoryToDemos[category].map((GalleryDemo demo) { + return new _DemoItem(demo: demo); + }).toList(), + ), + ), + ), + ), + ), ); } } class GalleryHome extends StatefulWidget { - const GalleryHome({ - Key key, - this.galleryTheme, - @required this.onThemeChanged, - this.timeDilation, - @required this.onTimeDilationChanged, - this.textScaleFactor, - this.onTextScaleFactorChanged, - this.showPerformanceOverlay, - this.onShowPerformanceOverlayChanged, - this.checkerboardRasterCacheImages, - this.onCheckerboardRasterCacheImagesChanged, - this.checkerboardOffscreenLayers, - this.onCheckerboardOffscreenLayersChanged, - this.onPlatformChanged, - this.overrideDirection: TextDirection.ltr, - this.onOverrideDirectionChanged, - this.onSendFeedback, - }) : assert(onThemeChanged != null), - assert(onTimeDilationChanged != null), - super(key: key); - // In checked mode our MaterialApp will show the default "debug" banner. // Otherwise show the "preview" banner. static bool showPreviewBanner = true; - final GalleryTheme galleryTheme; - final ValueChanged onThemeChanged; + const GalleryHome({ + Key key, + this.optionsPage, + }) : super(key: key); - final double timeDilation; - final ValueChanged onTimeDilationChanged; - - final double textScaleFactor; - final ValueChanged onTextScaleFactorChanged; - - final bool showPerformanceOverlay; - final ValueChanged onShowPerformanceOverlayChanged; - - final bool checkerboardRasterCacheImages; - final ValueChanged onCheckerboardRasterCacheImagesChanged; - - final bool checkerboardOffscreenLayers; - final ValueChanged onCheckerboardOffscreenLayersChanged; - - final ValueChanged onPlatformChanged; - - final TextDirection overrideDirection; - final ValueChanged onOverrideDirectionChanged; - - final VoidCallback onSendFeedback; + final Widget optionsPage; @override - GalleryHomeState createState() => new GalleryHomeState(); + _GalleryHomeState createState() => new _GalleryHomeState(); } -class GalleryHomeState extends State with SingleTickerProviderStateMixin { +class _GalleryHomeState extends State with SingleTickerProviderStateMixin { static final GlobalKey _scaffoldKey = new GlobalKey(); - AnimationController _controller; @override @@ -139,75 +299,27 @@ class GalleryHomeState extends State with SingleTickerProviderState super.dispose(); } - List _galleryListItems() { - final List listItems = []; - final ThemeData themeData = Theme.of(context); - final TextStyle headerStyle = themeData.textTheme.body2.copyWith(color: themeData.accentColor); - String category; - for (GalleryItem galleryItem in kAllGalleryItems) { - if (category != galleryItem.category) { - if (category != null) - listItems.add(const Divider()); - listItems.add( - new MergeSemantics( - child: new Container( - height: 48.0, - padding: const EdgeInsetsDirectional.only(start: 16.0), - alignment: AlignmentDirectional.centerStart, - child: new SafeArea( - top: false, - bottom: false, - child: new Semantics( - header: true, - child: new Text(galleryItem.category, style: headerStyle), - ), - ), - ), - ) - ); - category = galleryItem.category; - } - listItems.add(galleryItem); - } - return listItems; - } - @override Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final bool isDark = theme.brightness == Brightness.dark; + Widget home = new Scaffold( key: _scaffoldKey, - drawer: new GalleryDrawer( - galleryTheme: widget.galleryTheme, - onThemeChanged: widget.onThemeChanged, - timeDilation: widget.timeDilation, - onTimeDilationChanged: widget.onTimeDilationChanged, - textScaleFactor: widget.textScaleFactor, - onTextScaleFactorChanged: widget.onTextScaleFactorChanged, - showPerformanceOverlay: widget.showPerformanceOverlay, - onShowPerformanceOverlayChanged: widget.onShowPerformanceOverlayChanged, - checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages, - onCheckerboardRasterCacheImagesChanged: widget.onCheckerboardRasterCacheImagesChanged, - checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers, - onCheckerboardOffscreenLayersChanged: widget.onCheckerboardOffscreenLayersChanged, - onPlatformChanged: widget.onPlatformChanged, - overrideDirection: widget.overrideDirection, - onOverrideDirectionChanged: widget.onOverrideDirectionChanged, - onSendFeedback: widget.onSendFeedback, - ), - body: new CustomScrollView( - slivers: [ - const SliverAppBar( - pinned: true, - expandedHeight: _kFlexibleSpaceMaxHeight, - flexibleSpace: const FlexibleSpaceBar( - title: const Text('Flutter Gallery'), - // TODO(abarth): Wire up to the parallax in a way that doesn't pop during hero transition. - background: const _AppBarBackground(animation: kAlwaysDismissedAnimation), - ), + backgroundColor: isDark ? _kFlutterBlue : theme.primaryColor, + body: new SafeArea( + bottom: false, + child: new Backdrop( + backTitle: const Text('Options'), + backLayer: widget.optionsPage, + frontAction: const _FlutterLogo(), + frontTitle: const Text('Flutter gallery'), + frontHeading: new Container(height: 24.0), + frontLayer: new _CategoriesPage( + categories: kAllGalleryDemoCategories, ), - new SliverList(delegate: new SliverChildListDelegate(_galleryListItems())), - ], - ) + ), + ), ); assert(() { diff --git a/examples/flutter_gallery/lib/gallery/icons.dart b/examples/flutter_gallery/lib/gallery/icons.dart new file mode 100644 index 00000000000..7ec4cb2770b --- /dev/null +++ b/examples/flutter_gallery/lib/gallery/icons.dart @@ -0,0 +1,50 @@ +// 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 'package:flutter/material.dart'; + +class GalleryIcons { + GalleryIcons._(); + + static const IconData tooltip = const IconData(0xe900, fontFamily: 'GalleryIcons'); + static const IconData text_fields_alt = const IconData(0xe901, fontFamily: 'GalleryIcons'); + static const IconData tabs = const IconData(0xe902, fontFamily: 'GalleryIcons'); + static const IconData switches = const IconData(0xe903, fontFamily: 'GalleryIcons'); + static const IconData sliders = const IconData(0xe904, fontFamily: 'GalleryIcons'); + static const IconData shrine = const IconData(0xe905, fontFamily: 'GalleryIcons'); + static const IconData sentiment_very_satisfied = const IconData(0xe906, fontFamily: 'GalleryIcons'); + static const IconData refresh = const IconData(0xe907, fontFamily: 'GalleryIcons'); + static const IconData progress_activity = const IconData(0xe908, fontFamily: 'GalleryIcons'); + static const IconData phone_iphone = const IconData(0xe909, fontFamily: 'GalleryIcons'); + static const IconData page_control = const IconData(0xe90a, fontFamily: 'GalleryIcons'); + static const IconData more_vert = const IconData(0xe90b, fontFamily: 'GalleryIcons'); + static const IconData menu = const IconData(0xe90c, fontFamily: 'GalleryIcons'); + static const IconData list_alt = const IconData(0xe90d, fontFamily: 'GalleryIcons'); + static const IconData grid_on = const IconData(0xe90e, fontFamily: 'GalleryIcons'); + static const IconData expand_all = const IconData(0xe90f, fontFamily: 'GalleryIcons'); + static const IconData event = const IconData(0xe910, fontFamily: 'GalleryIcons'); + static const IconData drive_video = const IconData(0xe911, fontFamily: 'GalleryIcons'); + static const IconData dialogs = const IconData(0xe912, fontFamily: 'GalleryIcons'); + static const IconData data_table = const IconData(0xe913, fontFamily: 'GalleryIcons'); + static const IconData custom_typography = const IconData(0xe914, fontFamily: 'GalleryIcons'); + static const IconData colors = const IconData(0xe915, fontFamily: 'GalleryIcons'); + static const IconData chips = const IconData(0xe916, fontFamily: 'GalleryIcons'); + static const IconData check_box = const IconData(0xe917, fontFamily: 'GalleryIcons'); + static const IconData cards = const IconData(0xe918, fontFamily: 'GalleryIcons'); + static const IconData buttons = const IconData(0xe919, fontFamily: 'GalleryIcons'); + static const IconData bottom_sheets = const IconData(0xe91a, fontFamily: 'GalleryIcons'); + static const IconData bottom_navigation = const IconData(0xe91b, fontFamily: 'GalleryIcons'); + static const IconData animation = const IconData(0xe91c, fontFamily: 'GalleryIcons'); + static const IconData account_box = const IconData(0xe91d, fontFamily: 'GalleryIcons'); + static const IconData snackbar = const IconData(0xe91e, fontFamily: 'GalleryIcons'); + static const IconData category_mdc = const IconData(0xe91f, fontFamily: 'GalleryIcons'); + static const IconData cupertino_progress = const IconData(0xe920, fontFamily: 'GalleryIcons'); + static const IconData cupertino_pull_to_refresh = const IconData(0xe921, fontFamily: 'GalleryIcons'); + static const IconData cupertino_switch = const IconData(0xe922, fontFamily: 'GalleryIcons'); + static const IconData generic_buttons = const IconData(0xe923, fontFamily: 'GalleryIcons'); + static const IconData backdrop = const IconData(0xe924, fontFamily: 'GalleryIcons'); + static const IconData bottom_app_bar = const IconData(0xe925, fontFamily: 'GalleryIcons'); + static const IconData bottom_sheet_persistent = const IconData(0xe926, fontFamily: 'GalleryIcons'); + static const IconData lists_leave_behind = const IconData(0xe927, fontFamily: 'GalleryIcons'); +} diff --git a/examples/flutter_gallery/lib/gallery/options.dart b/examples/flutter_gallery/lib/gallery/options.dart new file mode 100644 index 00000000000..f3fbe02a46b --- /dev/null +++ b/examples/flutter_gallery/lib/gallery/options.dart @@ -0,0 +1,466 @@ +// 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 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'about.dart'; +import 'scales.dart'; +import 'themes.dart'; + +class GalleryOptions { + GalleryOptions({ + this.theme, + this.textScaleFactor, + this.textDirection: TextDirection.ltr, + this.timeDilation: 1.0, + this.platform, + this.showOffscreenLayersCheckerboard: false, + this.showRasterCacheImagesCheckerboard: false, + this.showPerformanceOverlay: false, + }); + + final GalleryTheme theme; + final GalleryTextScaleValue textScaleFactor; + final TextDirection textDirection; + final double timeDilation; + final TargetPlatform platform; + final bool showPerformanceOverlay; + final bool showRasterCacheImagesCheckerboard; + final bool showOffscreenLayersCheckerboard; + + GalleryOptions copyWith({ + GalleryTheme theme, + GalleryTextScaleValue textScaleFactor, + TextDirection textDirection, + double timeDilation, + TargetPlatform platform, + bool showPerformanceOverlay, + bool showRasterCacheImagesCheckerboard, + bool showOffscreenLayersCheckerboard, + }) { + return new GalleryOptions( + theme: theme ?? this.theme, + textScaleFactor: textScaleFactor ?? this.textScaleFactor, + textDirection: textDirection ?? this.textDirection, + timeDilation: timeDilation ?? this.timeDilation, + platform: platform ?? this.platform, + showPerformanceOverlay: showPerformanceOverlay ?? this.showPerformanceOverlay, + showOffscreenLayersCheckerboard: showOffscreenLayersCheckerboard ?? this.showOffscreenLayersCheckerboard, + showRasterCacheImagesCheckerboard: showRasterCacheImagesCheckerboard ?? this.showRasterCacheImagesCheckerboard, + ); + } + + @override + bool operator ==(dynamic other) { + if (runtimeType != other.runtimeType) + return false; + final GalleryOptions typedOther = other; + return theme == typedOther.theme + && textScaleFactor == typedOther.textScaleFactor + && textDirection == typedOther.textDirection + && platform == typedOther.platform + && showPerformanceOverlay == typedOther.showPerformanceOverlay + && showRasterCacheImagesCheckerboard == typedOther.showRasterCacheImagesCheckerboard + && showOffscreenLayersCheckerboard == typedOther.showRasterCacheImagesCheckerboard; + } + + @override + int get hashCode => hashValues( + theme, + textScaleFactor, + textDirection, + timeDilation, + platform, + showPerformanceOverlay, + showRasterCacheImagesCheckerboard, + showOffscreenLayersCheckerboard, + ); + + @override + String toString() { + return '$runtimeType($theme)'; + } +} + +const double _kItemHeight = 48.0; +const EdgeInsetsDirectional _kItemPadding = const EdgeInsetsDirectional.only(start: 56.0); + +class _OptionsItem extends StatelessWidget { + const _OptionsItem({ Key key, this.child }) : super(key: key); + + final Widget child; + + @override + Widget build(BuildContext context) { + final double textScaleFactor = MediaQuery.of(context)?.textScaleFactor ?? 1.0; + + return new Container( + constraints: new BoxConstraints(minHeight: _kItemHeight * textScaleFactor), + padding: _kItemPadding, + alignment: AlignmentDirectional.centerStart, + child: new DefaultTextStyle( + style: DefaultTextStyle.of(context).style, + maxLines: 2, + overflow: TextOverflow.fade, + child: new IconTheme( + data: Theme.of(context).primaryIconTheme, + child: child, + ), + ), + ); + } +} + +class _BooleanItem extends StatelessWidget { + const _BooleanItem(this.title, this.value, this.onChanged); + + final String title; + final bool value; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + final bool isDark = Theme.of(context).brightness == Brightness.dark; + return new _OptionsItem( + child: new Row( + children: [ + new Expanded(child: new Text(title)), + new Switch( + value: value, + onChanged: onChanged, + activeColor: const Color(0xFF39CEFD), + activeTrackColor: isDark ? Colors.white30 : Colors.black26, + ), + ], + ), + ); + } +} + +class _ActionItem extends StatelessWidget { + const _ActionItem(this.text, this.onTap); + + final String text; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return new _OptionsItem( + child: new _FlatButton( + onPressed: onTap, + child: new Text(text), + ), + ); + } +} + +class _FlatButton extends StatelessWidget { + const _FlatButton({ Key key, this.onPressed, this.child }) : super(key: key); + + final VoidCallback onPressed; + final Widget child; + + @override + Widget build(BuildContext context) { + return new FlatButton( + padding: EdgeInsets.zero, + onPressed: onPressed, + child: new DefaultTextStyle( + style: Theme.of(context).primaryTextTheme.subhead, + child: child, + ), + ); + } +} + +class _Heading extends StatelessWidget { + const _Heading(this.text); + + final String text; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + return new Semantics( + header: true, + child: new _OptionsItem( + child: new DefaultTextStyle( + style: theme.textTheme.body1.copyWith( + fontFamily: 'GoogleSans', + color: theme.accentColor, + ), + child: new Text(text), + ), + ), + ); + } +} + +class _ThemeItem extends StatelessWidget { + const _ThemeItem(this.options, this.onOptionsChanged); + + final GalleryOptions options; + final ValueChanged onOptionsChanged; + + @override + Widget build(BuildContext context) { + return new _BooleanItem( + 'Dark Theme', + options.theme == kDarkGalleryTheme, + (bool value) { + onOptionsChanged( + options.copyWith( + theme: value ? kDarkGalleryTheme : kLightGalleryTheme, + ), + ); + }, + ); + } +} + +class _TextScaleFactorItem extends StatelessWidget { + const _TextScaleFactorItem(this.options, this.onOptionsChanged); + + final GalleryOptions options; + final ValueChanged onOptionsChanged; + + @override + Widget build(BuildContext context) { + return new _OptionsItem( + child: new Row( + children: [ + new Expanded( + child: new Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Text size'), + new Text( + '${options.textScaleFactor.label}', + style: Theme.of(context).primaryTextTheme.body1, + ), + ], + ), + ), + new PopupMenuButton( + padding: const EdgeInsetsDirectional.only(end: 16.0), + icon: const Icon(Icons.arrow_drop_down), + itemBuilder: (BuildContext context) { + return kAllGalleryTextScaleValues.map((GalleryTextScaleValue scaleValue) { + return new PopupMenuItem( + value: scaleValue, + child: new Text(scaleValue.label), + ); + }).toList(); + }, + onSelected: (GalleryTextScaleValue scaleValue) { + onOptionsChanged( + options.copyWith(textScaleFactor: scaleValue), + ); + }, + ), + ], + ), + ); + } +} + +class _TextDirectionItem extends StatelessWidget { + const _TextDirectionItem(this.options, this.onOptionsChanged); + + final GalleryOptions options; + final ValueChanged onOptionsChanged; + + @override + Widget build(BuildContext context) { + return new _BooleanItem( + 'Force RTL', + options.textDirection == TextDirection.rtl, + (bool value) { + onOptionsChanged( + options.copyWith( + textDirection: value ? TextDirection.rtl : TextDirection.ltr, + ), + ); + }, + ); + } +} + +class _TimeDilationItem extends StatelessWidget { + const _TimeDilationItem(this.options, this.onOptionsChanged); + + final GalleryOptions options; + final ValueChanged onOptionsChanged; + + @override + Widget build(BuildContext context) { + return new _BooleanItem( + 'Slow motion', + options.timeDilation != 1.0, + (bool value) { + onOptionsChanged( + options.copyWith( + timeDilation: value ? 20.0 : 1.0, + ), + ); + }, + ); + } +} + +class _PlatformItem extends StatelessWidget { + const _PlatformItem(this.options, this.onOptionsChanged); + + final GalleryOptions options; + final ValueChanged onOptionsChanged; + + String _platformLabel(TargetPlatform platform) { + switch(platform) { + case TargetPlatform.android: + return 'Mountain View'; + case TargetPlatform.fuchsia: + return 'Fuchsia'; + case TargetPlatform.iOS: + return 'Cupertino'; + } + assert(false); + return null; + } + + @override + Widget build(BuildContext context) { + return new _OptionsItem( + child: new Row( + children: [ + new Expanded( + child: new Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Platform mechanics'), + new Text( + '${_platformLabel(options.platform)}', + style: Theme.of(context).primaryTextTheme.body1, + ), + ], + ), + ), + new PopupMenuButton( + padding: const EdgeInsetsDirectional.only(end: 16.0), + icon: const Icon(Icons.arrow_drop_down), + itemBuilder: (BuildContext context) { + return TargetPlatform.values.map((TargetPlatform platform) { + return new PopupMenuItem( + value: platform, + child: new Text(_platformLabel(platform)), + ); + }).toList(); + }, + onSelected: (TargetPlatform platform) { + onOptionsChanged( + options.copyWith(platform: platform), + ); + }, + ), + ], + ), + ); + } +} + +class GalleryOptionsPage extends StatelessWidget { + const GalleryOptionsPage({ + Key key, + this.options, + this.onOptionsChanged, + this.onSendFeedback, + }) : super(key: key); + + final GalleryOptions options; + final ValueChanged onOptionsChanged; + final VoidCallback onSendFeedback; + + List _enabledDiagnosticItems() { + // Boolean showFoo options with a value of null: don't display + // the showFoo option at all. + if (null == options.showOffscreenLayersCheckerboard + ?? options.showRasterCacheImagesCheckerboard + ?? options.showPerformanceOverlay) + return const []; + + final List items = [ + const Divider(), + const _Heading('Diagnostics'), + ]; + + if (options.showOffscreenLayersCheckerboard != null) { + items.add( + new _BooleanItem( + 'Highlight offscreen layers', + options.showOffscreenLayersCheckerboard, + (bool value) { + onOptionsChanged(options.copyWith(showOffscreenLayersCheckerboard: value)); + } + ), + ); + } + if (options.showRasterCacheImagesCheckerboard != null) { + items.add( + new _BooleanItem( + 'Highlight raster cache images', + options.showRasterCacheImagesCheckerboard, + (bool value) { + onOptionsChanged(options.copyWith(showRasterCacheImagesCheckerboard: value)); + }, + ), + ); + } + if (options.showPerformanceOverlay != null) { + items.add( + new _BooleanItem( + 'Show performance overlay', + options.showPerformanceOverlay, + (bool value) { + onOptionsChanged(options.copyWith(showPerformanceOverlay: value)); + }, + ), + ); + } + + return items; + } + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + + return new DefaultTextStyle( + style: theme.primaryTextTheme.subhead, + child: new ListView( + padding: const EdgeInsets.only(bottom: 124.0), + children: [ + const _Heading('Display'), + new _ThemeItem(options, onOptionsChanged), + new _TextScaleFactorItem(options, onOptionsChanged), + new _TextDirectionItem(options, onOptionsChanged), + new _TimeDilationItem(options, onOptionsChanged), + const Divider(), + const _Heading('Platform mechanics'), + new _PlatformItem(options, onOptionsChanged), + ]..addAll( + _enabledDiagnosticItems(), + )..addAll( + [ + const Divider(), + const _Heading('Flutter gallery'), + new _ActionItem('About Flutter Gallery', () { + showGalleryAboutDialog(context); + }), + new _ActionItem('Send feedback', onSendFeedback), + ], + ), + ), + ); + } +} diff --git a/examples/flutter_gallery/lib/gallery/scales.dart b/examples/flutter_gallery/lib/gallery/scales.dart new file mode 100644 index 00000000000..bd5c4740362 --- /dev/null +++ b/examples/flutter_gallery/lib/gallery/scales.dart @@ -0,0 +1,37 @@ +// 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 'package:flutter/material.dart'; + +class GalleryTextScaleValue { + const GalleryTextScaleValue(this.scale, this.label); + + final double scale; + final String label; + + @override + bool operator ==(dynamic other) { + if (runtimeType != other.runtimeType) + return false; + final GalleryTextScaleValue typedOther = other; + return scale == typedOther.scale && label == typedOther.label; + } + + @override + int get hashCode => hashValues(scale, label); + + @override + String toString() { + return '$runtimeType($label)'; + } + +} + +const List kAllGalleryTextScaleValues = const [ + const GalleryTextScaleValue(null, 'System Default'), + const GalleryTextScaleValue(0.8, 'Small'), + const GalleryTextScaleValue(1.0, 'Normal'), + const GalleryTextScaleValue(1.3, 'Large'), + const GalleryTextScaleValue(2.0, 'Huge'), +]; diff --git a/examples/flutter_gallery/lib/gallery/theme.dart b/examples/flutter_gallery/lib/gallery/theme.dart deleted file mode 100644 index f84b52bdf21..00000000000 --- a/examples/flutter_gallery/lib/gallery/theme.dart +++ /dev/null @@ -1,62 +0,0 @@ -// 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 'package:flutter/material.dart'; - -class GalleryTheme { - const GalleryTheme({ this.name, this.icon, this.theme }); - final String name; - final IconData icon; - final ThemeData theme; -} - -const int _kPurplePrimaryValue = 0xFF6200EE; -const MaterialColor _kPurpleSwatch = const MaterialColor( - _kPurplePrimaryValue, - const { - 50: const Color(0xFFF2E7FE), - 100: const Color(0xFFD7B7FD), - 200: const Color(0xFFBB86FC), - 300: const Color(0xFF9E55FC), - 400: const Color(0xFF7F22FD), - 500: const Color(_kPurplePrimaryValue), - 700: const Color(0xFF3700B3), - 800: const Color(0xFF270096), - 900: const Color(0xFF190078), - } -); - -final List kAllGalleryThemes = [ - new GalleryTheme( - name: 'Light', - icon: Icons.brightness_5, - theme: new ThemeData( - brightness: Brightness.light, - primarySwatch: Colors.blue, - ), - ), - new GalleryTheme( - name: 'Dark', - icon: Icons.brightness_7, - theme: new ThemeData( - brightness: Brightness.dark, - primarySwatch: Colors.blue, - ), - ), - new GalleryTheme( - name: 'Purple', - icon: Icons.brightness_6, - theme: new ThemeData( - brightness: Brightness.light, - primarySwatch: _kPurpleSwatch, - buttonColor: _kPurpleSwatch[500], - splashColor: Colors.white24, - splashFactory: InkRipple.splashFactory, - errorColor: const Color(0xFFFF1744), - buttonTheme: const ButtonThemeData( - textTheme: ButtonTextTheme.primary, - ), - ), - ), -]; diff --git a/examples/flutter_gallery/lib/gallery/themes.dart b/examples/flutter_gallery/lib/gallery/themes.dart new file mode 100644 index 00000000000..18d545c4ed0 --- /dev/null +++ b/examples/flutter_gallery/lib/gallery/themes.dart @@ -0,0 +1,65 @@ +// 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 'package:flutter/material.dart'; + +class GalleryTheme { + const GalleryTheme._(this.name, this.data); + + final String name; + final ThemeData data; +} + +final GalleryTheme kDarkGalleryTheme = new GalleryTheme._('Dark', _buildDarkTheme()); +final GalleryTheme kLightGalleryTheme = new GalleryTheme._('Light', _buildLightTheme()); + +TextTheme _buildTextTheme(TextTheme base) { + return base.copyWith( + title: base.title.copyWith( + fontFamily: 'GoogleSans', + ), + ); +} + +ThemeData _buildDarkTheme() { + const Color primaryColor = const Color(0xFF0175c2); + final ThemeData base = new ThemeData.dark(); + return base.copyWith( + primaryColor: primaryColor, + buttonColor: primaryColor, + indicatorColor: Colors.white, + accentColor: const Color(0xFF13B9FD), + canvasColor: const Color(0xFF202124), + scaffoldBackgroundColor: const Color(0xFF202124), + backgroundColor: const Color(0xFF202124), + buttonTheme: const ButtonThemeData( + textTheme: ButtonTextTheme.primary, + ), + textTheme: _buildTextTheme(base.textTheme), + primaryTextTheme: _buildTextTheme(base.primaryTextTheme), + accentTextTheme: _buildTextTheme(base.accentTextTheme), + ); +} + +ThemeData _buildLightTheme() { + const Color primaryColor = const Color(0xFF0175c2); + final ThemeData base = new ThemeData.light(); + return base.copyWith( + primaryColor: primaryColor, + buttonColor: primaryColor, + indicatorColor: Colors.white, + splashColor: Colors.white24, + splashFactory: InkRipple.splashFactory, + accentColor: const Color(0xFF13B9FD), + canvasColor: Colors.white, + scaffoldBackgroundColor: Colors.white, + backgroundColor: Colors.white, + buttonTheme: const ButtonThemeData( + textTheme: ButtonTextTheme.primary, + ), + textTheme: _buildTextTheme(base.textTheme), + primaryTextTheme: _buildTextTheme(base.primaryTextTheme), + accentTextTheme: _buildTextTheme(base.accentTextTheme), + ); +} diff --git a/examples/flutter_gallery/lib/gallery/updates.dart b/examples/flutter_gallery/lib/gallery/updater.dart similarity index 100% rename from examples/flutter_gallery/lib/gallery/updates.dart rename to examples/flutter_gallery/lib/gallery/updater.dart diff --git a/examples/flutter_gallery/pubspec.yaml b/examples/flutter_gallery/pubspec.yaml index 877d08aaeec..f7bca7446c2 100644 --- a/examples/flutter_gallery/pubspec.yaml +++ b/examples/flutter_gallery/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: flutter_gallery_assets: git: url: https://flutter.googlesource.com/gallery-assets - ref: d318485f208376e06d7e330d9f191141d14722b8 + ref: 43590e625ab1b07f6a5809287ce16f7e61d9e165 charcode: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" meta: 1.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -79,6 +79,11 @@ flutter: uses-material-design: true assets: - lib/gallery/example_code.dart + - packages/flutter_gallery_assets/white_logo/logo.png + - packages/flutter_gallery_assets/white_logo/1.5x/logo.png + - packages/flutter_gallery_assets/white_logo/2.5x/logo.png + - packages/flutter_gallery_assets/white_logo/3.0x/logo.png + - packages/flutter_gallery_assets/white_logo/4.0x/logo.png - packages/flutter_gallery_assets/videos/butterfly.mp4 - packages/flutter_gallery_assets/animated_flutter_lgtm.gif - packages/flutter_gallery_assets/animated_flutter_stickers.webp @@ -166,5 +171,42 @@ flutter: - family: AbrilFatface fonts: - asset: packages/flutter_gallery_assets/shrine/fonts/abrilfatface/AbrilFatface-Regular.ttf + - family: GalleryIcons + fonts: + - asset: packages/flutter_gallery_assets/fonts/GalleryIcons.ttf + - family: GoogleSans + fonts: + - asset: packages/flutter_gallery_assets/fonts/GoogleSans-BoldItalic.ttf + weight: 700 + style: italic + - asset: packages/flutter_gallery_assets/fonts/GoogleSans-Bold.ttf + weight: 700 + - asset: packages/flutter_gallery_assets/fonts/GoogleSans-Italic.ttf + weight: 400 + style: italic + - asset: packages/flutter_gallery_assets/fonts/GoogleSans-MediumItalic.ttf + weight: 500 + style: italic + - asset: packages/flutter_gallery_assets/fonts/GoogleSans-Medium.ttf + weight: 500 + - asset: packages/flutter_gallery_assets/fonts/GoogleSans-Regular.ttf + weight: 400 + - family: GoogleSansDisplay + fonts: + - asset: packages/flutter_gallery_assets/fonts/GoogleSansDisplay-BoldItalic.ttf + weight: 700 + style: italic + - asset: packages/flutter_gallery_assets/fonts/GoogleSansDisplay-Bold.ttf + weight: 700 + - asset: packages/flutter_gallery_assets/fonts/GoogleSansDisplay-Italic.ttf + weight: 400 + style: italic + - asset: packages/flutter_gallery_assets/fonts/GoogleSansDisplay-MediumItalic.ttf + style: italic + weight: 500 + - asset: packages/flutter_gallery_assets/fonts/GoogleSansDisplay-Medium.ttf + weight: 500 + - asset: packages/flutter_gallery_assets/fonts/GoogleSansDisplay-Regular.ttf + weight: 400 # PUBSPEC CHECKSUM: 50c7 diff --git a/examples/flutter_gallery/test/drawer_test.dart b/examples/flutter_gallery/test/drawer_test.dart index 5a108b34ae6..bcc1203a1f5 100644 --- a/examples/flutter_gallery/test/drawer_test.dart +++ b/examples/flutter_gallery/test/drawer_test.dart @@ -14,87 +14,82 @@ void main() { testWidgets('Flutter Gallery drawer item test', (WidgetTester tester) async { bool hasFeedback = false; - void mockOnSendFeedback() { - hasFeedback = true; - } - await tester.pumpWidget(new GalleryApp(onSendFeedback: mockOnSendFeedback)); + await tester.pumpWidget( + new GalleryApp( + onSendFeedback: () { + hasFeedback = true; + }, + ), + ); await tester.pump(); // see https://github.com/flutter/flutter/issues/1865 await tester.pump(); // triggers a frame - final Finder finder = find.byWidgetPredicate((Widget widget) { - return widget is Tooltip && widget.message == 'Open navigation menu'; - }); - expect(finder, findsOneWidget); - - // Open drawer - await tester.tap(finder); - await tester.pump(); // start animation - await tester.pump(const Duration(seconds: 1)); // end animation + // Show the options page + await tester.tap(find.byTooltip('Show options page')); + await tester.pumpAndSettle(); MaterialApp app = find.byType(MaterialApp).evaluate().first.widget; expect(app.theme.brightness, equals(Brightness.light)); - // Change theme - await tester.tap(find.text('Dark')); - await tester.pump(); // start animation - await tester.pump(const Duration(seconds: 1)); // end animation + // Switch to the dark theme: first switch control + await tester.tap(find.byType(Switch).first); + await tester.pumpAndSettle(); app = find.byType(MaterialApp).evaluate().first.widget; expect(app.theme.brightness, equals(Brightness.dark)); expect(app.theme.platform, equals(TargetPlatform.android)); - // Change platform - await tester.tap(find.text('iOS')); - await tester.pump(); // start animation - await tester.pump(const Duration(seconds: 1)); // end animation + // Popup the platform menu: second menu button, choose 'Cupertino' + await tester.tap(find.byIcon(Icons.arrow_drop_down).at(1)); + await tester.pumpAndSettle(); + await tester.tap(find.text('Cupertino').at(1)); + await tester.pumpAndSettle(); app = find.byType(MaterialApp).evaluate().first.widget; expect(app.theme.platform, equals(TargetPlatform.iOS)); // Verify the font scale. - final Size origTextSize = tester.getSize(find.text('Small')); - expect(origTextSize, equals(const Size(176.0, 14.0))); + final Size origTextSize = tester.getSize(find.text('Text size')); + expect(origTextSize, equals(const Size(144.0, 16.0))); - // Switch font scale. + // Popup the text size menu: first menu button, choose 'Small' + await tester.tap(find.byIcon(Icons.arrow_drop_down).first); + await tester.pumpAndSettle(); await tester.tap(find.text('Small')); - await tester.pump(); - await tester.pump(const Duration(seconds: 1)); // Wait until it's changed. - final Size textSize = tester.getSize(find.text('Small')); - expect(textSize, equals(const Size(176.0, 11.0))); + await tester.pumpAndSettle(); + Size textSize = tester.getSize(find.text('Text size')); + expect(textSize, equals(const Size(116.0, 13.0))); - // Set font scale back to default. + // Set font scale back to the default. + await tester.tap(find.byIcon(Icons.arrow_drop_down).first); + await tester.pumpAndSettle(); await tester.tap(find.text('System Default')); - await tester.pump(); - await tester.pump(const Duration(seconds: 1)); // Wait until it's changed. - final Size newTextSize = tester.getSize(find.text('Small')); - expect(newTextSize, equals(origTextSize)); + await tester.pumpAndSettle(); + textSize = tester.getSize(find.text('Text size')); + expect(textSize, origTextSize); - // Scroll to the bottom of the menu. - await tester.drag(find.text('Small'), const Offset(0.0, -1000.0)); - await tester.pump(); - await tester.pump(const Duration(seconds: 1)); // Wait until it's changed. - - // Test slow animations. - expect(timeDilation, equals(1.0)); - await tester.tap(find.text('Animate Slowly')); - await tester.pump(); - await tester.pump(const Duration(seconds: 1)); // Wait until it's changed. + // Switch to slow animation: third switch control + expect(timeDilation, 1.0); + await tester.tap(find.byType(Switch).at(2)); + await tester.pumpAndSettle(); expect(timeDilation, greaterThan(1.0)); - // Put back time dilation (so as not to throw off tests after this one). - await tester.tap(find.text('Animate Slowly')); - await tester.pump(); - await tester.pump(const Duration(seconds: 1)); // Wait until it's changed. - expect(timeDilation, equals(1.0)); + // Restore normal animation: third switch control + await tester.tap(find.byType(Switch).at(2)); + await tester.pumpAndSettle(); + expect(timeDilation, 1.0); // Send feedback. expect(hasFeedback, false); + + // Scroll to the end + await tester.drag(find.text('Text size'), const Offset(0.0, -1000.0)); + await tester.pumpAndSettle(); await tester.tap(find.text('Send feedback')); - await tester.pump(); + await tester.pumpAndSettle(); expect(hasFeedback, true); - // Close drawer - await tester.tap(find.byType(DrawerController)); - await tester.pump(); // start animation - await tester.pump(const Duration(seconds: 1)); // end animation + // Hide the options page + await tester.tap(find.byTooltip('Show options page')); + await tester.pumpAndSettle(); }); } diff --git a/examples/flutter_gallery/test/example_code_display_test.dart b/examples/flutter_gallery/test/example_code_display_test.dart index 8b9624ef80c..bdcba35c7b6 100644 --- a/examples/flutter_gallery/test/example_code_display_test.dart +++ b/examples/flutter_gallery/test/example_code_display_test.dart @@ -18,25 +18,16 @@ void main() { await tester.pump(); // see https://github.com/flutter/flutter/issues/1865 await tester.pump(); // triggers a frame - - // Scroll the Buttons demo into view so that a tap will succeed - final Offset allDemosOrigin = tester.getTopRight(find.text('Vignettes')); - final Finder button = find.text('Buttons'); - while (button.evaluate().isEmpty) { - await tester.dragFrom(allDemosOrigin, const Offset(0.0, -200.0)); - await tester.pumpAndSettle(); - } + Scrollable.ensureVisible(tester.element(find.text('Material')), alignment: 0.5); + await tester.pumpAndSettle(); + await tester.tap(find.text('Material')); + await tester.pumpAndSettle(); // Launch the buttons demo and then prove that showing the example // code dialog does not crash. await tester.tap(find.text('Buttons')); - await tester.pump(); // start animation - await tester.pump(const Duration(seconds: 1)); // end animation - - await tester.tap(find.text('RAISED')); - await tester.pump(); // start animation - await tester.pump(const Duration(seconds: 1)); // end animation + await tester.pumpAndSettle(); await tester.tap(find.byTooltip('Show example code')); await tester.pump(); // start animation diff --git a/examples/flutter_gallery/test/live_smoketest.dart b/examples/flutter_gallery/test/live_smoketest.dart index 08e2c69975e..caebc3ae74e 100644 --- a/examples/flutter_gallery/test/live_smoketest.dart +++ b/examples/flutter_gallery/test/live_smoketest.dart @@ -10,18 +10,15 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_gallery/gallery/app.dart'; -import 'package:flutter_gallery/gallery/item.dart'; +import 'package:flutter_gallery/gallery/demos.dart'; +import 'package:flutter_gallery/gallery/app.dart' show GalleryApp; // Reports success or failure to the native code. const MethodChannel _kTestChannel = const MethodChannel('io.flutter.demo.gallery/TestLifecycleListener'); -// The titles for all of the Gallery demos. -final List _kAllDemos = kAllGalleryItems.map((GalleryItem item) => item.title).toList(); - // We don't want to wait for animations to complete before tapping the // back button in the demos with these titles. -const List _kUnsynchronizedDemos = const [ +const List _kUnsynchronizedDemoTitles = const [ 'Progress indicators', 'Activity Indicator', 'Video', @@ -29,38 +26,45 @@ const List _kUnsynchronizedDemos = const [ // These demos can't be backed out of by tapping a button whose // tooltip is 'Back'. -const List _kSkippedDemos = const [ +const List _kSkippedDemoTitles = const [ 'Pull to refresh', + 'Progress indicators', + 'Activity Indicator', + 'Video', ]; Future main() async { try { // Verify that _kUnsynchronizedDemos and _kSkippedDemos identify // demos that actually exist. - if (!new Set.from(_kAllDemos).containsAll(_kUnsynchronizedDemos)) - fail('Unrecognized demo names in _kUnsynchronizedDemos: $_kUnsynchronizedDemos'); - if (!new Set.from(_kAllDemos).containsAll(_kSkippedDemos)) - fail('Unrecognized demo names in _kSkippedDemos: $_kSkippedDemos'); + final List allDemoTitles = kAllGalleryDemos.map((GalleryDemo demo) => demo.title).toList(); + if (!new Set.from(allDemoTitles).containsAll(_kUnsynchronizedDemoTitles)) + fail('Unrecognized demo titles in _kUnsynchronizedDemosTitles: $_kUnsynchronizedDemoTitles'); + if (!new Set.from(allDemoTitles).containsAll(_kSkippedDemoTitles)) + fail('Unrecognized demo names in _kSkippedDemoTitles: $_kSkippedDemoTitles'); runApp(const GalleryApp()); final _LiveWidgetController controller = new _LiveWidgetController(); - for (String demo in _kAllDemos) { - print('Testing "$demo" demo'); - final Finder menuItem = find.text(demo); - await controller.scrollIntoView(menuItem, alignment: 0.5); + for (GalleryDemoCategory category in kAllGalleryDemoCategories) { + await controller.tap(find.text(category.name)); + for (GalleryDemo demo in kGalleryCategoryToDemos[category]) { + final Finder demoItem = find.text(demo.title); + await controller.scrollIntoView(demoItem, alignment: 0.5); - if (_kSkippedDemos.contains(demo)) { - print('> skipped $demo'); - continue; - } + if (_kSkippedDemoTitles.contains(demo.title)) { + print('> skipped $demo'); + continue; + } - for (int i = 0; i < 2; i += 1) { - await controller.tap(menuItem); // Launch the demo - controller.frameSync = !_kUnsynchronizedDemos.contains(demo); - await controller.tap(find.byTooltip('Back')); - controller.frameSync = true; + for (int i = 0; i < 2; i += 1) { + await controller.tap(demoItem); // Launch the demo + controller.frameSync = !_kUnsynchronizedDemoTitles.contains(demo.title); + await controller.tap(find.byTooltip('Back')); + controller.frameSync = true; + } + print('Success'); } - print('Success'); + await controller.tap(find.byTooltip('Back')); } _kTestChannel.invokeMethod('success'); diff --git a/examples/flutter_gallery/test/pesto_test.dart b/examples/flutter_gallery/test/pesto_test.dart index 9bf2b44a81e..7d6d555aa7d 100644 --- a/examples/flutter_gallery/test/pesto_test.dart +++ b/examples/flutter_gallery/test/pesto_test.dart @@ -17,7 +17,7 @@ void main() { // The bug only manifests itself when the screen's orientation is portrait const Center( child: const SizedBox( - width: 400.0, + width: 450.0, height: 800.0, child: const GalleryApp() ) @@ -26,29 +26,32 @@ void main() { await tester.pump(); // see https://github.com/flutter/flutter/issues/1865 await tester.pump(); // triggers a frame + await tester.tap(find.text('Vignettes')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Pesto')); - await tester.pump(); // Launch pesto - await tester.pump(const Duration(seconds: 1)); // transition is complete + await tester.pumpAndSettle(); await tester.tap(find.text('Pesto Bruschetta')); - await tester.pump(); // Launch the recipe page - await tester.pump(const Duration(seconds: 1)); // transition is complete + await tester.pumpAndSettle(); await tester.drag(find.text('Pesto Bruschetta'), const Offset(0.0, -300.0)); - await tester.pump(); + await tester.pumpAndSettle(); Navigator.pop(find.byType(Scaffold).evaluate().single); - await tester.pump(); - await tester.pump(const Duration(seconds: 1)); // transition is complete + await tester.pumpAndSettle(); }); testWidgets('Pesto can be scrolled all the way down', (WidgetTester tester) async { await tester.pumpWidget(const GalleryApp()); await tester.pump(); // see https://github.com/flutter/flutter/issues/1865 + await tester.pump(); // triggers a frame + + await tester.tap(find.text('Vignettes')); + await tester.pumpAndSettle(); await tester.tap(find.text('Pesto')); - await tester.pump(); // Launch pesto - await tester.pump(const Duration(seconds: 1)); // transition is complete + await tester.pumpAndSettle(); await tester.fling(find.text('Pesto Bruschetta'), const Offset(0.0, -200.0), 10000.0); await tester.pumpAndSettle(); // start and finish fling diff --git a/examples/flutter_gallery/test/simple_smoke_test.dart b/examples/flutter_gallery/test/simple_smoke_test.dart index d889b30ea63..21baf516a04 100644 --- a/examples/flutter_gallery/test/simple_smoke_test.dart +++ b/examples/flutter_gallery/test/simple_smoke_test.dart @@ -16,37 +16,29 @@ void main() { await tester.pump(); // see https://github.com/flutter/flutter/issues/1865 await tester.pump(); // triggers a frame - final Finder finder = find.byWidgetPredicate((Widget widget) { - return widget is Tooltip && widget.message == 'Open navigation menu'; - }); - expect(finder, findsOneWidget); + final Finder showOptionsPageButton = find.byTooltip('Show options page'); - // Open drawer - await tester.tap(finder); - await tester.pump(); // start animation - await tester.pump(const Duration(seconds: 1)); // end animation + // Show the options page + await tester.tap(showOptionsPageButton); + await tester.pumpAndSettle(); - // Change theme - await tester.tap(find.text('Dark')); - await tester.pump(); // start animation - await tester.pump(const Duration(seconds: 1)); // end animation + // Switch to the dark theme: the first switch control + await tester.tap(find.byType(Switch).first); + await tester.pumpAndSettle(); - // Close drawer - await tester.tap(find.byType(DrawerController)); - await tester.pump(); // start animation - await tester.pump(const Duration(seconds: 1)); // end animation + // Close the options page + expect(showOptionsPageButton, findsOneWidget); + await tester.tap(showOptionsPageButton); + await tester.pumpAndSettle(); - // Open Demos + // Show the vignettes await tester.tap(find.text('Vignettes')); - await tester.pump(); // start animation - await tester.pump(const Duration(seconds: 1)); // end animation + await tester.pumpAndSettle(); - // Open Flexible space toolbar + // Show the Contact profile demo and scroll it upwards await tester.tap(find.text('Contact profile')); - await tester.pump(); // start animation - await tester.pump(const Duration(seconds: 1)); // end animation + await tester.pumpAndSettle(); - // Scroll it up await tester.drag(find.text('(650) 555-1234'), const Offset(0.0, -50.0)); await tester.pump(const Duration(milliseconds: 200)); await tester.drag(find.text('(650) 555-1234'), const Offset(0.0, -50.0)); diff --git a/examples/flutter_gallery/test/smoke_test.dart b/examples/flutter_gallery/test/smoke_test.dart index 888a9f1143a..18392629e2b 100644 --- a/examples/flutter_gallery/test/smoke_test.dart +++ b/examples/flutter_gallery/test/smoke_test.dart @@ -2,31 +2,21 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:collection' show LinkedHashSet; import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_gallery/gallery/item.dart' show GalleryItem, kAllGalleryItems; +import 'package:flutter_gallery/gallery/demos.dart'; import 'package:flutter_gallery/gallery/app.dart' show GalleryApp; -const String kCaption = 'Flutter Gallery'; +// This title is visible on the home and demo category pages. It's +// not visible when the demos are running. +const String kGalleryTitle = 'Flutter gallery'; -final List demoCategories = new LinkedHashSet.from( - kAllGalleryItems.map((GalleryItem item) => item.category) -).toList(); - -final List routeNames = - kAllGalleryItems.map((GalleryItem item) => item.routeName).toList(); - -Finder findGalleryItemByRouteName(WidgetTester tester, String routeName) { - return find.byWidgetPredicate((Widget widget) { - return widget is GalleryItem && widget.routeName == routeName; - }); -} - -int errors = 0; +// All of the classes printed by debugDump etc, must have toString() +// values approved by verityToStringOutput(). +int toStringErrors = 0; void reportToStringError(String name, String route, int lineNumber, List lines, String message) { // If you're on line 12, then it has index 11. @@ -36,7 +26,7 @@ void reportToStringError(String name, String route, int lineNumber, List final int firstLine = math.max(0, lineNumber - margin); final int lastLine = math.min(lines.length, lineNumber + margin); print('$name : $route : line $lineNumber of ${lines.length} : $message; nearby lines were:\n ${lines.sublist(firstLine, lastLine).join("\n ")}'); - errors += 1; + toStringErrors += 1; } void verifyToStringOutput(String name, String route, String testString) { @@ -56,22 +46,16 @@ void verifyToStringOutput(String name, String route, String testString) { } } -// Start a gallery demo and then go back. This function assumes that the -// we're starting on the home route and that the submenu that contains -// the item for a demo that pushes route 'routeName' is already open. -Future smokeDemo(WidgetTester tester, String routeName) async { - // Ensure that we're (likely to be) on the home page - final Finder menuItem = findGalleryItemByRouteName(tester, routeName); - expect(menuItem, findsOneWidget); - +Future smokeDemo(WidgetTester tester, GalleryDemo demo) async { + print(demo); // Don't use pumpUntilNoTransientCallbacks in this function, because some of // the smoketests have infinitely-running animations (e.g. the progress // indicators demo). - await tester.tap(menuItem); + await tester.tap(find.text(demo.title)); await tester.pump(); // Launch the demo. await tester.pump(const Duration(milliseconds: 400)); // Wait until the demo has opened. - expect(find.text(kCaption), findsNothing); + expect(find.text(kGalleryTitle), findsNothing); // Leave the demo on the screen briefly for manual testing. await tester.pump(const Duration(milliseconds: 400)); @@ -85,6 +69,7 @@ Future smokeDemo(WidgetTester tester, String routeName) async { await tester.pump(const Duration(milliseconds: 400)); // Verify that the dumps are pretty. + final String routeName = demo.routeName; verifyToStringOutput('debugDumpApp', routeName, WidgetsBinding.instance.renderViewElement.toStringDeep()); verifyToStringOutput('debugDumpRenderTree', routeName, RendererBinding.instance?.renderView?.toStringDeep()); verifyToStringOutput('debugDumpLayerTree', routeName, RendererBinding.instance?.renderView?.debugLayer?.toStringDeep()); @@ -108,74 +93,85 @@ Future smokeDemo(WidgetTester tester, String routeName) async { await tester.pump(); // Start the pop "back" operation. await tester.pump(); // Complete the willPop() Future. await tester.pump(const Duration(milliseconds: 400)); // Wait until it has finished. - - return null; } -Future runSmokeTest(WidgetTester tester) async { - bool hasFeedback = false; - void mockOnSendFeedback() { - hasFeedback = true; - } +Future smokeOptionsPage(WidgetTester tester) async { + final Finder showOptionsPageButton = find.byTooltip('Show options page'); - await tester.pumpWidget(new GalleryApp(onSendFeedback: mockOnSendFeedback)); + // Show the options page + await tester.tap(showOptionsPageButton); + await tester.pumpAndSettle(); + + // Switch to the dark theme: first switch control + await tester.tap(find.byType(Switch).first); + await tester.pumpAndSettle(); + + // Switch back to the light theme: first switch control again + await tester.tap(find.byType(Switch).first); + await tester.pumpAndSettle(); + + // Popup the text size menu: first menu button, choose 'Small' + await tester.tap(find.byIcon(Icons.arrow_drop_down).first); + await tester.pumpAndSettle(); + await tester.tap(find.text('Small')); + await tester.pumpAndSettle(); + + // Popup the text size menu: first menu button, choose 'Normal' + await tester.tap(find.byIcon(Icons.arrow_drop_down).first); + await tester.pumpAndSettle(); + await tester.tap(find.text('Normal')); + await tester.pumpAndSettle(); + + // Scroll the 'Send feedback' item into view + await tester.drag(find.text('Normal'), const Offset(0.0, -1000.0)); + await tester.pumpAndSettle(); + await tester.tap(find.text('Send feedback')); + await tester.pumpAndSettle(); + + // Close the options page + expect(showOptionsPageButton, findsOneWidget); + await tester.tap(showOptionsPageButton); + await tester.pumpAndSettle(); +} + +Future smokeGallery(WidgetTester tester) async { + bool sendFeedbackButtonPressed = false; + + await tester.pumpWidget( + new GalleryApp( + onSendFeedback: () { + sendFeedbackButtonPressed = true; // see smokeOptionsPage() + }, + ), + ); await tester.pump(); // see https://github.com/flutter/flutter/issues/1865 await tester.pump(); // triggers a frame - expect(find.text(kCaption), findsOneWidget); + expect(find.text(kGalleryTitle), findsOneWidget); - for (String routeName in routeNames) { - final Finder finder = findGalleryItemByRouteName(tester, routeName); - Scrollable.ensureVisible(tester.element(finder), alignment: 0.5); + for (GalleryDemoCategory category in kAllGalleryDemoCategories) { + await tester.tap(find.text(category.name)); + await tester.pumpAndSettle(); + for (GalleryDemo demo in kGalleryCategoryToDemos[category]) { + Scrollable.ensureVisible(tester.element(find.text(demo.title)), alignment: 0.5); + await smokeDemo(tester, demo); + tester.binding.debugAssertNoTransientCallbacks('A transient callback was still active after running $demo'); + } + await tester.pageBack(); await tester.pumpAndSettle(); - await smokeDemo(tester, routeName); - tester.binding.debugAssertNoTransientCallbacks('A transient callback was still active after leaving route $routeName'); } - expect(errors, 0); + expect(toStringErrors, 0); - final Finder navigationMenuButton = find.byTooltip('Open navigation menu'); - expect(navigationMenuButton, findsOneWidget); - await tester.tap(navigationMenuButton); - await tester.pump(); // Start opening drawer. - await tester.pump(const Duration(seconds: 1)); // Wait until it's really opened. - - // Switch theme. - await tester.tap(find.text('Dark')); - await tester.pump(); - await tester.pump(const Duration(seconds: 1)); // Wait until it's changed. - - // Switch theme. - await tester.tap(find.text('Light')); - await tester.pump(); - await tester.pump(const Duration(seconds: 1)); // Wait until it's changed. - - // Switch font scale. - await tester.tap(find.text('Small')); - await tester.pump(); - await tester.pump(const Duration(seconds: 1)); // Wait until it's changed. - // Switch font scale back to default. - await tester.tap(find.text('System Default')); - await tester.pump(); - await tester.pump(const Duration(seconds: 1)); // Wait until it's changed. - - // Scroll the 'Send feedback' item into view. - await tester.drag(find.text('Small'), const Offset(0.0, -1000.0)); - await tester.pump(); - await tester.pump(const Duration(seconds: 1)); // Wait until it's changed. - - // Send feedback. - expect(hasFeedback, false); - await tester.tap(find.text('Send feedback')); - await tester.pump(); - expect(hasFeedback, true); + await smokeOptionsPage(tester); + expect(sendFeedbackButtonPressed, true); } void main() { - testWidgets('Flutter Gallery app smoke test', runSmokeTest); + testWidgets('Flutter Gallery app smoke test', smokeGallery); testWidgets('Flutter Gallery app smoke test with semantics', (WidgetTester tester) async { RendererBinding.instance.setSemanticsEnabled(true); - await runSmokeTest(tester); + await smokeGallery(tester); RendererBinding.instance.setSemanticsEnabled(false); }); } diff --git a/examples/flutter_gallery/test/update_test.dart b/examples/flutter_gallery/test/update_test.dart index 9d3b1263c1b..f757a970019 100644 --- a/examples/flutter_gallery/test/update_test.dart +++ b/examples/flutter_gallery/test/update_test.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_gallery/gallery/app.dart'; +import 'package:flutter_gallery/gallery/app.dart' show GalleryApp; Future mockUpdateUrlFetcher() { // A real implementation would connect to the network to retrieve this value @@ -26,8 +26,8 @@ void main() { await tester.tap(find.text('NO THANKS')); await tester.pump(); - await tester.tap(find.text('Shrine')); - await tester.pump(); // Launch shrine + await tester.tap(find.text('Vignettes')); + await tester.pump(); // Launch await tester.pump(const Duration(seconds: 1)); // transition is complete final Finder backButton = find.byTooltip('Back'); diff --git a/examples/flutter_gallery/test_driver/memory_nav_test.dart b/examples/flutter_gallery/test_driver/memory_nav_test.dart index 29390de9431..8771a2fc369 100644 --- a/examples/flutter_gallery/test_driver/memory_nav_test.dart +++ b/examples/flutter_gallery/test_driver/memory_nav_test.dart @@ -14,14 +14,17 @@ void main() { }); test('navigation', () async { - final SerializableFinder menuItem = find.text('Text fields'); - await driver.scrollUntilVisible(find.byType('CustomScrollView'), menuItem, + await driver.tap(find.text('Material')); + + final SerializableFinder demoList = find.byValueKey('GalleryDemoList'); + final SerializableFinder demoItem = find.text('Text fields'); + await driver.scrollUntilVisible(demoList, demoItem, dyScroll: -300.0, alignment: 0.5, timeout: const Duration(minutes: 1), ); for (int i = 0; i < 15; i++) { - await driver.tap(menuItem); + await driver.tap(demoItem); await driver.tap(find.byTooltip('Back')); } }); diff --git a/examples/flutter_gallery/test_driver/scroll_perf_test.dart b/examples/flutter_gallery/test_driver/scroll_perf_test.dart index 48368fb3682..8ed957a6ffb 100644 --- a/examples/flutter_gallery/test_driver/scroll_perf_test.dart +++ b/examples/flutter_gallery/test_driver/scroll_perf_test.dart @@ -22,24 +22,21 @@ void main() { test('measure', () async { final Timeline timeline = await driver.traceAction(() async { - final SerializableFinder home = find.byValueKey('Gallery List'); - expect(home, isNotNull); + await driver.tap(find.text('Material')); - await driver.tap(find.text('Vignettes')); - await driver.tap(find.text('Components')); - await driver.tap(find.text('Style')); + final SerializableFinder demoList = find.byValueKey('GalleryDemoList'); // TODO(eseidel): These are very artificial scrolls, we should use better // https://github.com/flutter/flutter/issues/3316 // Scroll down for (int i = 0; i < 5; i++) { - await driver.scroll(home, 0.0, -300.0, const Duration(milliseconds: 300)); + await driver.scroll(demoList, 0.0, -300.0, const Duration(milliseconds: 300)); await new Future.delayed(const Duration(milliseconds: 500)); } // Scroll up for (int i = 0; i < 5; i++) { - await driver.scroll(home, 0.0, 300.0, const Duration(milliseconds: 300)); + await driver.scroll(demoList, 0.0, 300.0, const Duration(milliseconds: 300)); await new Future.delayed(const Duration(milliseconds: 500)); } }); diff --git a/examples/flutter_gallery/test_driver/transitions_perf.dart b/examples/flutter_gallery/test_driver/transitions_perf.dart index dcb401c0e91..ea21b2db6c4 100644 --- a/examples/flutter_gallery/test_driver/transitions_perf.dart +++ b/examples/flutter_gallery/test_driver/transitions_perf.dart @@ -6,13 +6,13 @@ import 'dart:async'; import 'dart:convert' show JsonEncoder; import 'package:flutter_driver/driver_extension.dart'; -import 'package:flutter_gallery/gallery/item.dart'; +import 'package:flutter_gallery/gallery/demos.dart'; import 'package:flutter_gallery/main.dart' as app; Future _handleMessages(String message) async { assert(message == 'demoNames'); return const JsonEncoder.withIndent(' ').convert( - kAllGalleryItems.map((GalleryItem item) => item.title).toList(), + kAllGalleryDemos.map((GalleryDemo demo) => '${demo.title}@${demo.category.name}').toList(), ); } diff --git a/examples/flutter_gallery/test_driver/transitions_perf_test.dart b/examples/flutter_gallery/test_driver/transitions_perf_test.dart index 11289347463..876cb921d50 100644 --- a/examples/flutter_gallery/test_driver/transitions_perf_test.dart +++ b/examples/flutter_gallery/test_driver/transitions_perf_test.dart @@ -45,8 +45,7 @@ const List kUnsynchronizedDemos = const [ 'Video', ]; -// All of the gallery demo titles in the order they appear on the -// gallery home page. +// All of the gallery demos, identified as "title@category". // // These names are reported by the test app, see _handleMessages() // in transitions_perf.dart. @@ -121,20 +120,26 @@ Future saveDurationsHistogram(List> events, String ou /// Scrolls each demo menu item into view, launches it, then returns to the /// home screen twice. Future runDemos(List demos, FlutterDriver driver) async { + final SerializableFinder demoList = find.byValueKey('GalleryDemoList'); + String currentDemoCategory; + for (String demo in demos) { - print('Testing "$demo" demo'); - final SerializableFinder menuItem = find.text(demo); - await driver.scrollUntilVisible(find.byType('CustomScrollView'), menuItem, - dyScroll: -48.0, - alignment: 0.5, - ); + final String demoAtCategory = _allDemos.firstWhere((String s) => s.startsWith(demo)); + final String demoCategory = demoAtCategory.substring(demoAtCategory.indexOf('@') + 1); + + if (currentDemoCategory == null) { + await driver.tap(find.text(demoCategory)); + } else if (currentDemoCategory != demoCategory) { + await driver.tap(find.byTooltip('Back')); + await driver.tap(find.text(demoCategory)); + } + currentDemoCategory = demoCategory; + + final SerializableFinder demoItem = find.text(demo); + await driver.scrollUntilVisible(demoList, demoItem, dyScroll: -48.0, alignment: 0.5); for (int i = 0; i < 2; i += 1) { - await driver.tap(menuItem); // Launch the demo - - // This demo's back button isn't initially visible. - if (demo == 'Backdrop') - await driver.tap(find.byTooltip('Tap to dismiss')); + await driver.tap(demoItem); // Launch the demo if (kUnsynchronizedDemos.contains(demo)) { await driver.runUnsynchronized>(() async { @@ -144,8 +149,12 @@ Future runDemos(List demos, FlutterDriver driver) async { await driver.tap(find.byTooltip('Back')); } } + print('Success'); } + + // Return to the home screen + await driver.tap(find.byTooltip('Back')); } void main([List args = const []]) { @@ -171,6 +180,7 @@ void main([List args = const []]) { }); test('all demos', () async { + // Collect timeline data for just a limited set of demos to avoid OOMs. final Timeline timeline = await driver.traceAction( () async { @@ -190,14 +200,9 @@ void main([List args = const []]) { final String histogramPath = path.join(testOutputsDirectory, 'transition_durations.timeline.json'); await saveDurationsHistogram(timeline.json['traceEvents'], histogramPath); - // Scroll back to the top - await driver.scrollUntilVisible(find.byType('CustomScrollView'), find.text(_allDemos[0]), - dyScroll: 200.0, - alignment: 0.0 - ); - // Execute the remaining tests. - final Set unprofiledDemos = new Set.from(_allDemos)..removeAll(kProfiledDemos); + final List allDemoNames = _allDemos.map((String s) => s.substring(0, s.indexOf('@'))); + final Set unprofiledDemos = new Set.from(allDemoNames)..removeAll(kProfiledDemos); await runDemos(unprofiledDemos.toList(), driver); }, timeout: const Timeout(const Duration(minutes: 5)));