Unverified Commit 7038597b authored by Hans Muller's avatar Hans Muller Committed by GitHub

New Flutter Gallery UI (#16936)

A new front-end for the Flutter Gallery example.
parent 76aa0287
...@@ -11,7 +11,7 @@ dependencies: ...@@ -11,7 +11,7 @@ dependencies:
flutter_gallery_assets: flutter_gallery_assets:
git: git:
url: https://flutter.googlesource.com/gallery-assets 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" 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" charcode: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
......
// 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: <Widget>[
new Padding(
padding: const EdgeInsets.only(top: 24.0),
child: new RichText(
text: new TextSpan(
children: <TextSpan>[
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: '.',
),
],
),
),
),
],
);
}
...@@ -8,53 +8,79 @@ import 'package:flutter/foundation.dart' show defaultTargetPlatform; ...@@ -8,53 +8,79 @@ import 'package:flutter/foundation.dart' show defaultTargetPlatform;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart' show timeDilation; import 'package:flutter/scheduler.dart' show timeDilation;
import 'package:url_launcher/url_launcher.dart';
import 'demos.dart';
import 'home.dart'; import 'home.dart';
import 'item.dart'; import 'options.dart';
import 'theme.dart'; import 'scales.dart';
import 'updates.dart'; import 'themes.dart';
import 'updater.dart';
class GalleryApp extends StatefulWidget { class GalleryApp extends StatefulWidget {
const GalleryApp({ const GalleryApp({
Key key,
this.updateUrlFetcher, this.updateUrlFetcher,
this.enablePerformanceOverlay: true, this.enablePerformanceOverlay: true,
this.checkerboardRasterCacheImages: true, this.enableRasterCacheImagesCheckerboard: true,
this.checkerboardOffscreenLayers: true, this.enableOffscreenLayersCheckerboard: true,
this.onSendFeedback, this.onSendFeedback,
Key key} }) : super(key: key);
) : super(key: key);
final UpdateUrlFetcher updateUrlFetcher; final UpdateUrlFetcher updateUrlFetcher;
final bool enablePerformanceOverlay; final bool enablePerformanceOverlay;
final bool enableRasterCacheImagesCheckerboard;
final bool checkerboardRasterCacheImages; final bool enableOffscreenLayersCheckerboard;
final bool checkerboardOffscreenLayers;
final VoidCallback onSendFeedback; final VoidCallback onSendFeedback;
@override @override
GalleryAppState createState() => new GalleryAppState(); _GalleryAppState createState() => new _GalleryAppState();
} }
class GalleryAppState extends State<GalleryApp> { class _GalleryAppState extends State<GalleryApp> {
GalleryTheme _galleryTheme = kAllGalleryThemes[0]; GalleryOptions _options;
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;
Timer _timeDilationTimer; Timer _timeDilationTimer;
Map<String, WidgetBuilder> _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<String, WidgetBuilder>.fromIterable(
kAllGalleryDemos,
key: (dynamic demo) => '${demo.routeName}',
value: (dynamic demo) => demo.buildRoute,
)..addAll(
new Map<String, WidgetBuilder>.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 @override
void initState() { void initState() {
_timeDilation = timeDilation;
super.initState(); super.initState();
_options = new GalleryOptions(
theme: kLightGalleryTheme,
textScaleFactor: kAllGalleryTextScaleValues[0],
timeDilation: timeDilation,
platform: defaultTargetPlatform,
);
} }
@override @override
...@@ -64,80 +90,50 @@ class GalleryAppState extends State<GalleryApp> { ...@@ -64,80 +90,50 @@ class GalleryAppState extends State<GalleryApp> {
super.dispose(); 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( return new Builder(
builder: (BuildContext context) => new MediaQuery( builder: (BuildContext context) {
data: MediaQuery.of(context).copyWith( return new MediaQuery(
textScaleFactor: _textScaleFactor, data: MediaQuery.of(context).copyWith(
), textScaleFactor: _options.textScaleFactor.scale,
child: child, ),
), child: child,
);
},
); );
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget home = new GalleryHome( Widget home = new GalleryHome(
galleryTheme: _galleryTheme, optionsPage: new GalleryOptionsPage(
onThemeChanged: (GalleryTheme value) { options: _options,
setState(() { onOptionsChanged: _handleOptionsChanged,
_galleryTheme = value; onSendFeedback: widget.onSendFeedback ?? () {
}); launch('https://github.com/flutter/flutter/issues/new');
}, },
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,
); );
if (widget.updateUrlFetcher != null) { if (widget.updateUrlFetcher != null) {
...@@ -147,31 +143,21 @@ class GalleryAppState extends State<GalleryApp> { ...@@ -147,31 +143,21 @@ class GalleryAppState extends State<GalleryApp> {
); );
} }
final Map<String, WidgetBuilder> _kRoutes = <String, WidgetBuilder>{};
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( return new MaterialApp(
theme: _options.theme.data.copyWith(platform: _options.platform),
title: 'Flutter Gallery', title: 'Flutter Gallery',
color: Colors.grey, color: Colors.grey,
theme: _galleryTheme.theme.copyWith(platform: _platform ?? defaultTargetPlatform), showPerformanceOverlay: _options.showPerformanceOverlay,
showPerformanceOverlay: _showPerformanceOverlay, checkerboardOffscreenLayers: _options.showOffscreenLayersCheckerboard,
checkerboardRasterCacheImages: _checkerboardRasterCacheImages, checkerboardRasterCacheImages: _options.showRasterCacheImagesCheckerboard,
checkerboardOffscreenLayers: _checkerboardOffscreenLayers, routes: _buildRoutes(),
routes: _kRoutes,
home: home,
builder: (BuildContext context, Widget child) { builder: (BuildContext context, Widget child) {
return new Directionality( return new Directionality(
textDirection: _overrideDirection, textDirection: _options.textDirection,
child: _applyScaleFactor(child), child: _applyTextScaleFactor(child),
); );
}, },
home: home,
); );
} }
} }
// 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<BorderRadius> _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<double> 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<double> 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: <Widget>[
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<Widget> children = <Widget>[
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<Backdrop> 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<RelativeRect> 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: <Widget>[
new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
// 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);
}
}
This diff is collapsed.
// 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');
}
This diff is collapsed.
// 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<GalleryTextScaleValue> kAllGalleryTextScaleValues = const <GalleryTextScaleValue>[
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'),
];
// 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 <int, Color> {
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<GalleryTheme> kAllGalleryThemes = <GalleryTheme>[
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,
),
),
),
];
// 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),
);
}
...@@ -15,7 +15,7 @@ dependencies: ...@@ -15,7 +15,7 @@ dependencies:
flutter_gallery_assets: flutter_gallery_assets:
git: git:
url: https://flutter.googlesource.com/gallery-assets 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" 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" meta: 1.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
...@@ -79,6 +79,11 @@ flutter: ...@@ -79,6 +79,11 @@ flutter:
uses-material-design: true uses-material-design: true
assets: assets:
- lib/gallery/example_code.dart - 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/videos/butterfly.mp4
- packages/flutter_gallery_assets/animated_flutter_lgtm.gif - packages/flutter_gallery_assets/animated_flutter_lgtm.gif
- packages/flutter_gallery_assets/animated_flutter_stickers.webp - packages/flutter_gallery_assets/animated_flutter_stickers.webp
...@@ -166,5 +171,42 @@ flutter: ...@@ -166,5 +171,42 @@ flutter:
- family: AbrilFatface - family: AbrilFatface
fonts: fonts:
- asset: packages/flutter_gallery_assets/shrine/fonts/abrilfatface/AbrilFatface-Regular.ttf - 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 # PUBSPEC CHECKSUM: 50c7
...@@ -14,87 +14,82 @@ void main() { ...@@ -14,87 +14,82 @@ void main() {
testWidgets('Flutter Gallery drawer item test', (WidgetTester tester) async { testWidgets('Flutter Gallery drawer item test', (WidgetTester tester) async {
bool hasFeedback = false; 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(); // see https://github.com/flutter/flutter/issues/1865
await tester.pump(); // triggers a frame await tester.pump(); // triggers a frame
final Finder finder = find.byWidgetPredicate((Widget widget) { // Show the options page
return widget is Tooltip && widget.message == 'Open navigation menu'; await tester.tap(find.byTooltip('Show options page'));
}); await tester.pumpAndSettle();
expect(finder, findsOneWidget);
// Open drawer
await tester.tap(finder);
await tester.pump(); // start animation
await tester.pump(const Duration(seconds: 1)); // end animation
MaterialApp app = find.byType(MaterialApp).evaluate().first.widget; MaterialApp app = find.byType(MaterialApp).evaluate().first.widget;
expect(app.theme.brightness, equals(Brightness.light)); expect(app.theme.brightness, equals(Brightness.light));
// Change theme // Switch to the dark theme: first switch control
await tester.tap(find.text('Dark')); await tester.tap(find.byType(Switch).first);
await tester.pump(); // start animation await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1)); // end animation
app = find.byType(MaterialApp).evaluate().first.widget; app = find.byType(MaterialApp).evaluate().first.widget;
expect(app.theme.brightness, equals(Brightness.dark)); expect(app.theme.brightness, equals(Brightness.dark));
expect(app.theme.platform, equals(TargetPlatform.android)); expect(app.theme.platform, equals(TargetPlatform.android));
// Change platform // Popup the platform menu: second menu button, choose 'Cupertino'
await tester.tap(find.text('iOS')); await tester.tap(find.byIcon(Icons.arrow_drop_down).at(1));
await tester.pump(); // start animation await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1)); // end animation await tester.tap(find.text('Cupertino').at(1));
await tester.pumpAndSettle();
app = find.byType(MaterialApp).evaluate().first.widget; app = find.byType(MaterialApp).evaluate().first.widget;
expect(app.theme.platform, equals(TargetPlatform.iOS)); expect(app.theme.platform, equals(TargetPlatform.iOS));
// Verify the font scale. // Verify the font scale.
final Size origTextSize = tester.getSize(find.text('Small')); final Size origTextSize = tester.getSize(find.text('Text size'));
expect(origTextSize, equals(const Size(176.0, 14.0))); 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.tap(find.text('Small'));
await tester.pump(); await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1)); // Wait until it's changed. Size textSize = tester.getSize(find.text('Text size'));
final Size textSize = tester.getSize(find.text('Small')); expect(textSize, equals(const Size(116.0, 13.0)));
expect(textSize, equals(const Size(176.0, 11.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.tap(find.text('System Default'));
await tester.pump(); await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1)); // Wait until it's changed. textSize = tester.getSize(find.text('Text size'));
final Size newTextSize = tester.getSize(find.text('Small')); expect(textSize, origTextSize);
expect(newTextSize, equals(origTextSize));
// Switch to slow animation: third switch control
// Scroll to the bottom of the menu. expect(timeDilation, 1.0);
await tester.drag(find.text('Small'), const Offset(0.0, -1000.0)); await tester.tap(find.byType(Switch).at(2));
await tester.pump(); await tester.pumpAndSettle();
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.
expect(timeDilation, greaterThan(1.0)); expect(timeDilation, greaterThan(1.0));
// Put back time dilation (so as not to throw off tests after this one). // Restore normal animation: third switch control
await tester.tap(find.text('Animate Slowly')); await tester.tap(find.byType(Switch).at(2));
await tester.pump(); await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1)); // Wait until it's changed. expect(timeDilation, 1.0);
expect(timeDilation, equals(1.0));
// Send feedback. // Send feedback.
expect(hasFeedback, false); 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.tap(find.text('Send feedback'));
await tester.pump(); await tester.pumpAndSettle();
expect(hasFeedback, true); expect(hasFeedback, true);
// Close drawer // Hide the options page
await tester.tap(find.byType(DrawerController)); await tester.tap(find.byTooltip('Show options page'));
await tester.pump(); // start animation await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1)); // end animation
}); });
} }
...@@ -18,25 +18,16 @@ void main() { ...@@ -18,25 +18,16 @@ void main() {
await tester.pump(); // see https://github.com/flutter/flutter/issues/1865 await tester.pump(); // see https://github.com/flutter/flutter/issues/1865
await tester.pump(); // triggers a frame await tester.pump(); // triggers a frame
Scrollable.ensureVisible(tester.element(find.text('Material')), alignment: 0.5);
// Scroll the Buttons demo into view so that a tap will succeed await tester.pumpAndSettle();
final Offset allDemosOrigin = tester.getTopRight(find.text('Vignettes')); await tester.tap(find.text('Material'));
final Finder button = find.text('Buttons'); await tester.pumpAndSettle();
while (button.evaluate().isEmpty) {
await tester.dragFrom(allDemosOrigin, const Offset(0.0, -200.0));
await tester.pumpAndSettle();
}
// Launch the buttons demo and then prove that showing the example // Launch the buttons demo and then prove that showing the example
// code dialog does not crash. // code dialog does not crash.
await tester.tap(find.text('Buttons')); await tester.tap(find.text('Buttons'));
await tester.pump(); // start animation await tester.pumpAndSettle();
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.tap(find.byTooltip('Show example code')); await tester.tap(find.byTooltip('Show example code'));
await tester.pump(); // start animation await tester.pump(); // start animation
......
...@@ -10,18 +10,15 @@ import 'package:flutter/material.dart'; ...@@ -10,18 +10,15 @@ import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_gallery/gallery/app.dart'; import 'package:flutter_gallery/gallery/demos.dart';
import 'package:flutter_gallery/gallery/item.dart'; import 'package:flutter_gallery/gallery/app.dart' show GalleryApp;
// Reports success or failure to the native code. // Reports success or failure to the native code.
const MethodChannel _kTestChannel = const MethodChannel('io.flutter.demo.gallery/TestLifecycleListener'); const MethodChannel _kTestChannel = const MethodChannel('io.flutter.demo.gallery/TestLifecycleListener');
// The titles for all of the Gallery demos.
final List<String> _kAllDemos = kAllGalleryItems.map((GalleryItem item) => item.title).toList();
// We don't want to wait for animations to complete before tapping the // We don't want to wait for animations to complete before tapping the
// back button in the demos with these titles. // back button in the demos with these titles.
const List<String> _kUnsynchronizedDemos = const <String>[ const List<String> _kUnsynchronizedDemoTitles = const <String>[
'Progress indicators', 'Progress indicators',
'Activity Indicator', 'Activity Indicator',
'Video', 'Video',
...@@ -29,38 +26,45 @@ const List<String> _kUnsynchronizedDemos = const <String>[ ...@@ -29,38 +26,45 @@ const List<String> _kUnsynchronizedDemos = const <String>[
// These demos can't be backed out of by tapping a button whose // These demos can't be backed out of by tapping a button whose
// tooltip is 'Back'. // tooltip is 'Back'.
const List<String> _kSkippedDemos = const <String>[ const List<String> _kSkippedDemoTitles = const <String>[
'Pull to refresh', 'Pull to refresh',
'Progress indicators',
'Activity Indicator',
'Video',
]; ];
Future<Null> main() async { Future<Null> main() async {
try { try {
// Verify that _kUnsynchronizedDemos and _kSkippedDemos identify // Verify that _kUnsynchronizedDemos and _kSkippedDemos identify
// demos that actually exist. // demos that actually exist.
if (!new Set<String>.from(_kAllDemos).containsAll(_kUnsynchronizedDemos)) final List<String> allDemoTitles = kAllGalleryDemos.map((GalleryDemo demo) => demo.title).toList();
fail('Unrecognized demo names in _kUnsynchronizedDemos: $_kUnsynchronizedDemos'); if (!new Set<String>.from(allDemoTitles).containsAll(_kUnsynchronizedDemoTitles))
if (!new Set<String>.from(_kAllDemos).containsAll(_kSkippedDemos)) fail('Unrecognized demo titles in _kUnsynchronizedDemosTitles: $_kUnsynchronizedDemoTitles');
fail('Unrecognized demo names in _kSkippedDemos: $_kSkippedDemos'); if (!new Set<String>.from(allDemoTitles).containsAll(_kSkippedDemoTitles))
fail('Unrecognized demo names in _kSkippedDemoTitles: $_kSkippedDemoTitles');
runApp(const GalleryApp()); runApp(const GalleryApp());
final _LiveWidgetController controller = new _LiveWidgetController(); final _LiveWidgetController controller = new _LiveWidgetController();
for (String demo in _kAllDemos) { for (GalleryDemoCategory category in kAllGalleryDemoCategories) {
print('Testing "$demo" demo'); await controller.tap(find.text(category.name));
final Finder menuItem = find.text(demo); for (GalleryDemo demo in kGalleryCategoryToDemos[category]) {
await controller.scrollIntoView(menuItem, alignment: 0.5); final Finder demoItem = find.text(demo.title);
await controller.scrollIntoView(demoItem, alignment: 0.5);
if (_kSkippedDemos.contains(demo)) {
print('> skipped $demo'); if (_kSkippedDemoTitles.contains(demo.title)) {
continue; print('> skipped $demo');
} continue;
}
for (int i = 0; i < 2; i += 1) {
await controller.tap(menuItem); // Launch the demo for (int i = 0; i < 2; i += 1) {
controller.frameSync = !_kUnsynchronizedDemos.contains(demo); await controller.tap(demoItem); // Launch the demo
await controller.tap(find.byTooltip('Back')); controller.frameSync = !_kUnsynchronizedDemoTitles.contains(demo.title);
controller.frameSync = true; await controller.tap(find.byTooltip('Back'));
controller.frameSync = true;
}
print('Success');
} }
print('Success'); await controller.tap(find.byTooltip('Back'));
} }
_kTestChannel.invokeMethod('success'); _kTestChannel.invokeMethod('success');
......
...@@ -17,7 +17,7 @@ void main() { ...@@ -17,7 +17,7 @@ void main() {
// The bug only manifests itself when the screen's orientation is portrait // The bug only manifests itself when the screen's orientation is portrait
const Center( const Center(
child: const SizedBox( child: const SizedBox(
width: 400.0, width: 450.0,
height: 800.0, height: 800.0,
child: const GalleryApp() child: const GalleryApp()
) )
...@@ -26,29 +26,32 @@ void main() { ...@@ -26,29 +26,32 @@ void main() {
await tester.pump(); // see https://github.com/flutter/flutter/issues/1865 await tester.pump(); // see https://github.com/flutter/flutter/issues/1865
await tester.pump(); // triggers a frame await tester.pump(); // triggers a frame
await tester.tap(find.text('Vignettes'));
await tester.pumpAndSettle();
await tester.tap(find.text('Pesto')); await tester.tap(find.text('Pesto'));
await tester.pump(); // Launch pesto await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1)); // transition is complete
await tester.tap(find.text('Pesto Bruschetta')); await tester.tap(find.text('Pesto Bruschetta'));
await tester.pump(); // Launch the recipe page await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1)); // transition is complete
await tester.drag(find.text('Pesto Bruschetta'), const Offset(0.0, -300.0)); 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); Navigator.pop(find.byType(Scaffold).evaluate().single);
await tester.pump(); await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1)); // transition is complete
}); });
testWidgets('Pesto can be scrolled all the way down', (WidgetTester tester) async { testWidgets('Pesto can be scrolled all the way down', (WidgetTester tester) async {
await tester.pumpWidget(const GalleryApp()); await tester.pumpWidget(const GalleryApp());
await tester.pump(); // see https://github.com/flutter/flutter/issues/1865 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.tap(find.text('Pesto'));
await tester.pump(); // Launch pesto await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1)); // transition is complete
await tester.fling(find.text('Pesto Bruschetta'), const Offset(0.0, -200.0), 10000.0); await tester.fling(find.text('Pesto Bruschetta'), const Offset(0.0, -200.0), 10000.0);
await tester.pumpAndSettle(); // start and finish fling await tester.pumpAndSettle(); // start and finish fling
......
...@@ -16,37 +16,29 @@ void main() { ...@@ -16,37 +16,29 @@ void main() {
await tester.pump(); // see https://github.com/flutter/flutter/issues/1865 await tester.pump(); // see https://github.com/flutter/flutter/issues/1865
await tester.pump(); // triggers a frame await tester.pump(); // triggers a frame
final Finder finder = find.byWidgetPredicate((Widget widget) { final Finder showOptionsPageButton = find.byTooltip('Show options page');
return widget is Tooltip && widget.message == 'Open navigation menu';
});
expect(finder, findsOneWidget);
// Open drawer // Show the options page
await tester.tap(finder); await tester.tap(showOptionsPageButton);
await tester.pump(); // start animation await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1)); // end animation
// Change theme // Switch to the dark theme: the first switch control
await tester.tap(find.text('Dark')); await tester.tap(find.byType(Switch).first);
await tester.pump(); // start animation await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1)); // end animation
// Close drawer // Close the options page
await tester.tap(find.byType(DrawerController)); expect(showOptionsPageButton, findsOneWidget);
await tester.pump(); // start animation await tester.tap(showOptionsPageButton);
await tester.pump(const Duration(seconds: 1)); // end animation await tester.pumpAndSettle();
// Open Demos // Show the vignettes
await tester.tap(find.text('Vignettes')); await tester.tap(find.text('Vignettes'));
await tester.pump(); // start animation await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1)); // end animation
// Open Flexible space toolbar // Show the Contact profile demo and scroll it upwards
await tester.tap(find.text('Contact profile')); await tester.tap(find.text('Contact profile'));
await tester.pump(); // start animation await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1)); // end animation
// Scroll it up
await tester.drag(find.text('(650) 555-1234'), const Offset(0.0, -50.0)); await tester.drag(find.text('(650) 555-1234'), const Offset(0.0, -50.0));
await tester.pump(const Duration(milliseconds: 200)); await tester.pump(const Duration(milliseconds: 200));
await tester.drag(find.text('(650) 555-1234'), const Offset(0.0, -50.0)); await tester.drag(find.text('(650) 555-1234'), const Offset(0.0, -50.0));
......
...@@ -2,31 +2,21 @@ ...@@ -2,31 +2,21 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:collection' show LinkedHashSet;
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.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; 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<String> demoCategories = new LinkedHashSet<String>.from( // All of the classes printed by debugDump etc, must have toString()
kAllGalleryItems.map<String>((GalleryItem item) => item.category) // values approved by verityToStringOutput().
).toList(); int toStringErrors = 0;
final List<String> 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;
void reportToStringError(String name, String route, int lineNumber, List<String> lines, String message) { void reportToStringError(String name, String route, int lineNumber, List<String> lines, String message) {
// If you're on line 12, then it has index 11. // If you're on line 12, then it has index 11.
...@@ -36,7 +26,7 @@ void reportToStringError(String name, String route, int lineNumber, List<String> ...@@ -36,7 +26,7 @@ void reportToStringError(String name, String route, int lineNumber, List<String>
final int firstLine = math.max(0, lineNumber - margin); final int firstLine = math.max(0, lineNumber - margin);
final int lastLine = math.min(lines.length, 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 ")}'); 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) { void verifyToStringOutput(String name, String route, String testString) {
...@@ -56,22 +46,16 @@ 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 Future<Null> smokeDemo(WidgetTester tester, GalleryDemo demo) async {
// we're starting on the home route and that the submenu that contains print(demo);
// the item for a demo that pushes route 'routeName' is already open.
Future<Null> 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);
// Don't use pumpUntilNoTransientCallbacks in this function, because some of // Don't use pumpUntilNoTransientCallbacks in this function, because some of
// the smoketests have infinitely-running animations (e.g. the progress // the smoketests have infinitely-running animations (e.g. the progress
// indicators demo). // indicators demo).
await tester.tap(menuItem); await tester.tap(find.text(demo.title));
await tester.pump(); // Launch the demo. await tester.pump(); // Launch the demo.
await tester.pump(const Duration(milliseconds: 400)); // Wait until the demo has opened. 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. // Leave the demo on the screen briefly for manual testing.
await tester.pump(const Duration(milliseconds: 400)); await tester.pump(const Duration(milliseconds: 400));
...@@ -85,6 +69,7 @@ Future<Null> smokeDemo(WidgetTester tester, String routeName) async { ...@@ -85,6 +69,7 @@ Future<Null> smokeDemo(WidgetTester tester, String routeName) async {
await tester.pump(const Duration(milliseconds: 400)); await tester.pump(const Duration(milliseconds: 400));
// Verify that the dumps are pretty. // Verify that the dumps are pretty.
final String routeName = demo.routeName;
verifyToStringOutput('debugDumpApp', routeName, WidgetsBinding.instance.renderViewElement.toStringDeep()); verifyToStringOutput('debugDumpApp', routeName, WidgetsBinding.instance.renderViewElement.toStringDeep());
verifyToStringOutput('debugDumpRenderTree', routeName, RendererBinding.instance?.renderView?.toStringDeep()); verifyToStringOutput('debugDumpRenderTree', routeName, RendererBinding.instance?.renderView?.toStringDeep());
verifyToStringOutput('debugDumpLayerTree', routeName, RendererBinding.instance?.renderView?.debugLayer?.toStringDeep()); verifyToStringOutput('debugDumpLayerTree', routeName, RendererBinding.instance?.renderView?.debugLayer?.toStringDeep());
...@@ -108,74 +93,85 @@ Future<Null> smokeDemo(WidgetTester tester, String routeName) async { ...@@ -108,74 +93,85 @@ Future<Null> smokeDemo(WidgetTester tester, String routeName) async {
await tester.pump(); // Start the pop "back" operation. await tester.pump(); // Start the pop "back" operation.
await tester.pump(); // Complete the willPop() Future. await tester.pump(); // Complete the willPop() Future.
await tester.pump(const Duration(milliseconds: 400)); // Wait until it has finished. await tester.pump(const Duration(milliseconds: 400)); // Wait until it has finished.
return null;
} }
Future<Null> runSmokeTest(WidgetTester tester) async { Future<Null> smokeOptionsPage(WidgetTester tester) async {
bool hasFeedback = false; final Finder showOptionsPageButton = find.byTooltip('Show options page');
void mockOnSendFeedback() {
hasFeedback = true;
}
await tester.pumpWidget(new GalleryApp(onSendFeedback: mockOnSendFeedback)); // Show the options page
await tester.pump(); // see https://github.com/flutter/flutter/issues/1865 await tester.tap(showOptionsPageButton);
await tester.pump(); // triggers a frame await tester.pumpAndSettle();
expect(find.text(kCaption), findsOneWidget); // Switch to the dark theme: first switch control
await tester.tap(find.byType(Switch).first);
await tester.pumpAndSettle();
for (String routeName in routeNames) { // Switch back to the light theme: first switch control again
final Finder finder = findGalleryItemByRouteName(tester, routeName); await tester.tap(find.byType(Switch).first);
Scrollable.ensureVisible(tester.element(finder), alignment: 0.5); await tester.pumpAndSettle();
await tester.pumpAndSettle();
await smokeDemo(tester, routeName);
tester.binding.debugAssertNoTransientCallbacks('A transient callback was still active after leaving route $routeName');
}
expect(errors, 0);
final Finder navigationMenuButton = find.byTooltip('Open navigation menu'); // Popup the text size menu: first menu button, choose 'Small'
expect(navigationMenuButton, findsOneWidget); await tester.tap(find.byIcon(Icons.arrow_drop_down).first);
await tester.tap(navigationMenuButton); await tester.pumpAndSettle();
await tester.pump(); // Start opening drawer. await tester.tap(find.text('Small'));
await tester.pump(const Duration(seconds: 1)); // Wait until it's really opened. await tester.pumpAndSettle();
// Switch theme. // Popup the text size menu: first menu button, choose 'Normal'
await tester.tap(find.text('Dark')); await tester.tap(find.byIcon(Icons.arrow_drop_down).first);
await tester.pump(); await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1)); // Wait until it's changed. await tester.tap(find.text('Normal'));
await tester.pumpAndSettle();
// Switch theme. // Scroll the 'Send feedback' item into view
await tester.tap(find.text('Light')); await tester.drag(find.text('Normal'), const Offset(0.0, -1000.0));
await tester.pump(); await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 1)); // Wait until it's changed. await tester.tap(find.text('Send feedback'));
await tester.pumpAndSettle();
// Switch font scale. // Close the options page
await tester.tap(find.text('Small')); expect(showOptionsPageButton, findsOneWidget);
await tester.pump(); await tester.tap(showOptionsPageButton);
await tester.pump(const Duration(seconds: 1)); // Wait until it's changed. await tester.pumpAndSettle();
// 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. Future<Null> smokeGallery(WidgetTester tester) async {
await tester.drag(find.text('Small'), const Offset(0.0, -1000.0)); bool sendFeedbackButtonPressed = false;
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // Wait until it's changed.
// Send feedback. await tester.pumpWidget(
expect(hasFeedback, false); new GalleryApp(
await tester.tap(find.text('Send feedback')); onSendFeedback: () {
await tester.pump(); sendFeedbackButtonPressed = true; // see smokeOptionsPage()
expect(hasFeedback, true); },
),
);
await tester.pump(); // see https://github.com/flutter/flutter/issues/1865
await tester.pump(); // triggers a frame
expect(find.text(kGalleryTitle), findsOneWidget);
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();
}
expect(toStringErrors, 0);
await smokeOptionsPage(tester);
expect(sendFeedbackButtonPressed, true);
} }
void main() { 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 { testWidgets('Flutter Gallery app smoke test with semantics', (WidgetTester tester) async {
RendererBinding.instance.setSemanticsEnabled(true); RendererBinding.instance.setSemanticsEnabled(true);
await runSmokeTest(tester); await smokeGallery(tester);
RendererBinding.instance.setSemanticsEnabled(false); RendererBinding.instance.setSemanticsEnabled(false);
}); });
} }
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_gallery/gallery/app.dart'; import 'package:flutter_gallery/gallery/app.dart' show GalleryApp;
Future<String> mockUpdateUrlFetcher() { Future<String> mockUpdateUrlFetcher() {
// A real implementation would connect to the network to retrieve this value // A real implementation would connect to the network to retrieve this value
...@@ -26,8 +26,8 @@ void main() { ...@@ -26,8 +26,8 @@ void main() {
await tester.tap(find.text('NO THANKS')); await tester.tap(find.text('NO THANKS'));
await tester.pump(); await tester.pump();
await tester.tap(find.text('Shrine')); await tester.tap(find.text('Vignettes'));
await tester.pump(); // Launch shrine await tester.pump(); // Launch
await tester.pump(const Duration(seconds: 1)); // transition is complete await tester.pump(const Duration(seconds: 1)); // transition is complete
final Finder backButton = find.byTooltip('Back'); final Finder backButton = find.byTooltip('Back');
......
...@@ -14,14 +14,17 @@ void main() { ...@@ -14,14 +14,17 @@ void main() {
}); });
test('navigation', () async { test('navigation', () async {
final SerializableFinder menuItem = find.text('Text fields'); await driver.tap(find.text('Material'));
await driver.scrollUntilVisible(find.byType('CustomScrollView'), menuItem,
final SerializableFinder demoList = find.byValueKey('GalleryDemoList');
final SerializableFinder demoItem = find.text('Text fields');
await driver.scrollUntilVisible(demoList, demoItem,
dyScroll: -300.0, dyScroll: -300.0,
alignment: 0.5, alignment: 0.5,
timeout: const Duration(minutes: 1), timeout: const Duration(minutes: 1),
); );
for (int i = 0; i < 15; i++) { for (int i = 0; i < 15; i++) {
await driver.tap(menuItem); await driver.tap(demoItem);
await driver.tap(find.byTooltip('Back')); await driver.tap(find.byTooltip('Back'));
} }
}); });
......
...@@ -22,24 +22,21 @@ void main() { ...@@ -22,24 +22,21 @@ void main() {
test('measure', () async { test('measure', () async {
final Timeline timeline = await driver.traceAction(() async { final Timeline timeline = await driver.traceAction(() async {
final SerializableFinder home = find.byValueKey('Gallery List'); await driver.tap(find.text('Material'));
expect(home, isNotNull);
await driver.tap(find.text('Vignettes')); final SerializableFinder demoList = find.byValueKey('GalleryDemoList');
await driver.tap(find.text('Components'));
await driver.tap(find.text('Style'));
// TODO(eseidel): These are very artificial scrolls, we should use better // TODO(eseidel): These are very artificial scrolls, we should use better
// https://github.com/flutter/flutter/issues/3316 // https://github.com/flutter/flutter/issues/3316
// Scroll down // Scroll down
for (int i = 0; i < 5; i++) { 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<Null>.delayed(const Duration(milliseconds: 500)); await new Future<Null>.delayed(const Duration(milliseconds: 500));
} }
// Scroll up // Scroll up
for (int i = 0; i < 5; i++) { 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<Null>.delayed(const Duration(milliseconds: 500)); await new Future<Null>.delayed(const Duration(milliseconds: 500));
} }
}); });
......
...@@ -6,13 +6,13 @@ import 'dart:async'; ...@@ -6,13 +6,13 @@ import 'dart:async';
import 'dart:convert' show JsonEncoder; import 'dart:convert' show JsonEncoder;
import 'package:flutter_driver/driver_extension.dart'; 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; import 'package:flutter_gallery/main.dart' as app;
Future<String> _handleMessages(String message) async { Future<String> _handleMessages(String message) async {
assert(message == 'demoNames'); assert(message == 'demoNames');
return const JsonEncoder.withIndent(' ').convert( return const JsonEncoder.withIndent(' ').convert(
kAllGalleryItems.map((GalleryItem item) => item.title).toList(), kAllGalleryDemos.map((GalleryDemo demo) => '${demo.title}@${demo.category.name}').toList(),
); );
} }
......
...@@ -45,8 +45,7 @@ const List<String> kUnsynchronizedDemos = const <String>[ ...@@ -45,8 +45,7 @@ const List<String> kUnsynchronizedDemos = const <String>[
'Video', 'Video',
]; ];
// All of the gallery demo titles in the order they appear on the // All of the gallery demos, identified as "title@category".
// gallery home page.
// //
// These names are reported by the test app, see _handleMessages() // These names are reported by the test app, see _handleMessages()
// in transitions_perf.dart. // in transitions_perf.dart.
...@@ -121,20 +120,26 @@ Future<Null> saveDurationsHistogram(List<Map<String, dynamic>> events, String ou ...@@ -121,20 +120,26 @@ Future<Null> saveDurationsHistogram(List<Map<String, dynamic>> events, String ou
/// Scrolls each demo menu item into view, launches it, then returns to the /// Scrolls each demo menu item into view, launches it, then returns to the
/// home screen twice. /// home screen twice.
Future<Null> runDemos(List<String> demos, FlutterDriver driver) async { Future<Null> runDemos(List<String> demos, FlutterDriver driver) async {
final SerializableFinder demoList = find.byValueKey('GalleryDemoList');
String currentDemoCategory;
for (String demo in demos) { for (String demo in demos) {
print('Testing "$demo" demo'); final String demoAtCategory = _allDemos.firstWhere((String s) => s.startsWith(demo));
final SerializableFinder menuItem = find.text(demo); final String demoCategory = demoAtCategory.substring(demoAtCategory.indexOf('@') + 1);
await driver.scrollUntilVisible(find.byType('CustomScrollView'), menuItem,
dyScroll: -48.0, if (currentDemoCategory == null) {
alignment: 0.5, await driver.tap(find.text(demoCategory));
); } else if (currentDemoCategory != demoCategory) {
await driver.tap(find.byTooltip('Back'));
await driver.tap(find.text(demoCategory));
}
currentDemoCategory = demoCategory;
for (int i = 0; i < 2; i += 1) { final SerializableFinder demoItem = find.text(demo);
await driver.tap(menuItem); // Launch the demo await driver.scrollUntilVisible(demoList, demoItem, dyScroll: -48.0, alignment: 0.5);
// This demo's back button isn't initially visible. for (int i = 0; i < 2; i += 1) {
if (demo == 'Backdrop') await driver.tap(demoItem); // Launch the demo
await driver.tap(find.byTooltip('Tap to dismiss'));
if (kUnsynchronizedDemos.contains(demo)) { if (kUnsynchronizedDemos.contains(demo)) {
await driver.runUnsynchronized<Future<Null>>(() async { await driver.runUnsynchronized<Future<Null>>(() async {
...@@ -144,8 +149,12 @@ Future<Null> runDemos(List<String> demos, FlutterDriver driver) async { ...@@ -144,8 +149,12 @@ Future<Null> runDemos(List<String> demos, FlutterDriver driver) async {
await driver.tap(find.byTooltip('Back')); await driver.tap(find.byTooltip('Back'));
} }
} }
print('Success'); print('Success');
} }
// Return to the home screen
await driver.tap(find.byTooltip('Back'));
} }
void main([List<String> args = const <String>[]]) { void main([List<String> args = const <String>[]]) {
...@@ -171,6 +180,7 @@ void main([List<String> args = const <String>[]]) { ...@@ -171,6 +180,7 @@ void main([List<String> args = const <String>[]]) {
}); });
test('all demos', () async { test('all demos', () async {
// Collect timeline data for just a limited set of demos to avoid OOMs. // Collect timeline data for just a limited set of demos to avoid OOMs.
final Timeline timeline = await driver.traceAction( final Timeline timeline = await driver.traceAction(
() async { () async {
...@@ -190,14 +200,9 @@ void main([List<String> args = const <String>[]]) { ...@@ -190,14 +200,9 @@ void main([List<String> args = const <String>[]]) {
final String histogramPath = path.join(testOutputsDirectory, 'transition_durations.timeline.json'); final String histogramPath = path.join(testOutputsDirectory, 'transition_durations.timeline.json');
await saveDurationsHistogram(timeline.json['traceEvents'], histogramPath); 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. // Execute the remaining tests.
final Set<String> unprofiledDemos = new Set<String>.from(_allDemos)..removeAll(kProfiledDemos); final List<String> allDemoNames = _allDemos.map((String s) => s.substring(0, s.indexOf('@')));
final Set<String> unprofiledDemos = new Set<String>.from(allDemoNames)..removeAll(kProfiledDemos);
await runDemos(unprofiledDemos.toList(), driver); await runDemos(unprofiledDemos.toList(), driver);
}, timeout: const Timeout(const Duration(minutes: 5))); }, timeout: const Timeout(const Duration(minutes: 5)));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment