Commit 8cc53120 authored by Steve Messick's avatar Steve Messick

Merge remote-tracking branch 'upstream/master'

parents cea21fb7 f04fd43f
...@@ -53,7 +53,7 @@ The typical cycle for editing a recipe is: ...@@ -53,7 +53,7 @@ The typical cycle for editing a recipe is:
1. Make your edits. 1. Make your edits.
2. Run `build/scripts/slave/recipes.py simulation_test train flutter` to update expected files (remove the flutter if you need to do a global update). 2. Run `build/scripts/slave/recipes.py simulation_test train flutter` to update expected files (remove the flutter if you need to do a global update).
3. Run `build/scripts/slave/recipes.py run flutter/flutter` (or flutter/engine) if something was strange during training and you need to run it locally. 3. Run `build/scripts/tools/run_recipe.py flutter/flutter` (or flutter/engine) if something was strange during training and you need to run it locally.
4. Upload the patch (`git commit`, `git cl upload`) and send it to someone in the `recipes/flutter/OWNERS` file for review. 4. Upload the patch (`git commit`, `git cl upload`) and send it to someone in the `recipes/flutter/OWNERS` file for review.
## Editing the client.flutter buildbot master ## Editing the client.flutter buildbot master
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
/// The Flutter animation system. /// The Flutter animation system.
/// ///
/// See [https://flutter.io/animations/] for an overview. /// See <https://flutter.io/animations/> for an overview.
/// ///
/// This library depends only on core Dart libraries and the `newton` package. /// This library depends only on core Dart libraries and the `newton` package.
library animation; library animation;
......
// Copyright 2015 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.
/// Manages connections with embedder-provided services.
library shell;
import 'dart:ui' as ui;
import 'package:mojo/application.dart';
import 'package:mojo/bindings.dart' as bindings;
import 'package:mojo/core.dart' as core;
import 'package:mojo/mojo/service_provider.mojom.dart' as mojom;
import 'package:mojo/mojo/shell.mojom.dart' as mojom;
/// A replacement for shell.connectToService. Implementations should return true
/// if they handled the request, or false if the request should fall through
/// to the default requestService.
typedef bool OverrideConnectToService(String url, Object proxy);
/// Manages connections with embedder-provided services.
class MojoShell {
MojoShell() {
assert(_instance == null);
_instance = this;
}
/// The unique instance of this class.
static MojoShell get instance => _instance;
static MojoShell _instance;
static mojom.ShellProxy _initShellProxy() {
core.MojoHandle shellHandle = new core.MojoHandle(ui.takeShellProxyHandle());
if (!shellHandle.isValid)
return null;
return new mojom.ShellProxy.fromHandle(shellHandle);
}
final mojom.Shell _shell = _initShellProxy()?.ptr;
static ApplicationConnection _initEmbedderConnection() {
core.MojoHandle servicesHandle = new core.MojoHandle(ui.takeServicesProvidedByEmbedder());
core.MojoHandle exposedServicesHandle = new core.MojoHandle(ui.takeServicesProvidedToEmbedder());
if (!servicesHandle.isValid || !exposedServicesHandle.isValid)
return null;
mojom.ServiceProviderProxy services = new mojom.ServiceProviderProxy.fromHandle(servicesHandle);
mojom.ServiceProviderStub exposedServices = new mojom.ServiceProviderStub.fromHandle(exposedServicesHandle);
return new ApplicationConnection(exposedServices, services);
}
final ApplicationConnection _embedderConnection = _initEmbedderConnection();
/// Attempts to connect to an application via the Mojo shell.
ApplicationConnection connectToApplication(String url) {
if (_shell == null)
return null;
mojom.ServiceProviderProxy services = new mojom.ServiceProviderProxy.unbound();
mojom.ServiceProviderStub exposedServices = new mojom.ServiceProviderStub.unbound();
_shell.connectToApplication(url, services, exposedServices);
return new ApplicationConnection(exposedServices, services);
}
/// Set this to intercept calls to [connectToService()] and supply an
/// alternative implementation of a service (for example, a mock for testing).
OverrideConnectToService overrideConnectToService;
/// Attempts to connect to a service implementing the interface for the given proxy.
/// If an application URL is specified, the service will be requested from that application.
/// Otherwise, it will be requested from the embedder (the Flutter engine).
void connectToService(String url, bindings.ProxyBase proxy) {
if (overrideConnectToService != null && overrideConnectToService(url, proxy))
return;
if (url == null || _shell == null) {
// If the application URL is null, it means the service to connect
// to is one provided by the embedder.
// If the applircation URL isn't null but there's no shell, then
// ask the embedder in case it provides it. (For example, if you're
// running on Android without the Mojo shell, then you can obtain
// the media service from the embedder directly, instead of having
// to ask the media application for it.)
// This makes it easier to write an application that works both
// with and without a Mojo environment.
_embedderConnection?.requestService(proxy);
return;
}
mojom.ServiceProviderProxy services = new mojom.ServiceProviderProxy.unbound();
_shell.connectToApplication(url, services, null);
core.MojoMessagePipe pipe = new core.MojoMessagePipe();
proxy.impl.bind(pipe.endpoints[0]);
services.ptr.connectToService(proxy.serviceName, pipe.endpoints[1]);
services.close();
}
/// Registers a service to expose to the embedder.
void provideService(String interfaceName, ServiceFactory factory) {
_embedderConnection?.provideService(interfaceName, factory);
}
}
/// The singleton object that manages connections with embedder-provided services.
MojoShell get shell => MojoShell.instance;
...@@ -2,13 +2,10 @@ ...@@ -2,13 +2,10 @@
// 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:async';
import 'dart:ui' as ui show WindowPadding, window;
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'page.dart'; import 'page.dart';
import 'theme.dart'; import 'theme.dart';
...@@ -21,204 +18,75 @@ const TextStyle _errorTextStyle = const TextStyle( ...@@ -21,204 +18,75 @@ const TextStyle _errorTextStyle = const TextStyle(
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
textAlign: TextAlign.right, textAlign: TextAlign.right,
decoration: TextDecoration.underline, decoration: TextDecoration.underline,
decorationColor: const Color(0xFFFF00), decorationColor: const Color(0xFFFFFF00),
decorationStyle: TextDecorationStyle.double decorationStyle: TextDecorationStyle.double
); );
AssetBundle _initDefaultBundle() {
if (rootBundle != null)
return rootBundle;
return new NetworkAssetBundle(Uri.base);
}
final AssetBundle _defaultBundle = _initDefaultBundle();
class RouteArguments {
const RouteArguments({ this.context });
final BuildContext context;
}
typedef Widget RouteBuilder(RouteArguments args);
typedef Future<LocaleQueryData> LocaleChangedCallback(Locale locale);
class MaterialApp extends StatefulComponent { class MaterialApp extends WidgetsApp {
MaterialApp({ MaterialApp({
Key key, Key key,
this.title, String title,
this.theme, ThemeData theme,
this.routes: const <String, RouteBuilder>{}, Map<String, RouteBuilder> routes: const <String, RouteBuilder>{},
this.onGenerateRoute, RouteFactory onGenerateRoute,
this.onLocaleChanged, LocaleChangedCallback onLocaleChanged,
this.debugShowMaterialGrid: false, this.debugShowMaterialGrid: false,
this.showPerformanceOverlay: false, bool showPerformanceOverlay: false,
this.showSemanticsDebugger: false, bool showSemanticsDebugger: false,
this.debugShowCheckedModeBanner: true bool debugShowCheckedModeBanner: true
}) : super(key: key) { }) : theme = theme,
assert(routes != null); super(
assert(routes.containsKey(Navigator.defaultRouteName) || onGenerateRoute != null); key: key,
title: title,
textStyle: _errorTextStyle,
color: theme?.primaryColor ?? Colors.blue[500], // blue[500] is the primary color of the default theme
routes: routes,
onGenerateRoute: (RouteSettings settings) {
RouteBuilder builder = routes[settings.name];
if (builder != null) {
return new MaterialPageRoute(
builder: (BuildContext context) {
return builder(new RouteArguments(context: context));
},
settings: settings
);
}
if (onGenerateRoute != null)
return onGenerateRoute(settings);
return null;
},
onLocaleChanged: onLocaleChanged,
showPerformanceOverlay: showPerformanceOverlay,
showSemanticsDebugger: showSemanticsDebugger,
debugShowCheckedModeBanner: debugShowCheckedModeBanner
) {
assert(debugShowMaterialGrid != null); assert(debugShowMaterialGrid != null);
assert(showPerformanceOverlay != null);
assert(showSemanticsDebugger != null);
} }
/// A one-line description of this app for use in the window manager.
final String title;
/// The colors to use for the application's widgets. /// The colors to use for the application's widgets.
final ThemeData theme; final ThemeData theme;
/// The default table of routes for the application. When the
/// [Navigator] is given a named route, the name will be looked up
/// in this table first. If the name is not available, then
/// [onGenerateRoute] will be called instead.
final Map<String, RouteBuilder> routes;
/// The route generator callback used when the app is navigated to a
/// named route but the name is not in the [routes] table.
final RouteFactory onGenerateRoute;
/// Callback that is invoked when the operating system changes the
/// current locale.
final LocaleChangedCallback onLocaleChanged;
/// Turns on a [GridPaper] overlay that paints a baseline grid /// Turns on a [GridPaper] overlay that paints a baseline grid
/// Material apps: /// Material apps:
/// https://www.google.com/design/spec/layout/metrics-keylines.html /// https://www.google.com/design/spec/layout/metrics-keylines.html
/// Only available in checked mode. /// Only available in checked mode.
final bool debugShowMaterialGrid; final bool debugShowMaterialGrid;
/// Turns on a performance overlay.
/// https://flutter.io/debugging/#performanceoverlay
final bool showPerformanceOverlay;
/// Turns on an overlay that shows the accessibility information
/// reported by the framework.
final bool showSemanticsDebugger;
/// Turns on a "SLOW MODE" little banner in checked mode to indicate
/// that the app is in checked mode. This is on by default (in
/// checked mode), to turn it off, set the constructor argument to
/// false. In release mode this has no effect.
///
/// To get this banner in your application if you're not using
/// MaterialApp, include a [CheckedModeBanner] widget in your app.
///
/// This banner is intended to avoid people complaining that your
/// app is slow when it's in checked mode. In checked mode, Flutter
/// enables a large number of expensive diagnostics to aid in
/// development, and so performance in checked mode is not
/// representative of what will happen in release mode.
final bool debugShowCheckedModeBanner;
_MaterialAppState createState() => new _MaterialAppState(); _MaterialAppState createState() => new _MaterialAppState();
} }
EdgeDims _getPadding(ui.WindowPadding padding) { class _MaterialAppState extends WidgetsAppState<MaterialApp> {
return new EdgeDims.TRBL(padding.top, padding.right, padding.bottom, padding.left);
}
class _MaterialAppState extends State<MaterialApp> implements BindingObserver {
GlobalObjectKey _navigator;
LocaleQueryData _localeData;
void initState() {
super.initState();
_navigator = new GlobalObjectKey(this);
didChangeLocale(ui.window.locale);
WidgetFlutterBinding.instance.addObserver(this);
}
void dispose() {
WidgetFlutterBinding.instance.removeObserver(this);
super.dispose();
}
bool didPopRoute() {
assert(mounted);
NavigatorState navigator = _navigator.currentState;
assert(navigator != null);
bool result = false;
navigator.openTransaction((NavigatorTransaction transaction) {
result = transaction.pop();
});
return result;
}
void didChangeMetrics() {
setState(() {
// The properties of ui.window have changed. We use them in our build
// function, so we need setState(), but we don't cache anything locally.
});
}
void didChangeLocale(Locale locale) {
if (config.onLocaleChanged != null) {
config.onLocaleChanged(locale).then((LocaleQueryData data) {
if (mounted)
setState(() { _localeData = data; });
});
}
}
void didChangeAppLifecycleState(AppLifecycleState state) { }
final HeroController _heroController = new HeroController(); final HeroController _heroController = new HeroController();
NavigatorObserver get navigatorObserver => _heroController;
Route _generateRoute(RouteSettings settings) {
RouteBuilder builder = config.routes[settings.name];
if (builder != null) {
return new MaterialPageRoute(
builder: (BuildContext context) {
return builder(new RouteArguments(context: context));
},
settings: settings
);
}
if (config.onGenerateRoute != null)
return config.onGenerateRoute(settings);
return null;
}
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (config.onLocaleChanged != null && _localeData == null) {
// If the app expects a locale but we don't yet know the locale, then
// don't build the widgets now.
return new Container();
}
ThemeData theme = config.theme ?? new ThemeData.fallback(); ThemeData theme = config.theme ?? new ThemeData.fallback();
Widget result = new MediaQuery( Widget result = new AnimatedTheme(
data: new MediaQueryData( data: theme,
size: ui.window.size, duration: kThemeAnimationDuration,
devicePixelRatio: ui.window.devicePixelRatio, child: super.build(context)
padding: _getPadding(ui.window.padding)
),
child: new LocaleQuery(
data: _localeData,
child: new AnimatedTheme(
data: theme,
duration: kThemeAnimationDuration,
child: new DefaultTextStyle(
style: _errorTextStyle,
child: new AssetVendor(
bundle: _defaultBundle,
devicePixelRatio: ui.window.devicePixelRatio,
child: new Title(
title: config.title,
color: theme.primaryColor,
child: new Navigator(
key: _navigator,
initialRoute: ui.window.defaultRouteName,
onGenerateRoute: _generateRoute,
observer: _heroController
)
)
)
)
)
)
); );
assert(() { assert(() {
if (config.debugShowMaterialGrid) { if (config.debugShowMaterialGrid) {
...@@ -232,27 +100,6 @@ class _MaterialAppState extends State<MaterialApp> implements BindingObserver { ...@@ -232,27 +100,6 @@ class _MaterialAppState extends State<MaterialApp> implements BindingObserver {
} }
return true; return true;
}); });
if (config.showPerformanceOverlay) {
result = new Stack(
children: <Widget>[
result,
new Positioned(bottom: 0.0, left: 0.0, right: 0.0, child: new PerformanceOverlay.allEnabled()),
]
);
}
if (config.showSemanticsDebugger) {
result = new SemanticsDebugger(
child: result
);
}
assert(() {
if (config.debugShowCheckedModeBanner) {
result = new CheckedModeBanner(
child: result
);
}
return true;
});
return result; return result;
} }
......
...@@ -80,7 +80,7 @@ class _BottomSheetState extends State<BottomSheet> { ...@@ -80,7 +80,7 @@ class _BottomSheetState extends State<BottomSheet> {
onVerticalDragUpdate: _handleDragUpdate, onVerticalDragUpdate: _handleDragUpdate,
onVerticalDragEnd: _handleDragEnd, onVerticalDragEnd: _handleDragEnd,
child: new Material( child: new Material(
key: _childKey, key: _childKey,
child: config.builder(context) child: config.builder(context)
) )
); );
......
...@@ -10,6 +10,7 @@ import 'package:intl/date_symbols.dart'; ...@@ -10,6 +10,7 @@ import 'package:intl/date_symbols.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'colors.dart'; import 'colors.dart';
import 'debug.dart';
import 'ink_well.dart'; import 'ink_well.dart';
import 'theme.dart'; import 'theme.dart';
import 'typography.dart'; import 'typography.dart';
...@@ -403,6 +404,7 @@ class _YearPickerState extends State<YearPicker> { ...@@ -403,6 +404,7 @@ class _YearPickerState extends State<YearPicker> {
} }
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
return new ScrollableLazyList( return new ScrollableLazyList(
itemExtent: _itemExtent, itemExtent: _itemExtent,
itemCount: config.lastDate.year - config.firstDate.year + 1, itemCount: config.lastDate.year - config.firstDate.year + 1,
......
...@@ -12,8 +12,17 @@ bool debugCheckHasMaterial(BuildContext context) { ...@@ -12,8 +12,17 @@ bool debugCheckHasMaterial(BuildContext context) {
if (context.widget is! Material && context.ancestorWidgetOfExactType(Material) == null) { if (context.widget is! Material && context.ancestorWidgetOfExactType(Material) == null) {
Element element = context; Element element = context;
throw new WidgetError( throw new WidgetError(
'Missing Material widget.', 'No Material widget found.\n'
'${context.widget} needs to be placed inside a Material widget. Ownership chain:\n${element.debugGetOwnershipChain(10)}' '${context.widget.runtimeType} widgets require a Material widget ancestor.\n'
'In material design, most widgets are conceptually "printed" on a sheet of material. In Flutter\'s material library, '
'that material is represented by the Material widget. It is the Material widget that renders ink splashes, for instance. '
'Because of this, many material library widgets require that there be a Material widget in the tree above them.\n'
'To introduce a Material widget, you can either directly include one, or use a widget that contains Material itself, '
'such as a Card, Dialog, Drawer, or Scaffold.\n'
'The specific widget that could not find a Material ancestor was:\n'
' ${context.widget}'
'The ownership chain for the affected widget is:\n'
' ${element.debugGetOwnershipChain(10)}'
); );
} }
return true; return true;
...@@ -27,8 +36,12 @@ bool debugCheckHasScaffold(BuildContext context) { ...@@ -27,8 +36,12 @@ bool debugCheckHasScaffold(BuildContext context) {
if (Scaffold.of(context) == null) { if (Scaffold.of(context) == null) {
Element element = context; Element element = context;
throw new WidgetError( throw new WidgetError(
'Missing Scaffold widget.', 'No Scaffold widget found.\n'
'${context.widget} needs to be placed inside the body of a Scaffold widget. Ownership chain:\n${element.debugGetOwnershipChain(10)}' '${context.widget.runtimeType} widgets require a Scaffold widget ancestor.\n'
'The specific widget that could not find a Scaffold ancestor was:\n'
' ${context.widget}'
'The ownership chain for the affected widget is:\n'
' ${element.debugGetOwnershipChain(10)}'
); );
} }
return true; return true;
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'colors.dart'; import 'colors.dart';
import 'debug.dart';
import 'icon.dart'; import 'icon.dart';
import 'icons.dart'; import 'icons.dart';
import 'ink_well.dart'; import 'ink_well.dart';
...@@ -55,6 +56,7 @@ class DrawerItem extends StatelessComponent { ...@@ -55,6 +56,7 @@ class DrawerItem extends StatelessComponent {
} }
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
ThemeData themeData = Theme.of(context); ThemeData themeData = Theme.of(context);
List<Widget> children = <Widget>[]; List<Widget> children = <Widget>[];
......
...@@ -182,6 +182,7 @@ class _DropDownRoute<T> extends PopupRoute<_DropDownRouteResult<T>> { ...@@ -182,6 +182,7 @@ class _DropDownRoute<T> extends PopupRoute<_DropDownRouteResult<T>> {
ModalPosition getPosition(BuildContext context) { ModalPosition getPosition(BuildContext context) {
RenderBox overlayBox = Overlay.of(context).context.findRenderObject(); RenderBox overlayBox = Overlay.of(context).context.findRenderObject();
assert(overlayBox != null); // can't be null; routes get inserted by Navigator which has its own Overlay
Size overlaySize = overlayBox.size; Size overlaySize = overlayBox.size;
RelativeRect menuRect = new RelativeRect.fromSize(rect, overlaySize); RelativeRect menuRect = new RelativeRect.fromSize(rect, overlaySize);
return new ModalPosition( return new ModalPosition(
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'debug.dart';
import 'icon.dart'; import 'icon.dart';
import 'icons.dart'; import 'icons.dart';
import 'ink_well.dart'; import 'ink_well.dart';
...@@ -53,6 +54,7 @@ class IconButton extends StatelessComponent { ...@@ -53,6 +54,7 @@ class IconButton extends StatelessComponent {
final String tooltip; final String tooltip;
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
Widget result = new Padding( Widget result = new Padding(
padding: const EdgeDims.all(8.0), padding: const EdgeDims.all(8.0),
child: new Icon( child: new Icon(
......
...@@ -12,6 +12,18 @@ import 'debug.dart'; ...@@ -12,6 +12,18 @@ import 'debug.dart';
import 'material.dart'; import 'material.dart';
import 'theme.dart'; import 'theme.dart';
/// An area of a Material that responds to touch. Has a configurable shape and
/// can be configured to clip splashes that extend outside its bounds or not.
///
/// For a variant of this widget that is specialised for rectangular areas that
/// always clip splashes, see [InkWell].
///
/// Must have an ancestor [Material] widget in which to cause ink reactions.
///
/// If a Widget uses this class directly, it should include the following line
/// at the top of its [build] function to call [debugCheckHasMaterial]:
///
/// assert(debugCheckHasMaterial(context));
class InkResponse extends StatefulComponent { class InkResponse extends StatefulComponent {
InkResponse({ InkResponse({
Key key, Key key,
...@@ -156,9 +168,14 @@ class _InkResponseState<T extends InkResponse> extends State<T> { ...@@ -156,9 +168,14 @@ class _InkResponseState<T extends InkResponse> extends State<T> {
} }
/// An area of a Material that responds to touch. /// A rectangular area of a Material that responds to touch.
///
/// Must have an ancestor [Material] widget in which to cause ink reactions.
///
/// If a Widget uses this class directly, it should include the following line
/// at the top of its [build] function to call [debugCheckHasMaterial]:
/// ///
/// Must have an ancestor Material widget in which to cause ink reactions. /// assert(debugCheckHasMaterial(context));
class InkWell extends InkResponse { class InkWell extends InkResponse {
InkWell({ InkWell({
Key key, Key key,
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'debug.dart';
import 'ink_well.dart'; import 'ink_well.dart';
import 'theme.dart'; import 'theme.dart';
...@@ -85,6 +86,7 @@ class ListItem extends StatelessComponent { ...@@ -85,6 +86,7 @@ class ListItem extends StatelessComponent {
} }
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
final bool isTwoLine = !isThreeLine && secondary != null; final bool isTwoLine = !isThreeLine && secondary != null;
final bool isOneLine = !isThreeLine && !isTwoLine; final bool isOneLine = !isThreeLine && !isTwoLine;
double itemHeight; double itemHeight;
......
...@@ -39,9 +39,8 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate { ...@@ -39,9 +39,8 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
final EdgeDims padding; final EdgeDims padding;
void performLayout(Size size, BoxConstraints constraints) { void performLayout(Size size) {
BoxConstraints looseConstraints = new BoxConstraints.loose(size);
BoxConstraints looseConstraints = constraints.loosen();
// This part of the layout has the same effect as putting the toolbar and // This part of the layout has the same effect as putting the toolbar and
// body in a column and making the body flexible. What's different is that // body in a column and making the body flexible. What's different is that
......
...@@ -10,6 +10,7 @@ import 'package:flutter/rendering.dart'; ...@@ -10,6 +10,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'colors.dart'; import 'colors.dart';
import 'debug.dart';
import 'icon.dart'; import 'icon.dart';
import 'icons.dart'; import 'icons.dart';
import 'icon_theme.dart'; import 'icon_theme.dart';
...@@ -330,6 +331,7 @@ class _Tab extends StatelessComponent { ...@@ -330,6 +331,7 @@ class _Tab extends StatelessComponent {
} }
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
Widget labelContent; Widget labelContent;
if (label.icon == null && label.iconBuilder == null) { if (label.icon == null && label.iconBuilder == null) {
labelContent = _buildLabelText(); labelContent = _buildLabelText();
......
...@@ -46,6 +46,7 @@ class Tooltip extends StatefulComponent { ...@@ -46,6 +46,7 @@ class Tooltip extends StatefulComponent {
assert(preferBelow != null); assert(preferBelow != null);
assert(fadeDuration != null); assert(fadeDuration != null);
assert(showDuration != null); assert(showDuration != null);
assert(child != null);
} }
final String message; final String message;
...@@ -150,7 +151,7 @@ class _TooltipState extends State<Tooltip> { ...@@ -150,7 +151,7 @@ class _TooltipState extends State<Tooltip> {
preferBelow: config.preferBelow preferBelow: config.preferBelow
); );
}); });
Overlay.of(context).insert(_entry); Overlay.of(context, debugRequiredFor: config).insert(_entry);
} }
_timer?.cancel(); _timer?.cancel();
if (_controller.status != AnimationStatus.completed) { if (_controller.status != AnimationStatus.completed) {
...@@ -175,7 +176,7 @@ class _TooltipState extends State<Tooltip> { ...@@ -175,7 +176,7 @@ class _TooltipState extends State<Tooltip> {
} }
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(Overlay.of(context) != null); assert(Overlay.of(context, debugRequiredFor: config) != null);
return new GestureDetector( return new GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onLongPress: showTooltip, onLongPress: showTooltip,
......
...@@ -31,22 +31,22 @@ class TextStyle { ...@@ -31,22 +31,22 @@ class TextStyle {
/// The color to use when painting the text. /// The color to use when painting the text.
final Color color; final Color color;
/// The name of the font to use when painting the text. /// The name of the font to use when painting the text (e.g., Roboto).
final String fontFamily; final String fontFamily;
/// The size of gyphs (in logical pixels) to use when painting the text. /// The size of gyphs (in logical pixels) to use when painting the text.
final double fontSize; final double fontSize;
/// The font weight to use when painting the text. /// The typeface thickness to use when painting the text (e.g., bold).
final FontWeight fontWeight; final FontWeight fontWeight;
/// The font style to use when painting the text. /// The typeface variant to use when drawing the letters (e.g., italics).
final FontStyle fontStyle; final FontStyle fontStyle;
/// The amount of space to add between each letter. /// The amount of space (in logical pixels) to add between each letter.
final double letterSpacing; final double letterSpacing;
/// The amount of space to add at each sequence of white-space (i.e. between each word). /// The amount of space (in logical pixels) to add at each sequence of white-space (i.e. between each word).
final double wordSpacing; final double wordSpacing;
/// How the text should be aligned (applies only to the outermost /// How the text should be aligned (applies only to the outermost
...@@ -59,13 +59,13 @@ class TextStyle { ...@@ -59,13 +59,13 @@ class TextStyle {
/// The distance between the text baselines, as a multiple of the font size. /// The distance between the text baselines, as a multiple of the font size.
final double height; final double height;
/// The decorations to paint near the text. /// The decorations to paint near the text (e.g., an underline).
final TextDecoration decoration; final TextDecoration decoration;
/// The color in which to paint the text decorations. /// The color in which to paint the text decorations.
final Color decorationColor; final Color decorationColor;
/// The style in which to paint the text decorations. /// The style in which to paint the text decorations (e.g., dashed).
final TextDecorationStyle decorationStyle; final TextDecorationStyle decorationStyle;
/// Returns a new text style that matches this text style but with the given /// Returns a new text style that matches this text style but with the given
......
...@@ -19,7 +19,7 @@ import 'semantics.dart'; ...@@ -19,7 +19,7 @@ import 'semantics.dart';
export 'package:flutter/gestures.dart' show HitTestResult; export 'package:flutter/gestures.dart' show HitTestResult;
/// The glue between the render tree and the Flutter engine. /// The glue between the render tree and the Flutter engine.
abstract class Renderer extends Object with Scheduler, MojoShell abstract class Renderer extends Object with Scheduler, Services
implements HitTestable { implements HitTestable {
void initInstances() { void initInstances() {
...@@ -67,7 +67,7 @@ abstract class Renderer extends Object with Scheduler, MojoShell ...@@ -67,7 +67,7 @@ abstract class Renderer extends Object with Scheduler, MojoShell
void initSemantics() { void initSemantics() {
SemanticsNode.onSemanticsEnabled = renderView.scheduleInitialSemantics; SemanticsNode.onSemanticsEnabled = renderView.scheduleInitialSemantics;
provideService(mojom.SemanticsServer.serviceName, (core.MojoMessagePipeEndpoint endpoint) { shell.provideService(mojom.SemanticsServer.serviceName, (core.MojoMessagePipeEndpoint endpoint) {
mojom.SemanticsServerStub server = new mojom.SemanticsServerStub.fromEndpoint(endpoint); mojom.SemanticsServerStub server = new mojom.SemanticsServerStub.fromEndpoint(endpoint);
server.impl = new SemanticsServer(); server.impl = new SemanticsServer();
}); });
...@@ -116,7 +116,7 @@ void debugDumpSemanticsTree() { ...@@ -116,7 +116,7 @@ void debugDumpSemanticsTree() {
/// A concrete binding for applications that use the Rendering framework /// A concrete binding for applications that use the Rendering framework
/// directly. This is the glue that binds the framework to the Flutter engine. /// directly. This is the glue that binds the framework to the Flutter engine.
class RenderingFlutterBinding extends BindingBase with Scheduler, Gesturer, MojoShell, Renderer { class RenderingFlutterBinding extends BindingBase with Scheduler, Gesturer, Services, Renderer {
RenderingFlutterBinding({ RenderBox root }) { RenderingFlutterBinding({ RenderBox root }) {
assert(renderView != null); assert(renderView != null);
renderView.child = root; renderView.child = root;
......
...@@ -20,7 +20,7 @@ import 'object.dart'; ...@@ -20,7 +20,7 @@ import 'object.dart';
mojom.ViewProxy _initViewProxy() { mojom.ViewProxy _initViewProxy() {
int viewHandle = ui.takeViewHandle(); int viewHandle = ui.takeViewHandle();
assert(() { assert(() {
if (viewHandle == 0) if (viewHandle == core.MojoHandle.INVALID)
debugPrint('Child view are supported only when running in Mojo shell.'); debugPrint('Child view are supported only when running in Mojo shell.');
return true; return true;
}); });
...@@ -35,8 +35,12 @@ mojom.ViewProxy _initViewProxy() { ...@@ -35,8 +35,12 @@ mojom.ViewProxy _initViewProxy() {
final mojom.ViewProxy _viewProxy = _initViewProxy(); final mojom.ViewProxy _viewProxy = _initViewProxy();
final mojom.View _view = _viewProxy?.ptr; final mojom.View _view = _viewProxy?.ptr;
/// (mojo-only) A connection with a child view.
///
/// Used with the [ChildView] widget to display a child view.
class ChildViewConnection { class ChildViewConnection {
ChildViewConnection({ this.url }) { /// Establishes a connection to the app at the given URL.
ChildViewConnection({ String url }) {
mojom.ViewProviderProxy viewProvider = new mojom.ViewProviderProxy.unbound(); mojom.ViewProviderProxy viewProvider = new mojom.ViewProviderProxy.unbound();
shell.connectToService(url, viewProvider); shell.connectToService(url, viewProvider);
mojom.ServiceProviderProxy incomingServices = new mojom.ServiceProviderProxy.unbound(); mojom.ServiceProviderProxy incomingServices = new mojom.ServiceProviderProxy.unbound();
...@@ -47,8 +51,16 @@ class ChildViewConnection { ...@@ -47,8 +51,16 @@ class ChildViewConnection {
_connection = new ApplicationConnection(outgoingServices, incomingServices); _connection = new ApplicationConnection(outgoingServices, incomingServices);
} }
final String url; /// Wraps an already-established connection ot a child app.
ChildViewConnection.fromViewOwner({
mojom.ViewOwnerProxy viewOwner,
ApplicationConnection connection
}) : _connection = connection, _viewOwner = viewOwner;
/// The underlying application connection to the child app.
///
/// Useful for requesting services from the child app and for providing
/// services to the child app.
ApplicationConnection get connection => _connection; ApplicationConnection get connection => _connection;
ApplicationConnection _connection; ApplicationConnection _connection;
...@@ -120,18 +132,16 @@ class ChildViewConnection { ...@@ -120,18 +132,16 @@ class ChildViewConnection {
..devicePixelRatio = scale; ..devicePixelRatio = scale;
return (await _view.layoutChild(_viewKey, layoutParams)).info; return (await _view.layoutChild(_viewKey, layoutParams)).info;
} }
String toString() {
return '$runtimeType(url: $url)';
}
} }
/// (mojo-only) A view of a child application.
class RenderChildView extends RenderBox { class RenderChildView extends RenderBox {
RenderChildView({ RenderChildView({
ChildViewConnection child, ChildViewConnection child,
double scale double scale
}) : _child = child, _scale = scale; }) : _child = child, _scale = scale;
/// The child to display.
ChildViewConnection get child => _child; ChildViewConnection get child => _child;
ChildViewConnection _child; ChildViewConnection _child;
void set child (ChildViewConnection value) { void set child (ChildViewConnection value) {
...@@ -150,6 +160,7 @@ class RenderChildView extends RenderBox { ...@@ -150,6 +160,7 @@ class RenderChildView extends RenderBox {
} }
} }
/// The device pixel ratio to provide the child.
double get scale => _scale; double get scale => _scale;
double _scale; double _scale;
void set scale (double value) { void set scale (double value) {
......
...@@ -103,7 +103,7 @@ abstract class MultiChildLayoutDelegate { ...@@ -103,7 +103,7 @@ abstract class MultiChildLayoutDelegate {
return '${childParentData.id}: $child'; return '${childParentData.id}: $child';
} }
void _callPerformLayout(Size size, BoxConstraints constraints, RenderBox firstChild) { void _callPerformLayout(Size size, RenderBox firstChild) {
// A particular layout delegate could be called reentrantly, e.g. if it used // A particular layout delegate could be called reentrantly, e.g. if it used
// by both a parent and a child. So, we must restore the _idToChild map when // by both a parent and a child. So, we must restore the _idToChild map when
// we return. // we return.
...@@ -138,7 +138,7 @@ abstract class MultiChildLayoutDelegate { ...@@ -138,7 +138,7 @@ abstract class MultiChildLayoutDelegate {
}); });
child = childParentData.nextSibling; child = childParentData.nextSibling;
} }
performLayout(size, constraints); performLayout(size);
assert(() { assert(() {
if (_debugChildrenNeedingLayout.isNotEmpty) { if (_debugChildrenNeedingLayout.isNotEmpty) {
if (_debugChildrenNeedingLayout.length > 1) { if (_debugChildrenNeedingLayout.length > 1) {
...@@ -176,11 +176,10 @@ abstract class MultiChildLayoutDelegate { ...@@ -176,11 +176,10 @@ abstract class MultiChildLayoutDelegate {
/// possible given the constraints. /// possible given the constraints.
Size getSize(BoxConstraints constraints) => constraints.biggest; Size getSize(BoxConstraints constraints) => constraints.biggest;
/// Override this method to lay out and position all children given /// Override this method to lay out and position all children given this
/// this widget's size and the specified constraints. This method /// widget's size. This method must call [layoutChild] for each child. It
/// must call [layoutChild] for each child. It should also specify /// should also specify the final position of each child with [positionChild].
/// the final position of each child with [positionChild]. void performLayout(Size size);
void performLayout(Size size, BoxConstraints constraints);
/// Override this method to return true when the children need to be /// Override this method to return true when the children need to be
/// laid out. This should compare the fields of the current delegate /// laid out. This should compare the fields of the current delegate
...@@ -257,7 +256,7 @@ class RenderCustomMultiChildLayoutBox extends RenderBox ...@@ -257,7 +256,7 @@ class RenderCustomMultiChildLayoutBox extends RenderBox
} }
void performLayout() { void performLayout() {
delegate._callPerformLayout(size, constraints, firstChild); delegate._callPerformLayout(size, firstChild);
} }
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
......
...@@ -53,9 +53,6 @@ bool debugPaintPointersEnabled = false; ...@@ -53,9 +53,6 @@ bool debugPaintPointersEnabled = false;
/// The color to use when reporting pointers. /// The color to use when reporting pointers.
int debugPaintPointersColorValue = 0x00BBBB; int debugPaintPointersColorValue = 0x00BBBB;
/// The color to use when painting [RenderErrorBox] objects in checked mode.
Color debugErrorBoxColor = const Color(0xFFFF0000);
/// Overlay a rotating set of colors when repainting layers in checked mode. /// Overlay a rotating set of colors when repainting layers in checked mode.
bool debugRepaintRainbowEnabled = false; bool debugRepaintRainbowEnabled = false;
......
...@@ -2,15 +2,56 @@ ...@@ -2,15 +2,56 @@
// 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:ui' as ui show Paragraph, ParagraphBuilder, ParagraphStyle, TextStyle;
import 'box.dart'; import 'box.dart';
import 'debug.dart';
import 'object.dart'; import 'object.dart';
const double _kMaxWidth = 100000.0; const double _kMaxWidth = 100000.0;
const double _kMaxHeight = 100000.0; const double _kMaxHeight = 100000.0;
/// A render object used as a placeholder when an error occurs /// A render object used as a placeholder when an error occurs.
///
/// The box will be painted in the color given by the
/// [RenderErrorBox.backgroundColor] static property.
///
/// A message can be provided. To simplify the class and thus help reduce the
/// likelihood of this class itself being the source of errors, the message
/// cannot be changed once the object has been created. If provided, the text
/// will be painted on top of the background, using the styles given by the
/// [RenderErrorBox.textStyle] and [RenderErrorBox.paragraphStyle] static
/// properties.
///
/// Again to help simplify the class, this box tries to be 100000.0 pixels wide
/// and high, to approximate being infinitely high but without using infinities.
class RenderErrorBox extends RenderBox { class RenderErrorBox extends RenderBox {
/// Constructs a RenderErrorBox render object.
///
/// A message can optionally be provided. If a message is provided, an attempt
/// will be made to render the message when the box paints.
RenderErrorBox([ this.message = '' ]) {
try {
if (message != '') {
// This class is intentionally doing things using the low-level
// primitives to avoid depending on any subsystems that may have ended
// up in an unstable state -- after all, this class is mainly used when
// things have gone wrong.
//
// Generally, the much better way to draw text in a RenderObject is to
// use the TextPainter class. If you're looking for code to crib from,
// see the paragraph.dart file and the RenderParagraph class.
ui.ParagraphBuilder builder = new ui.ParagraphBuilder();
builder.pushStyle(textStyle);
builder.addText(message);
_paragraph = builder.build(paragraphStyle);
}
} catch (e) { }
}
/// The message to attempt to display at paint time.
final String message;
ui.Paragraph _paragraph;
double getMinIntrinsicWidth(BoxConstraints constraints) { double getMinIntrinsicWidth(BoxConstraints constraints) {
return constraints.constrainWidth(0.0); return constraints.constrainWidth(0.0);
...@@ -36,8 +77,36 @@ class RenderErrorBox extends RenderBox { ...@@ -36,8 +77,36 @@ class RenderErrorBox extends RenderBox {
size = constraints.constrain(const Size(_kMaxWidth, _kMaxHeight)); size = constraints.constrain(const Size(_kMaxWidth, _kMaxHeight));
} }
/// The color to use when painting the background of [RenderErrorBox] objects.
static Color backgroundColor = const Color(0xF0900000);
/// The text style to use when painting [RenderErrorBox] objects.
static ui.TextStyle textStyle = new ui.TextStyle(
color: const Color(0xFFFFFF00),
fontFamily: 'monospace',
fontSize: 7.0
);
/// The paragraph style to use when painting [RenderErrorBox] objects.
static ui.ParagraphStyle paragraphStyle = new ui.ParagraphStyle(
lineHeight: 0.25 // TODO(ianh): https://github.com/flutter/flutter/issues/2460 will affect this
);
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
context.canvas.drawRect(offset & size, new Paint() .. color = debugErrorBoxColor); try {
context.canvas.drawRect(offset & size, new Paint() .. color = backgroundColor);
if (_paragraph != null) {
// See the comment in the RenderErrorBox constructor. This is not the
// code you want to be copying and pasting. :-)
if (parent is RenderBox) {
RenderBox parentBox = parent;
_paragraph.maxWidth = parentBox.size.width;
} else {
_paragraph.maxWidth = size.width;
}
_paragraph.layout();
_paragraph.paint(context.canvas, offset);
}
} catch (e) { }
} }
} }
...@@ -96,14 +96,11 @@ class MojoAssetBundle extends CachingAssetBundle { ...@@ -96,14 +96,11 @@ class MojoAssetBundle extends CachingAssetBundle {
} }
AssetBundle _initRootBundle() { AssetBundle _initRootBundle() {
try { int h = ui.takeRootBundleHandle();
AssetBundleProxy bundle = new AssetBundleProxy.fromHandle( if (h == core.MojoHandle.INVALID)
new core.MojoHandle(ui.takeRootBundleHandle())
);
return new MojoAssetBundle(bundle);
} catch (e) {
return null; return null;
} core.MojoHandle handle = new core.MojoHandle(h);
return new MojoAssetBundle(new AssetBundleProxy.fromHandle(handle));
} }
final AssetBundle rootBundle = _initRootBundle(); final AssetBundle rootBundle = _initRootBundle();
// Copyright 2015 The Chromium Authors. All rights reserved. // Copyright 2016 The Chromium Authors. All rights reserved.
// 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:ui' as ui; import 'package:flutter/shell.dart';
import 'package:mojo/application.dart'; export 'package:flutter/shell.dart';
import 'package:mojo/bindings.dart' as bindings;
import 'package:mojo/core.dart' as core;
import 'package:mojo/mojo/service_provider.mojom.dart' as mojom;
import 'package:mojo/mojo/shell.mojom.dart' as mojom;
/// Base class for mixins that provide singleton services (also known as /// Base class for mixins that provide singleton services (also known as
/// "bindings"). /// "bindings").
...@@ -41,84 +37,9 @@ abstract class BindingBase { ...@@ -41,84 +37,9 @@ abstract class BindingBase {
String toString() => '<$runtimeType>'; String toString() => '<$runtimeType>';
} }
// A replacement for shell.connectToService. Implementations should return true abstract class Services extends BindingBase {
// if they handled the request, or false if the request should fall through
// to the default requestService.
typedef bool OverrideConnectToService(String url, Object proxy);
abstract class MojoShell extends BindingBase {
void initInstances() { void initInstances() {
super.initInstances(); super.initInstances();
_instance = this; new MojoShell();
}
static MojoShell _instance;
static MojoShell get instance => _instance;
static mojom.ShellProxy _initShellProxy() {
core.MojoHandle shellHandle = new core.MojoHandle(ui.takeShellProxyHandle());
if (!shellHandle.isValid)
return null;
return new mojom.ShellProxy.fromHandle(shellHandle);
}
final mojom.Shell _shell = _initShellProxy()?.ptr;
static ApplicationConnection _initEmbedderConnection() {
core.MojoHandle servicesHandle = new core.MojoHandle(ui.takeServicesProvidedByEmbedder());
core.MojoHandle exposedServicesHandle = new core.MojoHandle(ui.takeServicesProvidedToEmbedder());
if (!servicesHandle.isValid || !exposedServicesHandle.isValid)
return null;
mojom.ServiceProviderProxy services = new mojom.ServiceProviderProxy.fromHandle(servicesHandle);
mojom.ServiceProviderStub exposedServices = new mojom.ServiceProviderStub.fromHandle(exposedServicesHandle);
return new ApplicationConnection(exposedServices, services);
}
final ApplicationConnection _embedderConnection = _initEmbedderConnection();
/// Attempts to connect to an application via the Mojo shell.
ApplicationConnection connectToApplication(String url) {
if (_shell == null)
return null;
mojom.ServiceProviderProxy services = new mojom.ServiceProviderProxy.unbound();
mojom.ServiceProviderStub exposedServices = new mojom.ServiceProviderStub.unbound();
_shell.connectToApplication(url, services, exposedServices);
return new ApplicationConnection(exposedServices, services);
}
/// Set this to intercept calls to [connectToService()] and supply an
/// alternative implementation of a service (for example, a mock for testing).
OverrideConnectToService overrideConnectToService;
/// Attempts to connect to a service implementing the interface for the given proxy.
/// If an application URL is specified, the service will be requested from that application.
/// Otherwise, it will be requested from the embedder (the Flutter engine).
void connectToService(String url, bindings.ProxyBase proxy) {
if (overrideConnectToService != null && overrideConnectToService(url, proxy))
return;
if (url == null || _shell == null) {
// If the application URL is null, it means the service to connect
// to is one provided by the embedder.
// If the applircation URL isn't null but there's no shell, then
// ask the embedder in case it provides it. (For example, if you're
// running on Android without the Mojo shell, then you can obtain
// the media service from the embedder directly, instead of having
// to ask the media application for it.)
// This makes it easier to write an application that works both
// with and without a Mojo environment.
_embedderConnection?.requestService(proxy);
return;
}
mojom.ServiceProviderProxy services = new mojom.ServiceProviderProxy.unbound();
_shell.connectToApplication(url, services, null);
core.MojoMessagePipe pipe = new core.MojoMessagePipe();
proxy.impl.bind(pipe.endpoints[0]);
services.ptr.connectToService(proxy.serviceName, pipe.endpoints[1]);
services.close();
}
/// Registers a service to expose to the embedder.
void provideService(String interfaceName, ServiceFactory factory) {
_embedderConnection?.provideService(interfaceName, factory);
} }
} }
MojoShell get shell => MojoShell.instance;
...@@ -46,9 +46,5 @@ void _debugPrintTask() { ...@@ -46,9 +46,5 @@ void _debugPrintTask() {
} }
void debugPrintStack() { void debugPrintStack() {
try { debugPrint(StackTrace.current.toString());
throw new Exception();
} catch (e, stack) {
debugPrint(stack.toString());
}
} }
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:ui' as ui show Locale, WindowPadding, window;
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'asset_vendor.dart';
import 'basic.dart';
import 'binding.dart';
import 'checked_mode_banner.dart';
import 'framework.dart';
import 'locale_query.dart';
import 'media_query.dart';
import 'navigator.dart';
import 'performance_overlay.dart';
import 'semantics_debugger.dart';
import 'title.dart';
AssetBundle _initDefaultBundle() {
if (rootBundle != null)
return rootBundle;
return new NetworkAssetBundle(Uri.base);
}
final AssetBundle _defaultBundle = _initDefaultBundle();
class RouteArguments {
const RouteArguments({ this.context });
final BuildContext context;
}
typedef Widget RouteBuilder(RouteArguments args);
typedef Future<LocaleQueryData> LocaleChangedCallback(Locale locale);
class WidgetsApp extends StatefulComponent {
WidgetsApp({
Key key,
this.title,
this.textStyle,
this.color,
this.routes: const <String, RouteBuilder>{},
this.onGenerateRoute,
this.onLocaleChanged,
this.showPerformanceOverlay: false,
this.showSemanticsDebugger: false,
this.debugShowCheckedModeBanner: true
}) : super(key: key) {
assert(routes != null);
assert(routes.containsKey(Navigator.defaultRouteName) || onGenerateRoute != null);
assert(showPerformanceOverlay != null);
assert(showSemanticsDebugger != null);
}
/// A one-line description of this app for use in the window manager.
final String title;
/// The default text style for [Text] in the application.
final TextStyle textStyle;
/// The primary color to use for the application in the operating system
/// interface.
///
/// For example, on Android this is the color used for the application in the
/// application switcher.
final Color color;
/// The default table of routes for the application. When the
/// [Navigator] is given a named route, the name will be looked up
/// in this table first. If the name is not available, then
/// [onGenerateRoute] will be called instead.
final Map<String, RouteBuilder> routes;
/// The route generator callback used when the app is navigated to a
/// named route but the name is not in the [routes] table.
final RouteFactory onGenerateRoute;
/// Callback that is invoked when the operating system changes the
/// current locale.
final LocaleChangedCallback onLocaleChanged;
/// Turns on a performance overlay.
/// https://flutter.io/debugging/#performanceoverlay
final bool showPerformanceOverlay;
/// Turns on an overlay that shows the accessibility information
/// reported by the framework.
final bool showSemanticsDebugger;
/// Turns on a "SLOW MODE" little banner in checked mode to indicate
/// that the app is in checked mode. This is on by default (in
/// checked mode), to turn it off, set the constructor argument to
/// false. In release mode this has no effect.
///
/// To get this banner in your application if you're not using
/// WidgetsApp, include a [CheckedModeBanner] widget in your app.
///
/// This banner is intended to avoid people complaining that your
/// app is slow when it's in checked mode. In checked mode, Flutter
/// enables a large number of expensive diagnostics to aid in
/// development, and so performance in checked mode is not
/// representative of what will happen in release mode.
final bool debugShowCheckedModeBanner;
WidgetsAppState<WidgetsApp> createState() => new WidgetsAppState<WidgetsApp>();
}
EdgeDims _getPadding(ui.WindowPadding padding) {
return new EdgeDims.TRBL(padding.top, padding.right, padding.bottom, padding.left);
}
class WidgetsAppState<T extends WidgetsApp> extends State<T> implements BindingObserver {
GlobalObjectKey _navigator;
LocaleQueryData _localeData;
void initState() {
super.initState();
_navigator = new GlobalObjectKey(this);
didChangeLocale(ui.window.locale);
WidgetFlutterBinding.instance.addObserver(this);
}
void dispose() {
WidgetFlutterBinding.instance.removeObserver(this);
super.dispose();
}
bool didPopRoute() {
assert(mounted);
NavigatorState navigator = _navigator.currentState;
assert(navigator != null);
bool result = false;
navigator.openTransaction((NavigatorTransaction transaction) {
result = transaction.pop();
});
return result;
}
void didChangeMetrics() {
setState(() {
// The properties of ui.window have changed. We use them in our build
// function, so we need setState(), but we don't cache anything locally.
});
}
void didChangeLocale(Locale locale) {
if (config.onLocaleChanged != null) {
config.onLocaleChanged(locale).then((LocaleQueryData data) {
if (mounted)
setState(() { _localeData = data; });
});
}
}
void didChangeAppLifecycleState(AppLifecycleState state) { }
NavigatorObserver get navigatorObserver => null;
Widget build(BuildContext context) {
if (config.onLocaleChanged != null && _localeData == null) {
// If the app expects a locale but we don't yet know the locale, then
// don't build the widgets now.
// TODO(ianh): Make this unnecessary. See https://github.com/flutter/flutter/issues/1865
return new Container();
}
Widget result = new MediaQuery(
data: new MediaQueryData(
size: ui.window.size,
devicePixelRatio: ui.window.devicePixelRatio,
padding: _getPadding(ui.window.padding)
),
child: new LocaleQuery(
data: _localeData,
child: new DefaultTextStyle(
style: config.textStyle,
child: new AssetVendor(
bundle: _defaultBundle,
devicePixelRatio: ui.window.devicePixelRatio,
child: new Title(
title: config.title,
color: config.color,
child: new Navigator(
key: _navigator,
initialRoute: ui.window.defaultRouteName,
onGenerateRoute: config.onGenerateRoute,
observer: navigatorObserver
)
)
)
)
)
);
if (config.showPerformanceOverlay) {
result = new Stack(
children: <Widget>[
result,
new Positioned(bottom: 0.0, left: 0.0, right: 0.0, child: new PerformanceOverlay.allEnabled()),
]
);
}
if (config.showSemanticsDebugger) {
result = new SemanticsDebugger(
child: result
);
}
assert(() {
if (config.debugShowCheckedModeBanner) {
result = new CheckedModeBanner(
child: result
);
}
return true;
});
return result;
}
}
...@@ -999,6 +999,7 @@ class BlockBody extends MultiChildRenderObjectWidget { ...@@ -999,6 +999,7 @@ class BlockBody extends MultiChildRenderObjectWidget {
} }
} }
/// A base class for widgets that accept [Positioned] children.
abstract class StackRenderObjectWidgetBase extends MultiChildRenderObjectWidget { abstract class StackRenderObjectWidgetBase extends MultiChildRenderObjectWidget {
StackRenderObjectWidgetBase({ StackRenderObjectWidgetBase({
List<Widget> children: _emptyWidgetList, List<Widget> children: _emptyWidgetList,
......
...@@ -24,7 +24,7 @@ class BindingObserver { ...@@ -24,7 +24,7 @@ class BindingObserver {
/// A concrete binding for applications based on the Widgets framework. /// A concrete binding for applications based on the Widgets framework.
/// This is the glue that binds the framework to the Flutter engine. /// This is the glue that binds the framework to the Flutter engine.
class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, MojoShell, Renderer { class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, Services, Renderer {
/// Creates and initializes the WidgetFlutterBinding. This constructor is /// Creates and initializes the WidgetFlutterBinding. This constructor is
/// idempotent; calling it a second time will just return the /// idempotent; calling it a second time will just return the
......
...@@ -256,7 +256,7 @@ class _DismissableState extends State<Dismissable> { ...@@ -256,7 +256,7 @@ class _DismissableState extends State<Dismissable> {
assert(_resizeAnimation.status == AnimationStatus.completed); assert(_resizeAnimation.status == AnimationStatus.completed);
throw new WidgetError( throw new WidgetError(
'Dismissable widget completed its resize animation without being removed from the tree.\n' 'Dismissable widget completed its resize animation without being removed from the tree.\n'
'Make sure to implement the onDismissed handler and to immediately remove the Dismissable\n' 'Make sure to implement the onDismissed handler and to immediately remove the Dismissable '
'widget from the application once that handler has fired.' 'widget from the application once that handler has fired.'
); );
} }
......
...@@ -234,7 +234,7 @@ class _DraggableState<T> extends State<DraggableBase<T>> { ...@@ -234,7 +234,7 @@ class _DraggableState<T> extends State<DraggableBase<T>> {
_activeCount += 1; _activeCount += 1;
}); });
return new _DragAvatar<T>( return new _DragAvatar<T>(
overlay: Overlay.of(context), overlay: Overlay.of(context, debugRequiredFor: config),
data: config.data, data: config.data,
initialPosition: position, initialPosition: position,
dragStartPoint: dragStartPoint, dragStartPoint: dragStartPoint,
...@@ -249,6 +249,7 @@ class _DraggableState<T> extends State<DraggableBase<T>> { ...@@ -249,6 +249,7 @@ class _DraggableState<T> extends State<DraggableBase<T>> {
} }
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(Overlay.of(context, debugRequiredFor: config) != null);
final bool canDrag = config.maxSimultaneousDrags == null || final bool canDrag = config.maxSimultaneousDrags == null ||
_activeCount < config.maxSimultaneousDrags; _activeCount < config.maxSimultaneousDrags;
final bool showChild = _activeCount == 0 || config.childWhenDragging == null; final bool showChild = _activeCount == 0 || config.childWhenDragging == null;
......
...@@ -149,8 +149,12 @@ abstract class GlobalKey<T extends State<StatefulComponent>> extends Key { ...@@ -149,8 +149,12 @@ abstract class GlobalKey<T extends State<StatefulComponent>> extends Key {
message += 'The following GlobalKey was found multiple times among mounted elements: $key (${_debugDuplicates[key]} instances)\n'; message += 'The following GlobalKey was found multiple times among mounted elements: $key (${_debugDuplicates[key]} instances)\n';
message += 'The most recently registered instance is: ${_registry[key]}\n'; message += 'The most recently registered instance is: ${_registry[key]}\n';
} }
if (!_debugDuplicates.isEmpty) if (!_debugDuplicates.isEmpty) {
throw new WidgetError('Incorrect GlobalKey usage.', message); throw new WidgetError(
'Incorrect GlobalKey usage.\n'
'$message'
);
}
return true; return true;
} }
...@@ -1012,8 +1016,24 @@ abstract class Element<T extends Widget> implements BuildContext { ...@@ -1012,8 +1016,24 @@ abstract class Element<T extends Widget> implements BuildContext {
} }
} }
/// A widget that renders an exception's message. This widget is used when a
/// build function fails, to help with determining where the problem lies.
/// Exceptions are also logged to the console, which you can read using `flutter
/// logs`. The console will also include additional information such as the
/// stack trace for the exception.
class ErrorWidget extends LeafRenderObjectWidget { class ErrorWidget extends LeafRenderObjectWidget {
RenderBox createRenderObject() => new RenderErrorBox(); ErrorWidget(
Object exception
) : message = _stringify(exception),
super(key: new UniqueKey());
final String message;
static String _stringify(Object exception) {
try {
return exception.toString();
} catch (e) { }
return 'Error';
}
RenderBox createRenderObject() => new RenderErrorBox(message);
} }
typedef void BuildScheduler(BuildableElement element); typedef void BuildScheduler(BuildableElement element);
...@@ -1109,9 +1129,10 @@ abstract class BuildableElement<T extends Widget> extends Element<T> { ...@@ -1109,9 +1129,10 @@ abstract class BuildableElement<T extends Widget> extends Element<T> {
} }
if (_debugStateLocked && (!_debugAllowIgnoredCallsToMarkNeedsBuild || !dirty)) { if (_debugStateLocked && (!_debugAllowIgnoredCallsToMarkNeedsBuild || !dirty)) {
throw new WidgetError( throw new WidgetError(
'Cannot mark this component as needing to build because the framework is ' 'setState() or markNeedsBuild() called during build.\n'
'already in the process of building widgets. A widget can be marked as ' 'This component cannot be marked as needing to build because the framework '
'needing to be built during the build phase only if one if its ancestor ' 'is already in the process of building widgets. A widget can be marked as '
'needing to be built during the build phase only if one if its ancestors '
'is currently building. This exception is allowed because the framework ' 'is currently building. This exception is allowed because the framework '
'builds parent widgets before children, which means a dirty descendant ' 'builds parent widgets before children, which means a dirty descendant '
'will always be built. Otherwise, the framework might not visit this ' 'will always be built. Otherwise, the framework might not visit this '
...@@ -1208,15 +1229,18 @@ abstract class ComponentElement<T extends Widget> extends BuildableElement<T> { ...@@ -1208,15 +1229,18 @@ abstract class ComponentElement<T extends Widget> extends BuildableElement<T> {
assert(() { assert(() {
if (built == null) { if (built == null) {
throw new WidgetError( throw new WidgetError(
'A build function returned null. Build functions must never return null.', 'A build function returned null.\n'
'The offending widget is: $widget' 'The offending widget is: $widget\n'
'Build functions must never return null. '
'To return an empty space that causes the building widget to fill available room, return "new Container()". '
'To return an empty space that takes as little room as possible, return "new Container(width: 0.0, height: 0.0)".'
); );
} }
return true; return true;
}); });
} catch (e, stack) { } catch (e, stack) {
_debugReportException('building $_widget', e, stack); _debugReportException('building $_widget', e, stack);
built = new ErrorWidget(); built = new ErrorWidget(e);
} finally { } finally {
// We delay marking the element as clean until after calling _builder so // We delay marking the element as clean until after calling _builder so
// that attempts to markNeedsBuild() during build() will be ignored. // that attempts to markNeedsBuild() during build() will be ignored.
...@@ -1228,7 +1252,7 @@ abstract class ComponentElement<T extends Widget> extends BuildableElement<T> { ...@@ -1228,7 +1252,7 @@ abstract class ComponentElement<T extends Widget> extends BuildableElement<T> {
assert(_child != null); assert(_child != null);
} catch (e, stack) { } catch (e, stack) {
_debugReportException('building $_widget', e, stack); _debugReportException('building $_widget', e, stack);
built = new ErrorWidget(); built = new ErrorWidget(e);
_child = updateChild(null, built, slot); _child = updateChild(null, built, slot);
} }
} }
...@@ -1289,7 +1313,11 @@ class StatefulComponentElement<T extends StatefulComponent, U extends State<T>> ...@@ -1289,7 +1313,11 @@ class StatefulComponentElement<T extends StatefulComponent, U extends State<T>>
assert(() { assert(() {
if (_state._debugLifecycleState == _StateLifecycle.initialized) if (_state._debugLifecycleState == _StateLifecycle.initialized)
return true; return true;
throw new WidgetError('${_state.runtimeType}.initState failed to call super.initState.'); throw new WidgetError(
'${_state.runtimeType}.initState failed to call super.initState.\n'
'initState() implementations must always call their superclass initState() method, to ensure '
'that the entire widget is initialized correctly.'
);
}); });
assert(() { _state._debugLifecycleState = _StateLifecycle.ready; return true; }); assert(() { _state._debugLifecycleState = _StateLifecycle.ready; return true; });
super._firstBuild(); super._firstBuild();
...@@ -1324,7 +1352,11 @@ class StatefulComponentElement<T extends StatefulComponent, U extends State<T>> ...@@ -1324,7 +1352,11 @@ class StatefulComponentElement<T extends StatefulComponent, U extends State<T>>
assert(() { assert(() {
if (_state._debugLifecycleState == _StateLifecycle.defunct) if (_state._debugLifecycleState == _StateLifecycle.defunct)
return true; return true;
throw new WidgetError('${_state.runtimeType}.dispose failed to call super.dispose.'); throw new WidgetError(
'${_state.runtimeType}.dispose failed to call super.dispose.\n'
'dispose() implementations must always call their superclass dispose() method, to ensure '
'that all the resources used by the widget are fully released.'
);
}); });
assert(!dirty); // See BuildableElement.unmount for why this is important. assert(!dirty); // See BuildableElement.unmount for why this is important.
_state._element = null; _state._element = null;
...@@ -1381,12 +1413,15 @@ class ParentDataElement extends _ProxyElement<ParentDataWidget> { ...@@ -1381,12 +1413,15 @@ class ParentDataElement extends _ProxyElement<ParentDataWidget> {
} }
if (ancestor != null && badAncestors.isEmpty) if (ancestor != null && badAncestors.isEmpty)
return true; return true;
throw new WidgetError('Incorrect use of ParentDataWidget.', widget.debugDescribeInvalidAncestorChain( throw new WidgetError(
description: "$this", 'Incorrect use of ParentDataWidget.\n' +
ownershipChain: parent.debugGetOwnershipChain(10), widget.debugDescribeInvalidAncestorChain(
foundValidAncestor: ancestor != null, description: "$this",
badAncestors: badAncestors ownershipChain: parent.debugGetOwnershipChain(10),
)); foundValidAncestor: ancestor != null,
badAncestors: badAncestors
)
);
}); });
super.mount(parent, slot); super.mount(parent, slot);
} }
...@@ -1507,7 +1542,14 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Buildab ...@@ -1507,7 +1542,14 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Buildab
// 'BuildContext' argument which you can pass to Theme.of() and other // 'BuildContext' argument which you can pass to Theme.of() and other
// InheritedWidget APIs which eventually trigger a rebuild.) // InheritedWidget APIs which eventually trigger a rebuild.)
assert(() { assert(() {
throw new WidgetError('$runtimeType failed to implement reinvokeBuilders(), but got marked dirty.'); throw new WidgetError(
'$runtimeType failed to implement reinvokeBuilders(), but got marked dirty.\n'
'If a RenderObjectElement subclass supports being marked dirty, then the '
'reinvokeBuilders() method must be implemented.\n'
'If a RenderObjectElement uses a builder callback, it must support being '
'marked dirty, because builder callbacks can register the object as having '
'an Inherited dependency.'
);
}); });
} }
...@@ -1812,7 +1854,11 @@ class MultiChildRenderObjectElement<T extends MultiChildRenderObjectWidget> exte ...@@ -1812,7 +1854,11 @@ class MultiChildRenderObjectElement<T extends MultiChildRenderObjectWidget> exte
continue; // when these nodes are reordered, we just reassign the data continue; // when these nodes are reordered, we just reassign the data
if (!idSet.add(child.key)) { if (!idSet.add(child.key)) {
throw new WidgetError('If multiple keyed nodes exist as children of another node, they must have unique keys. $widget has multiple children with key "${child.key}".'); throw new WidgetError(
'Duplicate keys found.\n'
'If multiple keyed nodes exist as children of another node, they must have unique keys.\n'
'$widget has multiple children with key "${child.key}".'
);
} }
} }
return false; return false;
...@@ -1841,16 +1887,10 @@ class MultiChildRenderObjectElement<T extends MultiChildRenderObjectWidget> exte ...@@ -1841,16 +1887,10 @@ class MultiChildRenderObjectElement<T extends MultiChildRenderObjectWidget> exte
} }
} }
class WidgetError extends Error { class WidgetError extends AssertionError {
WidgetError(String message, [ String rawDetails = '' ]) { WidgetError(this.message);
rawDetails = rawDetails.trimRight(); // remove trailing newlines final String message;
if (rawDetails != '') String toString() => message;
_message = '$message\n$rawDetails';
else
_message = message;
}
String _message;
String toString() => _message;
} }
typedef void WidgetsExceptionHandler(String context, dynamic exception, StackTrace stack); typedef void WidgetsExceptionHandler(String context, dynamic exception, StackTrace stack);
......
...@@ -71,11 +71,20 @@ class GestureDetector extends StatelessComponent { ...@@ -71,11 +71,20 @@ class GestureDetector extends StatelessComponent {
bool havePan = onPanStart != null || onPanUpdate != null || onPanEnd != null; bool havePan = onPanStart != null || onPanUpdate != null || onPanEnd != null;
bool haveScale = onScaleStart != null || onScaleUpdate != null || onScaleEnd != null; bool haveScale = onScaleStart != null || onScaleUpdate != null || onScaleEnd != null;
if (havePan || haveScale) { if (havePan || haveScale) {
if (havePan && haveScale) if (havePan && haveScale) {
throw new WidgetError('Having both a pan gesture recognizer and a scale gesture recognizer is redundant; scale is a superset of pan. Just use the scale gesture recognizer.'); throw new WidgetError(
'Incorrect GestureDetector arguments.\n'
'Having both a pan gesture recognizer and a scale gesture recognizer is redundant; scale is a superset of pan. Just use the scale gesture recognizer.'
);
}
String recognizer = havePan ? 'pan' : 'scale'; String recognizer = havePan ? 'pan' : 'scale';
if (haveVerticalDrag && haveHorizontalDrag) if (haveVerticalDrag && haveHorizontalDrag) {
throw new WidgetError('Simultaneously having a vertical drag gesture recognizer, a horizontal drag gesture recognizer, and a $recognizer gesture recognizer will result in the $recognizer gesture recognizer being ignored, since the other two will catch all drags.'); throw new WidgetError(
'Incorrect GestureDetector arguments.\n'
'Simultaneously having a vertical drag gesture recognizer, a horizontal drag gesture recognizer, and a $recognizer gesture recognizer '
'will result in the $recognizer gesture recognizer being ignored, since the other two will catch all drags.'
);
}
} }
return true; return true;
}); });
...@@ -279,8 +288,15 @@ class RawGestureDetectorState extends State<RawGestureDetector> { ...@@ -279,8 +288,15 @@ class RawGestureDetectorState extends State<RawGestureDetector> {
/// the gesture detector should be enabled. /// the gesture detector should be enabled.
void replaceGestureRecognizers(Map<Type, GestureRecognizerFactory> gestures) { void replaceGestureRecognizers(Map<Type, GestureRecognizerFactory> gestures) {
assert(() { assert(() {
if (!RenderObject.debugDoingLayout) if (!RenderObject.debugDoingLayout) {
throw new WidgetError('replaceGestureRecognizers() can only be called during the layout phase.'); throw new WidgetError(
'Unexpected call to replaceGestureRecognizers() method of RawGestureDetectorState.\n'
'The replaceGestureRecognizers() method can only be called during the layout phase. '
'To set the gesture recognisers at other times, trigger a new build using setState() '
'and provide the new gesture recognisers as constructor arguments to the corresponding '
'RawGestureDetector or GestureDetector object.'
);
}
return true; return true;
}); });
_syncAll(gestures); _syncAll(gestures);
......
...@@ -113,8 +113,8 @@ class Hero extends StatefulComponent { ...@@ -113,8 +113,8 @@ class Hero extends StatefulComponent {
if (tagHeroes.containsKey(key)) { if (tagHeroes.containsKey(key)) {
new WidgetError( new WidgetError(
'There are multiple heroes that share the same key within the same subtree.\n' 'There are multiple heroes that share the same key within the same subtree.\n'
'Within each subtree for which heroes are to be animated (typically a PageRoute subtree),\n' 'Within each subtree for which heroes are to be animated (typically a PageRoute subtree), '
'either each Hero must have a unique tag, or, all the heroes with a particular tag must\n' 'either each Hero must have a unique tag, or, all the heroes with a particular tag must '
'have different keys.\n' 'have different keys.\n'
'In this case, the tag "$tag" had multiple heroes with the key "$key".' 'In this case, the tag "$tag" had multiple heroes with the key "$key".'
); );
......
...@@ -137,6 +137,9 @@ class Mimic extends StatelessComponent { ...@@ -137,6 +137,9 @@ class Mimic extends StatelessComponent {
} }
/// A widget that can be copied by a [Mimic]. /// A widget that can be copied by a [Mimic].
///
/// This widget's State, [MimicableState], contains an API for initiating the
/// mimic operation.
class Mimicable extends StatefulComponent { class Mimicable extends StatefulComponent {
Mimicable({ Key key, this.child }) : super(key: key); Mimicable({ Key key, this.child }) : super(key: key);
...@@ -173,7 +176,18 @@ class MimicableState extends State<Mimicable> { ...@@ -173,7 +176,18 @@ class MimicableState extends State<Mimicable> {
/// passing it to a [Mimic] widget. To mimic the child in the /// passing it to a [Mimic] widget. To mimic the child in the
/// [Overlay], consider using [liftToOverlay()] instead. /// [Overlay], consider using [liftToOverlay()] instead.
MimicableHandle startMimic() { MimicableHandle startMimic() {
assert(_placeholderSize == null); assert(() {
if (_placeholderSize != null) {
throw new WidgetError(
'Mimicable started while already active.\n'
'When startMimic() or liftToOverlay() is called on a MimicableState, the mimic becomes active. '
'While active, it cannot be reactivated until it is stopped. '
'To stop a Mimicable started with startMimic(), call the MimicableHandle object\'s stopMimic() method. '
'To stop a Mimicable started with liftToOverlay(), call dispose() on the MimicOverlayEntry.'
);
}
return true;
});
RenderBox box = context.findRenderObject(); RenderBox box = context.findRenderObject();
assert(box != null); assert(box != null);
assert(box.hasSize); assert(box.hasSize);
...@@ -193,8 +207,7 @@ class MimicableState extends State<Mimicable> { ...@@ -193,8 +207,7 @@ class MimicableState extends State<Mimicable> {
/// had when the mimicking process started and (2) the child will be /// had when the mimicking process started and (2) the child will be
/// placed in the enclosing overlay. /// placed in the enclosing overlay.
MimicOverlayEntry liftToOverlay() { MimicOverlayEntry liftToOverlay() {
OverlayState overlay = Overlay.of(context); OverlayState overlay = Overlay.of(context, debugRequiredFor: config);
assert(overlay != null); // You need an overlay to lift into.
MimicOverlayEntry entry = new MimicOverlayEntry._(startMimic()); MimicOverlayEntry entry = new MimicOverlayEntry._(startMimic());
overlay.insert(entry._overlayEntry); overlay.insert(entry._overlayEntry);
return entry; return entry;
......
...@@ -260,8 +260,12 @@ class Navigator extends StatefulComponent { ...@@ -260,8 +260,12 @@ class Navigator extends StatefulComponent {
static void openTransaction(BuildContext context, NavigatorTransactionCallback callback) { static void openTransaction(BuildContext context, NavigatorTransactionCallback callback) {
NavigatorState navigator = context.ancestorStateOfType(const TypeMatcher<NavigatorState>()); NavigatorState navigator = context.ancestorStateOfType(const TypeMatcher<NavigatorState>());
assert(() { assert(() {
if (navigator == null) if (navigator == null) {
throw new WidgetError('openTransaction called with a context that does not include a Navigator. The context passed to the Navigator.openTransaction() method must be that of a widget that is a descendant of a Navigator widget.'); throw new WidgetError(
'openTransaction called with a context that does not include a Navigator.\n'
'The context passed to the Navigator.openTransaction() method must be that of a widget that is a descendant of a Navigator widget.'
);
}
return true; return true;
}); });
navigator.openTransaction(callback); navigator.openTransaction(callback);
......
...@@ -71,7 +71,32 @@ class Overlay extends StatefulComponent { ...@@ -71,7 +71,32 @@ class Overlay extends StatefulComponent {
final List<OverlayEntry> initialEntries; final List<OverlayEntry> initialEntries;
/// The state from the closest instance of this class that encloses the given context. /// The state from the closest instance of this class that encloses the given context.
static OverlayState of(BuildContext context) => context.ancestorStateOfType(const TypeMatcher<OverlayState>()); ///
/// In checked mode, if the [debugRequiredFor] argument is provided then this
/// function will assert that an overlay was found and will throw an exception
/// if not. The exception attempts to explain that the calling [Widget] (the
/// one given by the [debugRequiredFor] argument) needs an [Overlay] to be
/// present to function.
static OverlayState of(BuildContext context, { Widget debugRequiredFor }) {
OverlayState result = context.ancestorStateOfType(const TypeMatcher<OverlayState>());
assert(() {
if (debugRequiredFor != null && result == null) {
String additional = context.widget != debugRequiredFor
? '\nThe context from which that widget was searching for an overlay was:\n $context'
: '';
throw new WidgetError(
'No Overlay widget found.\n'
'${debugRequiredFor.runtimeType} widgets require an Overlay widget ancestor for correct operation.\n'
'The most common way to add an Overlay to an application is to include a MaterialApp or Navigator widget in the runApp() call.\n'
'The specific widget that failed to find an overlay was:\n'
' $debugRequiredFor'
'$additional'
);
}
return true;
});
return result;
}
OverlayState createState() => new OverlayState(); OverlayState createState() => new OverlayState();
} }
......
...@@ -18,6 +18,10 @@ enum ItemsSnapAlignment { ...@@ -18,6 +18,10 @@ enum ItemsSnapAlignment {
adjacentItem adjacentItem
} }
/// Scrollable widget that scrolls one "page" at a time.
///
/// In a pageable list, one child is visible at a time. Scrolling the list
/// reveals either the next or previous child.
class PageableList extends Scrollable { class PageableList extends Scrollable {
PageableList({ PageableList({
Key key, Key key,
...@@ -46,17 +50,34 @@ class PageableList extends Scrollable { ...@@ -46,17 +50,34 @@ class PageableList extends Scrollable {
snapOffsetCallback: snapOffsetCallback snapOffsetCallback: snapOffsetCallback
); );
/// Whether the first item should be revealed after scrolling past the last item.
final bool itemsWrap; final bool itemsWrap;
/// Controls whether a fling always reveals the adjacent item or whether flings can traverse many items.
final ItemsSnapAlignment itemsSnapAlignment; final ItemsSnapAlignment itemsSnapAlignment;
/// Called when the currently visible page changes.
final ValueChanged<int> onPageChanged; final ValueChanged<int> onPageChanged;
/// Used to paint the scrollbar for this list.
final ScrollableListPainter scrollableListPainter; final ScrollableListPainter scrollableListPainter;
/// The duration used when animating to a given page.
final Duration duration; final Duration duration;
/// The animation curve to use when animating to a given page.
final Curve curve; final Curve curve;
/// The list of pages themselves.
final Iterable<Widget> children; final Iterable<Widget> children;
PageableListState createState() => new PageableListState(); PageableListState createState() => new PageableListState();
} }
/// State for a [PageableList] widget.
///
/// Widgets that subclass [PageableList] can subclass this class to have
/// sensible default behaviors for pageable lists.
class PageableListState<T extends PageableList> extends ScrollableState<T> { class PageableListState<T extends PageableList> extends ScrollableState<T> {
int get _itemCount => config.children?.length ?? 0; int get _itemCount => config.children?.length ?? 0;
int _previousItemCount; int _previousItemCount;
......
...@@ -12,6 +12,9 @@ class Placeholder extends StatefulComponent { ...@@ -12,6 +12,9 @@ class Placeholder extends StatefulComponent {
PlaceholderState createState() => new PlaceholderState(); PlaceholderState createState() => new PlaceholderState();
} }
/// State for a [Placeholder] widget.
///
/// Useful for setting the child currently displayed by this placeholder widget.
class PlaceholderState extends State<Placeholder> { class PlaceholderState extends State<Placeholder> {
/// The child that this widget builds. /// The child that this widget builds.
/// ///
......
...@@ -327,15 +327,9 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -327,15 +327,9 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
}); });
PageStorage.of(context)?.writeState(context, _scrollOffset); PageStorage.of(context)?.writeState(context, _scrollOffset);
new ScrollNotification(this, _scrollOffset).dispatch(context); new ScrollNotification(this, _scrollOffset).dispatch(context);
final needsScrollStart = !_isBetweenOnScrollStartAndOnScrollEnd; _startScroll();
if (needsScrollStart) {
dispatchOnScrollStart();
assert(_isBetweenOnScrollStartAndOnScrollEnd);
}
dispatchOnScroll(); dispatchOnScroll();
assert(_isBetweenOnScrollStartAndOnScrollEnd); _endScroll();
if (needsScrollStart)
dispatchOnScrollEnd();
} }
/// Scroll this widget by the given scroll delta. /// Scroll this widget by the given scroll delta.
...@@ -372,8 +366,8 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -372,8 +366,8 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
Future _animateTo(double newScrollOffset, Duration duration, Curve curve) { Future _animateTo(double newScrollOffset, Duration duration, Curve curve) {
_controller.stop(); _controller.stop();
_controller.value = scrollOffset; _controller.value = scrollOffset;
_dispatchOnScrollStartIfNeeded(); _startScroll();
return _controller.animateTo(newScrollOffset, duration: duration, curve: curve).then(_dispatchOnScrollEndIfNeeded); return _controller.animateTo(newScrollOffset, duration: duration, curve: curve).then(_endScroll);
} }
/// Fling the scroll offset with the given velocity. /// Fling the scroll offset with the given velocity.
...@@ -401,8 +395,8 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -401,8 +395,8 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
Simulation simulation = _createSnapSimulation(scrollVelocity) ?? _createFlingSimulation(scrollVelocity); Simulation simulation = _createSnapSimulation(scrollVelocity) ?? _createFlingSimulation(scrollVelocity);
if (simulation == null) if (simulation == null)
return new Future.value(); return new Future.value();
_dispatchOnScrollStartIfNeeded(); _startScroll();
return _controller.animateWith(simulation).then(_dispatchOnScrollEndIfNeeded); return _controller.animateWith(simulation).then(_endScroll);
} }
/// Whether this scrollable should attempt to snap scroll offsets. /// Whether this scrollable should attempt to snap scroll offsets.
...@@ -453,14 +447,21 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -453,14 +447,21 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
return simulation; return simulation;
} }
// When we start an scroll animation, we stop any previous scroll animation.
bool _isBetweenOnScrollStartAndOnScrollEnd = false; // However, the code that would deliver the onScrollEnd callback is watching
// for animations to end using a Future that resolves at the end of the
// microtask. That causes animations to "overlap" between the time we start a
// new animation and the end of the microtask. By the time the microtask is
// over and we check whether to deliver an onScrollEnd callback, we will have
// started the new animation (having skipped the onScrollStart) and therefore
// we won't deliver the onScrollEnd until the second animation is finished.
int _numberOfInProgressScrolls = 0;
/// Calls the onScroll callback. /// Calls the onScroll callback.
/// ///
/// Subclasses can override this function to hook the scroll callback. /// Subclasses can override this function to hook the scroll callback.
void dispatchOnScroll() { void dispatchOnScroll() {
assert(_isBetweenOnScrollStartAndOnScrollEnd); assert(_numberOfInProgressScrolls > 0);
if (config.onScroll != null) if (config.onScroll != null)
config.onScroll(_scrollOffset); config.onScroll(_scrollOffset);
} }
...@@ -470,11 +471,12 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -470,11 +471,12 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
} }
void _handleDragStart(_) { void _handleDragStart(_) {
_dispatchOnScrollStartIfNeeded(); _startScroll();
} }
void _dispatchOnScrollStartIfNeeded() { void _startScroll() {
if (!_isBetweenOnScrollStartAndOnScrollEnd) _numberOfInProgressScrolls += 1;
if (_numberOfInProgressScrolls == 1)
dispatchOnScrollStart(); dispatchOnScrollStart();
} }
...@@ -482,8 +484,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -482,8 +484,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
/// ///
/// Subclasses can override this function to hook the scroll start callback. /// Subclasses can override this function to hook the scroll start callback.
void dispatchOnScrollStart() { void dispatchOnScrollStart() {
assert(!_isBetweenOnScrollStartAndOnScrollEnd); assert(_numberOfInProgressScrolls == 1);
_isBetweenOnScrollStartAndOnScrollEnd = true;
if (config.onScrollStart != null) if (config.onScrollStart != null)
config.onScrollStart(_scrollOffset); config.onScrollStart(_scrollOffset);
} }
...@@ -495,11 +496,12 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -495,11 +496,12 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
Future _handleDragEnd(Velocity velocity) { Future _handleDragEnd(Velocity velocity) {
double scrollVelocity = pixelDeltaToScrollOffset(velocity.pixelsPerSecond) / Duration.MILLISECONDS_PER_SECOND; double scrollVelocity = pixelDeltaToScrollOffset(velocity.pixelsPerSecond) / Duration.MILLISECONDS_PER_SECOND;
// The gesture velocity properties are pixels/second, config min,max limits are pixels/ms // The gesture velocity properties are pixels/second, config min,max limits are pixels/ms
return fling(scrollVelocity.clamp(-kMaxFlingVelocity, kMaxFlingVelocity)).then(_dispatchOnScrollEndIfNeeded); return fling(scrollVelocity.clamp(-kMaxFlingVelocity, kMaxFlingVelocity)).then(_endScroll);
} }
void _dispatchOnScrollEndIfNeeded(_) { void _endScroll([_]) {
if (_isBetweenOnScrollStartAndOnScrollEnd) _numberOfInProgressScrolls -= 1;
if (_numberOfInProgressScrolls == 0)
dispatchOnScrollEnd(); dispatchOnScrollEnd();
} }
...@@ -507,8 +509,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -507,8 +509,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
/// ///
/// Subclasses can override this function to hook the scroll end callback. /// Subclasses can override this function to hook the scroll end callback.
void dispatchOnScrollEnd() { void dispatchOnScrollEnd() {
assert(_isBetweenOnScrollStartAndOnScrollEnd); assert(_numberOfInProgressScrolls == 0);
_isBetweenOnScrollStartAndOnScrollEnd = false;
if (config.onScrollEnd != null) if (config.onScrollEnd != null)
config.onScrollEnd(_scrollOffset); config.onScrollEnd(_scrollOffset);
} }
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
/// The Flutter widget framework. /// The Flutter widget framework.
library widgets; library widgets;
export 'src/widgets/app.dart';
export 'src/widgets/asset_vendor.dart'; export 'src/widgets/asset_vendor.dart';
export 'src/widgets/auto_layout.dart'; export 'src/widgets/auto_layout.dart';
export 'src/widgets/basic.dart'; export 'src/widgets/basic.dart';
......
...@@ -26,7 +26,7 @@ enum EnginePhase { ...@@ -26,7 +26,7 @@ enum EnginePhase {
composite composite
} }
class TestRenderingFlutterBinding extends BindingBase with Scheduler, MojoShell, Renderer, Gesturer { class TestRenderingFlutterBinding extends BindingBase with Scheduler, Services, Renderer, Gesturer {
void initRenderView() { void initRenderView() {
if (renderView == null) { if (renderView == null) {
renderView = new TestRenderView(); renderView = new TestRenderView();
...@@ -58,7 +58,7 @@ void layout(RenderBox box, { BoxConstraints constraints, EnginePhase phase: Engi ...@@ -58,7 +58,7 @@ void layout(RenderBox box, { BoxConstraints constraints, EnginePhase phase: Engi
_renderer ??= new TestRenderingFlutterBinding(); _renderer ??= new TestRenderingFlutterBinding();
renderer.renderView.child = null; renderer.renderView.child = null;
if (constraints != null) { if (constraints != null) {
box = new RenderPositionedBox( box = new RenderPositionedBox(
child: new RenderConstrainedBox( child: new RenderConstrainedBox(
......
...@@ -17,16 +17,15 @@ class TestMultiChildLayoutDelegate extends MultiChildLayoutDelegate { ...@@ -17,16 +17,15 @@ class TestMultiChildLayoutDelegate extends MultiChildLayoutDelegate {
} }
Size performLayoutSize; Size performLayoutSize;
BoxConstraints performLayoutConstraints;
Size performLayoutSize0; Size performLayoutSize0;
Size performLayoutSize1; Size performLayoutSize1;
bool performLayoutIsChild; bool performLayoutIsChild;
void performLayout(Size size, BoxConstraints constraints) { void performLayout(Size size) {
assert(!RenderObject.debugCheckingIntrinsics); assert(!RenderObject.debugCheckingIntrinsics);
expect(() { expect(() {
performLayoutSize = size; performLayoutSize = size;
performLayoutConstraints = constraints; BoxConstraints constraints = new BoxConstraints.loose(size);
performLayoutSize0 = layoutChild(0, constraints); performLayoutSize0 = layoutChild(0, constraints);
performLayoutSize1 = layoutChild(1, constraints); performLayoutSize1 = layoutChild(1, constraints);
performLayoutIsChild = isChild('fred'); performLayoutIsChild = isChild('fred');
...@@ -68,10 +67,6 @@ void main() { ...@@ -68,10 +67,6 @@ void main() {
expect(delegate.performLayoutSize.width, 200.0); expect(delegate.performLayoutSize.width, 200.0);
expect(delegate.performLayoutSize.height, 300.0); expect(delegate.performLayoutSize.height, 300.0);
expect(delegate.performLayoutConstraints.minWidth, 0.0);
expect(delegate.performLayoutConstraints.maxWidth, 800.0);
expect(delegate.performLayoutConstraints.minHeight, 0.0);
expect(delegate.performLayoutConstraints.maxHeight, 600.0);
expect(delegate.performLayoutSize0.width, 150.0); expect(delegate.performLayoutSize0.width, 150.0);
expect(delegate.performLayoutSize0.height, 100.0); expect(delegate.performLayoutSize0.height, 100.0);
expect(delegate.performLayoutSize1.width, 100.0); expect(delegate.performLayoutSize1.width, 100.0);
......
...@@ -95,4 +95,45 @@ void main() { ...@@ -95,4 +95,45 @@ void main() {
expect(log, equals(['scrollstart', 'scroll', 'scroll', 'scrollend'])); expect(log, equals(['scrollstart', 'scroll', 'scroll', 'scrollend']));
}); });
}); });
test('Scroll during animation', () {
testWidgets((WidgetTester tester) {
GlobalKey<ScrollableState> scrollKey = new GlobalKey<ScrollableState>();
List<String> log = <String>[];
tester.pumpWidget(_buildScroller(key: scrollKey, log: log));
expect(log, equals([]));
scrollKey.currentState.scrollTo(100.0, duration: const Duration(seconds: 1));
expect(log, equals(['scrollstart']));
tester.pump(const Duration(milliseconds: 100));
expect(log, equals(['scrollstart']));
tester.pump(const Duration(milliseconds: 100));
expect(log, equals(['scrollstart', 'scroll']));
scrollKey.currentState.scrollTo(100.0, duration: const Duration(seconds: 1));
expect(log, equals(['scrollstart', 'scroll']));
tester.pump(const Duration(milliseconds: 100));
expect(log, equals(['scrollstart', 'scroll']));
tester.pump(const Duration(milliseconds: 1500));
expect(log, equals(['scrollstart', 'scroll', 'scroll', 'scrollend']));
});
});
test('fling, fling generates one start/end pair', () {
testWidgets((WidgetTester tester) {
GlobalKey<ScrollableState> scrollKey = new GlobalKey<ScrollableState>();
List<String> log = <String>[];
tester.pumpWidget(_buildScroller(key: scrollKey, log: log));
expect(log, equals([]));
tester.flingFrom(new Point(100.0, 100.0), new Offset(-50.0, -50.0), 500.0);
tester.pump(new Duration(seconds: 1));
log.removeWhere((String value) => value == 'scroll');
expect(log, equals(['scrollstart']));
tester.flingFrom(new Point(100.0, 100.0), new Offset(-50.0, -50.0), 500.0);
tester.pump(new Duration(seconds: 1));
tester.pump(new Duration(seconds: 1));
log.removeWhere((String value) => value == 'scroll');
expect(log, equals(['scrollstart', 'scrollend']));
});
});
} }
...@@ -78,7 +78,7 @@ class WidgetTester extends Instrumentation { ...@@ -78,7 +78,7 @@ class WidgetTester extends Instrumentation {
super(binding: _SteppedWidgetFlutterBinding.ensureInitialized()) { super(binding: _SteppedWidgetFlutterBinding.ensureInitialized()) {
timeDilation = 1.0; timeDilation = 1.0;
ui.window.onBeginFrame = null; ui.window.onBeginFrame = null;
runApp(new ErrorWidget()); // flush out the last build entirely runApp(new Container(key: new UniqueKey())); // flush out the last build entirely
} }
final FakeAsync async; final FakeAsync async;
......
...@@ -128,21 +128,19 @@ All done! In order to run your application, type: ...@@ -128,21 +128,19 @@ All done! In order to run your application, type:
void _renderTemplates(String projectName, String dirPath, void _renderTemplates(String projectName, String dirPath,
String flutterPackagesDirectory, { bool renderDriverTest: false }) { String flutterPackagesDirectory, { bool renderDriverTest: false }) {
String relativePackagesDirectory = path.relative( new Directory(dirPath).createSync(recursive: true);
flutterPackagesDirectory,
from: path.join(dirPath, 'pubspec.yaml')
);
printStatus('Creating project ${path.basename(projectName)}:'); flutterPackagesDirectory = path.normalize(flutterPackagesDirectory);
flutterPackagesDirectory = _relativePath(from: dirPath, to: flutterPackagesDirectory);
new Directory(dirPath).createSync(recursive: true); printStatus('Creating project ${path.basename(projectName)}:');
Map templateContext = <String, dynamic>{ Map templateContext = <String, dynamic>{
'projectName': projectName, 'projectName': projectName,
'androidIdentifier': _createAndroidIdentifier(projectName), 'androidIdentifier': _createAndroidIdentifier(projectName),
'iosIdentifier': _createUTIIdentifier(projectName), 'iosIdentifier': _createUTIIdentifier(projectName),
'description': description, 'description': description,
'flutterPackagesDirectory': relativePackagesDirectory, 'flutterPackagesDirectory': flutterPackagesDirectory,
'androidMinApiLevel': android.minApiLevel 'androidMinApiLevel': android.minApiLevel
}; };
...@@ -211,3 +209,11 @@ String _validateProjectName(String projectName) { ...@@ -211,3 +209,11 @@ String _validateProjectName(String projectName) {
return null; return null;
} }
String _relativePath({ String from, String to }) {
String result = path.relative(to, from: from);
// `path.relative()` doesn't always return a correct result: dart-lang/path#12.
if (FileSystemEntity.isDirectorySync(path.join(from, result)))
return result;
return to;
}
...@@ -4,20 +4,18 @@ ...@@ -4,20 +4,18 @@
import 'tolerance.dart'; import 'tolerance.dart';
abstract class Simulatable {
/// The current position of the object in the simulation
double x(double time);
/// The current velocity of the object in the simulation
double dx(double time);
}
/// The base class for all simulations. The user is meant to instantiate an /// The base class for all simulations. The user is meant to instantiate an
/// instance of a simulation and query the same for the position and velocity /// instance of a simulation and query the same for the position and velocity
/// of the body at a given interval. /// of the body at a given interval.
abstract class Simulation implements Simulatable { abstract class Simulation {
Tolerance tolerance = toleranceDefault; Tolerance tolerance = toleranceDefault;
/// The current position of the object in the simulation
double x(double time);
/// The current velocity of the object in the simulation
double dx(double time); // TODO(ianh): remove this; see https://github.com/flutter/flutter/issues/2092
/// Returns if the simulation is done at a given time /// Returns if the simulation is done at a given time
bool isDone(double time); bool isDone(double time);
} }
...@@ -10,7 +10,8 @@ import 'utils.dart'; ...@@ -10,7 +10,8 @@ import 'utils.dart';
/// must implement the appropriate methods to select the appropriate simulation /// must implement the appropriate methods to select the appropriate simulation
/// at a given time interval. The simulation group takes care to call the `step` /// at a given time interval. The simulation group takes care to call the `step`
/// method at appropriate intervals. If more fine grained control over the the /// method at appropriate intervals. If more fine grained control over the the
/// step is necessary, subclasses may override `Simulatable` methods. /// step is necessary, subclasses may override the [x], [dx], and [isDone]
/// methods.
abstract class SimulationGroup extends Simulation { abstract class SimulationGroup extends Simulation {
/// The currently active simulation /// The currently active simulation
......
...@@ -7,120 +7,154 @@ import 'dart:math' as math; ...@@ -7,120 +7,154 @@ import 'dart:math' as math;
import 'simulation.dart'; import 'simulation.dart';
import 'utils.dart'; import 'utils.dart';
abstract class _SpringSolution implements Simulatable { enum SpringType { unknown, criticallyDamped, underDamped, overDamped }
factory _SpringSolution(
SpringDescription desc, double initialPosition, double initialVelocity) {
double cmk =
desc.damping * desc.damping - 4 * desc.mass * desc.springConstant;
if (cmk == 0.0) { abstract class _SpringSolution {
factory _SpringSolution(
SpringDescription desc,
double initialPosition,
double initialVelocity
) {
double cmk = desc.damping * desc.damping - 4 * desc.mass * desc.springConstant;
if (cmk == 0.0)
return new _CriticalSolution(desc, initialPosition, initialVelocity); return new _CriticalSolution(desc, initialPosition, initialVelocity);
} else if (cmk > 0.0) { if (cmk > 0.0)
return new _OverdampedSolution(desc, initialPosition, initialVelocity); return new _OverdampedSolution(desc, initialPosition, initialVelocity);
} else { return new _UnderdampedSolution(desc, initialPosition, initialVelocity);
return new _UnderdampedSolution(desc, initialPosition, initialVelocity);
}
return null;
} }
double x(double time);
double dx(double time);
SpringType get type; SpringType get type;
} }
class _CriticalSolution implements _SpringSolution { class _CriticalSolution implements _SpringSolution {
final double _r, _c1, _c2;
factory _CriticalSolution( factory _CriticalSolution(
SpringDescription desc, double distance, double velocity) { SpringDescription desc,
double distance,
double velocity
) {
final double r = -desc.damping / (2.0 * desc.mass); final double r = -desc.damping / (2.0 * desc.mass);
final double c1 = distance; final double c1 = distance;
final double c2 = velocity / (r * distance); final double c2 = velocity / (r * distance);
return new _CriticalSolution.withArgs(r, c1, c2); return new _CriticalSolution.withArgs(r, c1, c2);
} }
SpringType get type => SpringType.criticallyDamped;
_CriticalSolution.withArgs(double r, double c1, double c2) _CriticalSolution.withArgs(double r, double c1, double c2)
: _r = r, : _r = r,
_c1 = c1, _c1 = c1,
_c2 = c2; _c2 = c2;
double x(double time) => (_c1 + _c2 * time) * math.pow(math.E, _r * time); final double _r, _c1, _c2;
double x(double time) {
return (_c1 + _c2 * time) * math.pow(math.E, _r * time);
}
double dx(double time) { double dx(double time) {
final double power = math.pow(math.E, _r * time); final double power = math.pow(math.E, _r * time);
return _r * (_c1 + _c2 * time) * power + _c2 * power; return _r * (_c1 + _c2 * time) * power + _c2 * power;
} }
SpringType get type => SpringType.criticallyDamped;
} }
class _OverdampedSolution implements _SpringSolution { class _OverdampedSolution implements _SpringSolution {
final double _r1, _r2, _c1, _c2;
factory _OverdampedSolution( factory _OverdampedSolution(
SpringDescription desc, double distance, double velocity) { SpringDescription desc,
final double cmk = double distance,
desc.damping * desc.damping - 4 * desc.mass * desc.springConstant; double velocity
) {
final double cmk = desc.damping * desc.damping - 4 * desc.mass * desc.springConstant;
final double r1 = (-desc.damping - math.sqrt(cmk)) / (2.0 * desc.mass); final double r1 = (-desc.damping - math.sqrt(cmk)) / (2.0 * desc.mass);
final double r2 = (-desc.damping + math.sqrt(cmk)) / (2.0 * desc.mass); final double r2 = (-desc.damping + math.sqrt(cmk)) / (2.0 * desc.mass);
final double c2 = (velocity - r1 * distance) / (r2 - r1); final double c2 = (velocity - r1 * distance) / (r2 - r1);
final double c1 = distance - c2; final double c1 = distance - c2;
return new _OverdampedSolution.withArgs(r1, r2, c1, c2); return new _OverdampedSolution.withArgs(r1, r2, c1, c2);
} }
_OverdampedSolution.withArgs(double r1, double r2, double c1, double c2) _OverdampedSolution.withArgs(double r1, double r2, double c1, double c2)
: _r1 = r1, : _r1 = r1,
_r2 = r2, _r2 = r2,
_c1 = c1, _c1 = c1,
_c2 = c2; _c2 = c2;
SpringType get type => SpringType.overDamped; final double _r1, _r2, _c1, _c2;
double x(double time) => double x(double time) {
(_c1 * math.pow(math.E, _r1 * time) + _c2 * math.pow(math.E, _r2 * time)); return _c1 * math.pow(math.E, _r1 * time) +
_c2 * math.pow(math.E, _r2 * time);
}
double dx(double time) {
return _c1 * _r1 * math.pow(math.E, _r1 * time) +
_c2 * _r2 * math.pow(math.E, _r2 * time);
}
double dx(double time) => (_c1 * _r1 * math.pow(math.E, _r1 * time) + SpringType get type => SpringType.overDamped;
_c2 * _r2 * math.pow(math.E, _r2 * time));
} }
class _UnderdampedSolution implements _SpringSolution { class _UnderdampedSolution implements _SpringSolution {
final double _w, _r, _c1, _c2;
factory _UnderdampedSolution( factory _UnderdampedSolution(
SpringDescription desc, double distance, double velocity) { SpringDescription desc,
double distance,
double velocity
) {
final double w = math.sqrt(4.0 * desc.mass * desc.springConstant - final double w = math.sqrt(4.0 * desc.mass * desc.springConstant -
desc.damping * desc.damping) / desc.damping * desc.damping) / (2.0 * desc.mass);
(2.0 * desc.mass);
final double r = -(desc.damping / 2.0 * desc.mass); final double r = -(desc.damping / 2.0 * desc.mass);
final double c1 = distance; final double c1 = distance;
final double c2 = (velocity - r * distance) / w; final double c2 = (velocity - r * distance) / w;
return new _UnderdampedSolution.withArgs(w, r, c1, c2); return new _UnderdampedSolution.withArgs(w, r, c1, c2);
} }
_UnderdampedSolution.withArgs(double w, double r, double c1, double c2) _UnderdampedSolution.withArgs(double w, double r, double c1, double c2)
: _w = w, : _w = w,
_r = r, _r = r,
_c1 = c1, _c1 = c1,
_c2 = c2; _c2 = c2;
SpringType get type => SpringType.underDamped; final double _w, _r, _c1, _c2;
double x(double time) => math.pow(math.E, _r * time) * double x(double time) {
(_c1 * math.cos(_w * time) + _c2 * math.sin(_w * time)); return math.pow(math.E, _r * time) *
(_c1 * math.cos(_w * time) + _c2 * math.sin(_w * time));
}
double dx(double time) { double dx(double time) {
final double power = math.pow(math.E, _r * time); final double power = math.pow(math.E, _r * time);
final double cosine = math.cos(_w * time); final double cosine = math.cos(_w * time);
final double sine = math.sin(_w * time); final double sine = math.sin(_w * time);
return power * (_c2 * _w * cosine - _c1 * _w * sine) +
return power * (_c2 * _w * cosine - _c1 * _w * sine) + _r * power * (_c2 * sine + _c1 * cosine);
_r * power * (_c2 * sine + _c1 * cosine);
} }
SpringType get type => SpringType.underDamped;
} }
class SpringDescription { class SpringDescription {
SpringDescription({
this.mass,
this.springConstant,
this.damping
}) {
assert(mass != null);
assert(springConstant != null);
assert(damping != null);
}
/// Create a spring given the mass, spring constant and the damping ratio. The
/// damping ratio is especially useful trying to determing the type of spring
/// to create. A ratio of 1.0 creates a critically damped spring, > 1.0
/// creates an overdamped spring and < 1.0 an underdamped one.
SpringDescription.withDampingRatio({
double mass,
double springConstant,
double ratio: 1.0
}) : mass = mass,
springConstant = springConstant,
damping = ratio * 2.0 * math.sqrt(mass * springConstant);
/// The mass of the spring (m) /// The mass of the spring (m)
final double mass; final double mass;
...@@ -131,41 +165,23 @@ class SpringDescription { ...@@ -131,41 +165,23 @@ class SpringDescription {
/// Not to be confused with the damping ratio. Use the separate /// Not to be confused with the damping ratio. Use the separate
/// constructor provided for this purpose /// constructor provided for this purpose
final double damping; final double damping;
SpringDescription(
{ this.mass, this.springConstant, this.damping }
) {
assert(mass != null);
assert(springConstant != null);
assert(damping != null);
}
/// Create a spring given the mass, spring constant and the damping ratio. The
/// damping ratio is especially useful trying to determing the type of spring
/// to create. A ratio of 1.0 creates a critically damped spring, > 1.0
/// creates an overdamped spring and < 1.0 an underdamped one.
SpringDescription.withDampingRatio(
{double mass, double springConstant, double ratio: 1.0})
: mass = mass,
springConstant = springConstant,
damping = ratio * 2.0 * math.sqrt(mass * springConstant);
} }
enum SpringType { unknown, criticallyDamped, underDamped, overDamped, }
/// Creates a spring simulation. Depending on the spring description, a /// Creates a spring simulation. Depending on the spring description, a
/// critically, under or overdamped spring will be created. /// critically, under or overdamped spring will be created.
class SpringSimulation extends Simulation { class SpringSimulation extends Simulation {
final double _endPosition;
final _SpringSolution _solution;
/// A spring description with the provided spring description, start distance, /// A spring description with the provided spring description, start distance,
/// end distance and velocity. /// end distance and velocity.
SpringSimulation( SpringSimulation(
SpringDescription desc, double start, double end, double velocity) SpringDescription desc,
: this._endPosition = end, double start,
_solution = new _SpringSolution(desc, start - end, velocity); double end,
double velocity
) : _endPosition = end,
_solution = new _SpringSolution(desc, start - end, velocity);
final double _endPosition;
final _SpringSolution _solution;
SpringType get type => _solution.type; SpringType get type => _solution.type;
...@@ -182,8 +198,12 @@ class SpringSimulation extends Simulation { ...@@ -182,8 +198,12 @@ class SpringSimulation extends Simulation {
/// A SpringSimulation where the value of x() is guaranteed to have exactly the /// A SpringSimulation where the value of x() is guaranteed to have exactly the
/// end value when the simulation isDone(). /// end value when the simulation isDone().
class ScrollSpringSimulation extends SpringSimulation { class ScrollSpringSimulation extends SpringSimulation {
ScrollSpringSimulation(SpringDescription desc, double start, double end, double velocity) ScrollSpringSimulation(
: super(desc, start, end, velocity); SpringDescription desc,
double start,
double end,
double velocity
) : super(desc, start, end, velocity);
double x(double time) => isDone(time) ? _endPosition : super.x(time); double x(double time) => isDone(time) ? _endPosition : super.x(time);
} }
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