Commit 8a900f90 authored by Ian Hickson's avatar Ian Hickson Committed by Hixie

Track scroll position

- Change RouteArguments to pass the route's BuildContext rather than
  the Navigator. This caused the bulk of the examples/ and .../test/
  changes (those are mostly mechanical changes). It also meant I could
  simplify Navigator.of().

- Make initState() actually get called when the State's Element is in
  the tree, so you can use Foo.of() functions there. Added a test for
  this also.

- Provide a RouteWidget so that routes have a position in the Widget
  tree. The bulk of the route logic is still in a longer-lived Route
  object for now.

- Make Route.setState() only rebuild the actual route, not the whole
  navigator.

- Provided a Route.of().

- Provided a Route.writeState / Route.readState API that tries to
  identify the clients by their runtimeType, their key, and their
  ancestors keys, up to the nearest ancestor with a GlobalKey.

- Made scrollables hook into this API to track state. Added a test to
  make sure this works.

- Fix the debug output of GestureDetector and the hashCode of
  MixedViewport.

- Fixed ScrollableWidgetListState<T> to handle infinite lists.
parent e77cad81
......@@ -44,9 +44,8 @@ class DialogMenuItem extends StatelessComponent {
}
class FeedFragment extends StatefulComponent {
FeedFragment({ this.navigator, this.userData, this.onItemCreated, this.onItemDeleted });
FeedFragment({ this.userData, this.onItemCreated, this.onItemDeleted });
final NavigatorState navigator;
final UserData userData;
final FitnessItemHandler onItemCreated;
final FitnessItemHandler onItemDeleted;
......@@ -62,7 +61,7 @@ class FeedFragmentState extends State<FeedFragment> {
setState(() {
_fitnessMode = value;
});
config.navigator.pop();
Navigator.of(context).pop();
}
void _showDrawer() {
......@@ -93,8 +92,8 @@ class FeedFragmentState extends State<FeedFragment> {
}
void _handleShowSettings() {
config.navigator.pop();
config.navigator.pushNamed('/settings');
Navigator.of(context)..pop()
..pushNamed('/settings');
}
// TODO(jackson): We should be localizing
......@@ -122,7 +121,7 @@ class FeedFragmentState extends State<FeedFragment> {
content: new Text("Item deleted."),
actions: <SnackBarAction>[new SnackBarAction(label: "UNDO", onPressed: () {
config.onItemCreated(item);
config.navigator.pop();
Navigator.of(context).pop();
})]
);
}
......@@ -193,7 +192,7 @@ class FeedFragmentState extends State<FeedFragment> {
void _handleActionButtonPressed() {
showDialog(context: context, child: new AddItemDialog()).then((routeName) {
if (routeName != null)
config.navigator.pushNamed(routeName);
Navigator.of(context).pushNamed(routeName);
});
}
......
......@@ -135,7 +135,6 @@ class FitnessAppState extends State<FitnessApp> {
routes: <String, RouteBuilder>{
'/': (RouteArguments args) {
return new FeedFragment(
navigator: args.navigator,
userData: _userData,
onItemCreated: _handleItemCreated,
onItemDeleted: _handleItemDeleted
......@@ -143,19 +142,16 @@ class FitnessAppState extends State<FitnessApp> {
},
'/meals/new': (RouteArguments args) {
return new MealFragment(
navigator: args.navigator,
onCreated: _handleItemCreated
);
},
'/measurements/new': (RouteArguments args) {
return new MeasurementFragment(
navigator: args.navigator,
onCreated: _handleItemCreated
);
},
'/settings': (RouteArguments args) {
return new SettingsFragment(
navigator: args.navigator,
userData: _userData,
updater: settingsUpdater
);
......
......@@ -43,9 +43,8 @@ class MealRow extends FitnessItemRow {
}
class MealFragment extends StatefulComponent {
MealFragment({ this.navigator, this.onCreated });
MealFragment({ this.onCreated });
NavigatorState navigator;
FitnessItemHandler onCreated;
MealFragmentState createState() => new MealFragmentState();
......@@ -56,14 +55,14 @@ class MealFragmentState extends State<MealFragment> {
void _handleSave() {
config.onCreated(new Meal(when: new DateTime.now(), description: _description));
config.navigator.pop();
Navigator.of(context).pop();
}
Widget buildToolBar() {
return new ToolBar(
left: new IconButton(
icon: "navigation/close",
onPressed: config.navigator.pop),
onPressed: Navigator.of(context).pop),
center: new Text('New Meal'),
right: <Widget>[
// TODO(abarth): Should this be a FlatButton?
......
......@@ -104,9 +104,8 @@ class MeasurementDateDialogState extends State<MeasurementDateDialog> {
}
class MeasurementFragment extends StatefulComponent {
MeasurementFragment({ this.navigator, this.onCreated });
MeasurementFragment({ this.onCreated });
final NavigatorState navigator;
final FitnessItemHandler onCreated;
MeasurementFragmentState createState() => new MeasurementFragmentState();
......@@ -131,14 +130,14 @@ class MeasurementFragmentState extends State<MeasurementFragment> {
);
}
config.onCreated(new Measurement(when: _when, weight: parsedWeight));
config.navigator.pop();
Navigator.of(context).pop();
}
Widget buildToolBar() {
return new ToolBar(
left: new IconButton(
icon: "navigation/close",
onPressed: config.navigator.pop),
onPressed: Navigator.of(context).pop),
center: new Text('New Measurement'),
right: <Widget>[
// TODO(abarth): Should this be a FlatButton?
......
......@@ -10,9 +10,8 @@ typedef void SettingsUpdater({
});
class SettingsFragment extends StatefulComponent {
SettingsFragment({ this.navigator, this.userData, this.updater });
SettingsFragment({ this.userData, this.updater });
final NavigatorState navigator;
final UserData userData;
final SettingsUpdater updater;
......@@ -29,7 +28,7 @@ class SettingsFragmentState extends State<SettingsFragment> {
return new ToolBar(
left: new IconButton(
icon: "navigation/arrow_back",
onPressed: config.navigator.pop
onPressed: () => Navigator.of(context).pop()
),
center: new Text('Settings')
);
......@@ -48,7 +47,7 @@ class SettingsFragmentState extends State<SettingsFragment> {
void _handleGoalWeightChanged(String goalWeight) {
// TODO(jackson): Looking for null characters to detect enter key is a hack
if (goalWeight.endsWith("\u{0}")) {
config.navigator.pop(double.parse(goalWeight.replaceAll("\u{0}", "")));
Navigator.of(context).pop(double.parse(goalWeight.replaceAll("\u{0}", "")));
} else {
setState(() {
try {
......
......@@ -108,10 +108,10 @@ class GameDemoState extends State<GameDemo> {
_sounds,
(int lastScore) {
setState(() { _lastScore = lastScore; });
args.navigator.pop();
Navigator.of(args.context).pop();
}
);
args.navigator.pushNamed('/game');
Navigator.of(args.context).pushNamed('/game');
},
texture: _spriteSheetUI['btn_play_up.png'],
textureDown: _spriteSheetUI['btn_play_down.png'],
......
......@@ -92,8 +92,8 @@ class StocksAppState extends State<StocksApp> {
title: 'Stocks',
theme: theme,
routes: <String, RouteBuilder>{
'/': (RouteArguments args) => new StockHome(args.navigator, _stocks, _symbols, _optimismSetting, modeUpdater),
'/settings': (RouteArguments args) => new StockSettings(args.navigator, _optimismSetting, _backupSetting, settingsUpdater)
'/': (RouteArguments args) => new StockHome(_stocks, _symbols, _optimismSetting, modeUpdater),
'/settings': (RouteArguments args) => new StockSettings(_optimismSetting, _backupSetting, settingsUpdater)
},
onGenerateRoute: _getRoute
);
......
......@@ -7,9 +7,8 @@ part of stocks;
typedef void ModeUpdater(StockMode mode);
class StockHome extends StatefulComponent {
StockHome(this.navigator, this.stocks, this.symbols, this.stockMode, this.modeUpdater);
StockHome(this.stocks, this.symbols, this.stockMode, this.modeUpdater);
final NavigatorState navigator;
final Map<String, Stock> stocks;
final List<String> symbols;
final StockMode stockMode;
......@@ -25,7 +24,7 @@ class StockHomeState extends State<StockHome> {
String _searchQuery;
void _handleSearchBegin() {
config.navigator.pushState(this, (_) {
Navigator.of(context).pushState(this, (_) {
setState(() {
_isSearching = false;
_searchQuery = null;
......@@ -38,10 +37,10 @@ class StockHomeState extends State<StockHome> {
void _handleSearchEnd() {
assert(() {
final StateRoute currentRoute = config.navigator.currentRoute;
final StateRoute currentRoute = Navigator.of(context).currentRoute;
return currentRoute.owner == this;
});
config.navigator.pop();
Navigator.of(context).pop();
}
void _handleSearchQueryChanged(String query) {
......@@ -92,13 +91,13 @@ class StockHomeState extends State<StockHome> {
new FlatButton(
child: new Text('USE IT'),
onPressed: () {
config.navigator.pop(false);
Navigator.of(context).pop(false);
}
),
new FlatButton(
child: new Text('OH WELL'),
onPressed: () {
config.navigator.pop(false);
Navigator.of(context).pop(false);
}
),
]
......@@ -142,8 +141,8 @@ class StockHomeState extends State<StockHome> {
}
void _handleShowSettings() {
config.navigator.pop();
config.navigator.pushNamed('/settings');
Navigator.of(context)..pop()
..pushNamed('/settings');
}
Widget buildToolBar() {
......@@ -193,7 +192,7 @@ class StockHomeState extends State<StockHome> {
onOpen: (Stock stock, Key arrowKey) {
Set<Key> mostValuableKeys = new Set<Key>();
mostValuableKeys.add(arrowKey);
config.navigator.pushNamed('/stock/${stock.symbol}', mostValuableKeys: mostValuableKeys);
Navigator.of(context).pushNamed('/stock/${stock.symbol}', mostValuableKeys: mostValuableKeys);
}
);
}
......@@ -239,7 +238,7 @@ class StockHomeState extends State<StockHome> {
}
void _handleUndo() {
config.navigator.pop();
Navigator.of(context).pop();
}
void _handleStockPurchased() {
......
......@@ -10,9 +10,8 @@ typedef void SettingsUpdater({
});
class StockSettings extends StatefulComponent {
const StockSettings(this.navigator, this.optimism, this.backup, this.updater);
const StockSettings(this.optimism, this.backup, this.updater);
final NavigatorState navigator;
final StockMode optimism;
final BackupMode backup;
final SettingsUpdater updater;
......@@ -41,19 +40,19 @@ class StockSettingsState extends State<StockSettings> {
title: new Text("Change mode?"),
content: new Text("Optimistic mode means everything is awesome. Are you sure you can handle that?"),
onDismiss: () {
config.navigator.pop(false);
Navigator.of(context).pop(false);
},
actions: <Widget>[
new FlatButton(
child: new Text('NO THANKS'),
onPressed: () {
config.navigator.pop(false);
Navigator.of(context).pop(false);
}
),
new FlatButton(
child: new Text('AGREE'),
onPressed: () {
config.navigator.pop(true);
Navigator.of(context).pop(true);
}
),
]
......@@ -75,7 +74,7 @@ class StockSettingsState extends State<StockSettings> {
return new ToolBar(
left: new IconButton(
icon: 'navigation/arrow_back',
onPressed: config.navigator.pop
onPressed: () => Navigator.of(context).pop()
),
center: new Text('Settings')
);
......
......@@ -65,8 +65,7 @@ class Dot extends StatelessComponent {
}
class ExampleDragSource extends StatelessComponent {
ExampleDragSource({ Key key, this.navigator, this.name, this.color }) : super(key: key);
final NavigatorState navigator;
ExampleDragSource({ Key key, this.name, this.color }) : super(key: key);
final String name;
final Color color;
......@@ -75,7 +74,6 @@ class ExampleDragSource extends StatelessComponent {
Widget build(BuildContext context) {
return new Draggable(
navigator: navigator,
data: new DragData(name),
child: new Dot(color: color, size: kDotSize),
feedback: new Transform(
......@@ -91,13 +89,7 @@ class ExampleDragSource extends StatelessComponent {
}
}
class DragAndDropApp extends StatefulComponent {
DragAndDropApp({ this.navigator });
final NavigatorState navigator;
DragAndDropAppState createState() => new DragAndDropAppState();
}
class DragAndDropAppState extends State<DragAndDropApp> {
class DragAndDropApp extends StatelessComponent {
Widget build(BuildContext context) {
return new Scaffold(
toolBar: new ToolBar(
......@@ -107,9 +99,9 @@ class DragAndDropAppState extends State<DragAndDropApp> {
style: Theme.of(context).text.body1.copyWith(textAlign: TextAlign.center),
child: new Column(<Widget>[
new Flexible(child: new Row(<Widget>[
new ExampleDragSource(navigator: config.navigator, name: 'Orange', color: const Color(0xFFFF9000)),
new ExampleDragSource(navigator: config.navigator, name: 'Teal', color: const Color(0xFF00FFFF)),
new ExampleDragSource(navigator: config.navigator, name: 'Yellow', color: const Color(0xFFFFF000)),
new ExampleDragSource(name: 'Orange', color: const Color(0xFFFF9000)),
new ExampleDragSource(name: 'Teal', color: const Color(0xFF00FFFF)),
new ExampleDragSource(name: 'Yellow', color: const Color(0xFFFFF000)),
],
alignItems: FlexAlignItems.center,
justifyContent: FlexJustifyContent.spaceAround
......@@ -130,7 +122,7 @@ void main() {
runApp(new MaterialApp(
title: 'Drag and Drop Flutter Demo',
routes: <String, RouteBuilder>{
'/': (RouteArguments args) => new DragAndDropApp(navigator: args.navigator)
'/': (RouteArguments args) => new DragAndDropApp()
}
));
}
......@@ -12,11 +12,11 @@ final Map<String, RouteBuilder> routes = <String, RouteBuilder>{
new Text("You are at home"),
new RaisedButton(
child: new Text('GO SHOPPING'),
onPressed: () => args.navigator.pushNamed('/shopping')
onPressed: () => Navigator.of(args.context).pushNamed('/shopping')
),
new RaisedButton(
child: new Text('START ADVENTURE'),
onPressed: () => args.navigator.pushNamed('/adventure')
onPressed: () => Navigator.of(args.context).pushNamed('/adventure')
)],
justifyContent: FlexJustifyContent.center
)
......@@ -28,11 +28,11 @@ final Map<String, RouteBuilder> routes = <String, RouteBuilder>{
new Text("Village Shop"),
new RaisedButton(
child: new Text('RETURN HOME'),
onPressed: () => args.navigator.pop()
onPressed: () => Navigator.of(args.context).pop()
),
new RaisedButton(
child: new Text('GO TO DUNGEON'),
onPressed: () => args.navigator.pushNamed('/adventure')
onPressed: () => Navigator.of(args.context).pushNamed('/adventure')
)],
justifyContent: FlexJustifyContent.center
)
......@@ -44,7 +44,7 @@ final Map<String, RouteBuilder> routes = <String, RouteBuilder>{
new Text("Monster's Lair"),
new RaisedButton(
child: new Text('RUN!!!'),
onPressed: () => args.navigator.pop()
onPressed: () => Navigator.of(args.context).pop()
)],
justifyContent: FlexJustifyContent.center
)
......
......@@ -26,7 +26,7 @@ class IconTheme extends InheritedWidget {
bool updateShouldNotify(IconTheme old) => data != old.data;
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
super.debugFillDescription(description);
description.add('$data');
}
}
......@@ -37,19 +37,16 @@ enum DragAnchor {
class Draggable extends StatefulComponent {
Draggable({
Key key,
this.navigator,
this.data,
this.child,
this.feedback,
this.feedbackOffset: Offset.zero,
this.dragAnchor: DragAnchor.child
}) : super(key: key) {
assert(navigator != null);
assert(child != null);
assert(feedback != null);
}
final NavigatorState navigator;
final dynamic data;
final Widget child;
final Widget feedback;
......@@ -91,12 +88,12 @@ class _DraggableState extends State<Draggable> {
}
);
_route.update(point);
config.navigator.push(_route);
Navigator.of(context).push(_route);
}
void _updateDrag(PointerInputEvent event) {
if (_route != null) {
config.navigator.setState(() {
Navigator.of(context).setState(() {
_route.update(new Point(event.x, event.y));
});
}
......@@ -104,7 +101,7 @@ class _DraggableState extends State<Draggable> {
void _cancelDrag(PointerInputEvent event) {
if (_route != null) {
config.navigator.popRoute(_route, DragEndKind.canceled);
Navigator.of(context).popRoute(_route, DragEndKind.canceled);
assert(_route == null);
}
}
......@@ -112,7 +109,7 @@ class _DraggableState extends State<Draggable> {
void _drop(PointerInputEvent event) {
if (_route != null) {
_route.update(new Point(event.x, event.y));
config.navigator.popRoute(_route, DragEndKind.dropped);
Navigator.of(context).popRoute(_route, DragEndKind.dropped);
assert(_route == null);
}
}
......
......@@ -111,8 +111,9 @@ class Focus extends StatefulComponent {
return true;
}
// Don't call moveTo() from your build() function, it's intended to be called
// from event listeners, e.g. in response to a finger tap or tab key.
// Don't call moveTo() and moveScopeTo() from your build()
// functions, it's intended to be called from event listeners, e.g.
// in response to a finger tap or tab key.
static void moveTo(BuildContext context, Widget widget) {
assert(widget != null);
......@@ -122,7 +123,7 @@ class Focus extends StatefulComponent {
focusScope.focusState._setFocusedWidget(widget.key);
}
static void _moveScopeTo(BuildContext context, Focus component) {
static void moveScopeTo(BuildContext context, Focus component) {
assert(component != null);
assert(component.key != null);
_FocusScope focusScope = context.inheritedWidgetOfType(_FocusScope);
......@@ -209,8 +210,6 @@ class FocusState extends State<Focus> {
void initState() {
super.initState();
if (config.autofocus)
Focus._moveScopeTo(context, config);
_updateWidgetRemovalListener(_focusedWidget);
_updateScopeRemovalListener(_focusedScope);
}
......
......@@ -1023,10 +1023,14 @@ abstract class ComponentElement<T extends Widget> extends BuildableElement<T> {
super.mount(parent, newSlot);
assert(_child == null);
assert(_active);
rebuild();
_firstBuild();
assert(_child != null);
}
void _firstBuild() {
rebuild();
}
/// Reinvokes the build() method of the StatelessComponent object (for
/// stateless components) or the State object (for stateful components) and
/// then updates the widget tree.
......@@ -1098,6 +1102,13 @@ class StatefulComponentElement<T extends StatefulComponent, U extends State<T>>
assert(_state._config == null);
_state._config = widget;
assert(_state._debugLifecycleState == _StateLifecycle.created);
}
U get state => _state;
U _state;
void _firstBuild() {
assert(_state._debugLifecycleState == _StateLifecycle.created);
try {
_debugSetAllowIgnoredCallsToMarkNeedsBuild(true);
_state.initState();
......@@ -1111,11 +1122,9 @@ class StatefulComponentElement<T extends StatefulComponent, U extends State<T>>
return false;
});
assert(() { _state._debugLifecycleState = _StateLifecycle.ready; return true; });
super._firstBuild();
}
U get state => _state;
U _state;
void update(T newWidget) {
super.update(newWidget);
assert(widget == newWidget);
......
......@@ -245,6 +245,7 @@ class _GestureDetectorState extends State<GestureDetector> {
}
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
List<String> gestures = <String>[];
if (_tap != null)
gestures.add('tap');
......@@ -262,7 +263,7 @@ class _GestureDetectorState extends State<GestureDetector> {
gestures.add('pan');
if (_scale != null)
gestures.add('scale');
if (gestures.isEmpty);
if (gestures.isEmpty)
gestures.add('<none>');
description.add('gestures: ${gestures.join(", ")}');
}
......
......@@ -63,7 +63,7 @@ class _ChildKey {
return type == typedOther.type &&
key == typedOther.key;
}
int get hashCode => 373 * 37 * type.hashCode + key.hashCode;
int get hashCode => ((373 * 37) + type.hashCode) * 37 + key.hashCode;
String toString() => "_ChildKey(type: $type, key: $key)";
}
......
......@@ -21,8 +21,8 @@ Color debugGridColor = const Color(0x7F7F2020);
const String kDefaultRouteName = '/';
class RouteArguments {
const RouteArguments(this.navigator, { this.previousPerformance, this.nextPerformance });
final NavigatorState navigator;
const RouteArguments({ this.context, this.previousPerformance, this.nextPerformance });
final BuildContext context;
final PerformanceView previousPerformance;
final PerformanceView nextPerformance;
}
......@@ -49,17 +49,13 @@ class Navigator extends StatefulComponent {
static NavigatorState of(BuildContext context) {
NavigatorState result;
bool visitor(Element element) {
if (element is StatefulComponentElement) {
if (element.state is NavigatorState) {
result = element.state;
return false;
}
context.visitAncestorElements((Element element) {
if (element is StatefulComponentElement && element.state is NavigatorState) {
result = element.state;
return false;
}
return true;
}
if (visitor(context))
context.visitAncestorElements(visitor);
});
return result;
}
......@@ -268,9 +264,9 @@ class NavigatorState extends State<Navigator> {
if (_desiredHeroes.hasInstructions) {
if ((_desiredHeroes.to == route || _desiredHeroes.from == route) && nextHeroPerformance == null)
nextHeroPerformance = route.performance;
visibleRoutes.add(route._internalBuild(nextContentRoute, buildTargetHeroes: _desiredHeroes.to == route));
visibleRoutes.add(new _RouteWidget(route: route, nextRoute: nextContentRoute, buildTargetHeroes: _desiredHeroes.to == route));
} else {
visibleRoutes.add(route._internalBuild(nextContentRoute));
visibleRoutes.add(new _RouteWidget(route: route, nextRoute: nextContentRoute));
}
if (route.isActuallyOpaque) {
assert(!_desiredHeroes.hasInstructions ||
......@@ -330,6 +326,82 @@ class NavigatorState extends State<Navigator> {
}
class _RouteWidget extends StatefulComponent {
_RouteWidget({
Route route,
this.nextRoute,
this.buildTargetHeroes: false
}) : route = route,
super(key: new ObjectKey(route)) {
assert(route != null);
}
final Route route;
final Route nextRoute;
final bool buildTargetHeroes;
_RouteWidgetState createState() => new _RouteWidgetState();
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
if (route.performance != null)
description.add('${route.performance}');
else
description.add('${route.debugLabel}');
if (buildTargetHeroes)
description.add('building target heroes this frame');
}
}
class _RouteWidgetState extends State<_RouteWidget> {
void initState() {
super.initState();
config.route._widgetState = this;
}
void dispose() {
config.route._widgetState = null;
super.dispose();
}
Widget build(BuildContext context) {
return config.route._internalBuild(context, config.nextRoute, buildTargetHeroes: config.buildTargetHeroes);
}
}
class _StorageEntryIdentifier {
Type clientType;
List<Key> keys;
void addKey(Key key) {
assert(key != null);
assert(key is! GlobalKey);
keys ??= <Key>[];
keys.add(key);
}
GlobalKey scopeKey;
bool operator ==(dynamic other) {
if (other is! _StorageEntryIdentifier)
return false;
final _StorageEntryIdentifier typedOther = other;
if (clientType != typedOther.clientType ||
scopeKey != typedOther.scopeKey ||
keys?.length != typedOther.keys?.length)
return false;
if (keys != null) {
for (int index = 0; index < keys.length; index += 1) {
if (keys[index] != typedOther.keys[index])
return false;
}
}
return true;
}
int get hashCode {
int value = 373;
value = 37 * value + clientType.hashCode;
value = 37 * value + scopeKey.hashCode;
if (keys != null) {
for (Key key in keys)
value = 37 * value + key.hashCode;
}
return value;
}
}
abstract class Route {
Route() {
_subtreeKey = new GlobalKey(label: debugLabel);
......@@ -390,10 +462,13 @@ abstract class Route {
NavigatorState get navigator => _navigator;
NavigatorState _navigator;
_RouteWidgetState _widgetState;
void setState(void fn()) {
assert(navigator != null);
navigator.setState(fn);
if (_widgetState != null)
_widgetState.setState(fn);
else
fn();
}
void didPush(NavigatorState navigator) {
......@@ -419,18 +494,21 @@ abstract class Route {
}
}
/// Called by the navigator.build() function if hasContent is true, to get the
/// subtree for this route.
/// Called (indirectly, via a RouteWidget) by the navigator.build()
/// function if hasContent is true, to get the subtree for this
/// route.
///
/// If buildTargetHeroes is true, then getHeroesToAnimate() will be called
/// after this build, before the next build, and this build should render the
/// route off-screen, at the end of its animation. Next frame, the argument
/// will be false, and the tree should be built at the first frame of the
/// transition animation, whatever that is.
Widget _internalBuild(Route nextRoute, { bool buildTargetHeroes: false }) {
Widget _internalBuild(BuildContext context, Route nextRoute, { bool buildTargetHeroes: false }) {
assert(navigator != null);
assert(_widgetState != null);
assert(hasContent);
return keySubtree(build(new RouteArguments(
navigator,
context: context,
previousPerformance: performance,
nextPerformance: nextRoute?.performance
)));
......@@ -446,12 +524,12 @@ abstract class Route {
Map<Object, HeroHandle> getHeroesToAnimate([Set<Key> mostValuableKeys]) => const <Object, HeroHandle>{};
bool _hasActiveHeroes = false;
GlobalKey _subtreeKey;
/// Returns the BuildContext for the root of the subtree built for this route,
/// assuming that internalBuild used keySubtree to build that subtree.
/// This is only valid after a build phase.
BuildContext get context => _subtreeKey.currentContext;
GlobalKey _subtreeKey;
BuildContext get subtreeContext => _subtreeKey.currentContext;
/// Wraps the given subtree in a route-specific GlobalKey.
Widget keySubtree(Widget child) {
......@@ -465,6 +543,52 @@ abstract class Route {
/// change what subtree is built for this route.
Widget build(RouteArguments args);
static Route of(BuildContext context) {
Route result;
context.visitAncestorElements((Element element) {
if (element is StatefulComponentElement && element.state is _RouteWidgetState) {
result = element.widget.route;
return false;
}
return true;
});
return result;
}
_StorageEntryIdentifier _computeStorageIdentifier(BuildContext context) {
_StorageEntryIdentifier result = new _StorageEntryIdentifier();
result.clientType = context.widget.runtimeType;
Key lastKey = context.widget.key;
if (lastKey is! GlobalKey) {
context.visitAncestorElements((Element element) {
if (element.widget.key is GlobalKey) {
lastKey = element.widget.key;
return false;
} else if (element.widget is Navigator) {
// Not quite everyone who is in a Navigator actually is in a Route.
// For example, the modal barrier.
StatefulComponentElement statefulElement = element;
lastKey = new GlobalObjectKey(statefulElement.state);
return false;
} else if (element.widget.key != null) {
result.addKey(element.widget.key);
}
return true;
});
return result;
}
assert(lastKey is GlobalKey);
result.scopeKey = lastKey;
return result;
}
Map<_StorageEntryIdentifier, dynamic> _storage;
void writeState(BuildContext context, dynamic data) {
_storage ??= <_StorageEntryIdentifier, dynamic>{};
_storage[_computeStorageIdentifier(context)] = data;
}
dynamic readState(BuildContext context) => _storage != null ? _storage[_computeStorageIdentifier(context)] : null;
String get debugLabel => '$runtimeType';
String toString() => '$runtimeType(performance: $performance; key: $_subtreeKey)';
......@@ -487,7 +611,7 @@ abstract class PerformanceRoute extends Route {
Duration get transitionDuration;
Widget _internalBuild(Route nextRoute, { bool buildTargetHeroes: false }) {
Widget _internalBuild(BuildContext context, Route nextRoute, { bool buildTargetHeroes: false }) {
assert(hasContent);
assert(transitionDuration > Duration.ZERO);
if (buildTargetHeroes && performance.progress != 1.0) {
......@@ -496,11 +620,11 @@ abstract class PerformanceRoute extends Route {
fakePerformance.progress = 1.0;
return new OffStage(
child: keySubtree(
build(new RouteArguments(navigator, previousPerformance: fakePerformance))
build(new RouteArguments(context: context, previousPerformance: fakePerformance))
)
);
}
return super._internalBuild(nextRoute, buildTargetHeroes: buildTargetHeroes);
return super._internalBuild(context, nextRoute, buildTargetHeroes: buildTargetHeroes);
}
void didPush(NavigatorState navigator) {
......@@ -539,7 +663,7 @@ class PageRoute extends PerformanceRoute {
Duration get transitionDuration => _kTransitionDuration;
Map<Object, HeroHandle> getHeroesToAnimate([Set<Key> mostValuableKeys]) {
return Hero.of(context, mostValuableKeys);
return Hero.of(subtreeContext, mostValuableKeys);
}
Widget build(RouteArguments args) {
......
......@@ -16,6 +16,7 @@ import 'framework.dart';
import 'gesture_detector.dart';
import 'homogeneous_viewport.dart';
import 'mixed_viewport.dart';
import 'navigator.dart';
// The gesture velocity properties are pixels/second, config min,max limits are pixels/ms
const double _kMillisecondsPerSecond = 1000.0;
......@@ -54,15 +55,14 @@ abstract class Scrollable extends StatefulComponent {
abstract class ScrollableState<T extends Scrollable> extends State<T> {
void initState() {
super.initState();
if (config.initialScrollOffset is double)
_scrollOffset = config.initialScrollOffset;
_animation = new SimulationStepper(_setScrollOffset);
_scrollOffset = Route.of(context)?.readState(context) ?? config.initialScrollOffset ?? 0.0;
}
SimulationStepper _animation;
double _scrollOffset = 0.0;
double get scrollOffset => _scrollOffset;
double _scrollOffset;
Offset get scrollOffsetVector {
if (config.scrollDirection == ScrollDirection.horizontal)
......@@ -178,6 +178,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
setState(() {
_scrollOffset = newScrollOffset;
});
Route.of(context)?.writeState(context, _scrollOffset);
dispatchOnScroll();
}
......@@ -510,8 +511,8 @@ abstract class ScrollableWidgetListState<T extends ScrollableWidgetList> extends
}
if (itemCount != _previousItemCount) {
scrollBehaviorUpdateNeeded = true;
_previousItemCount = itemCount;
scrollBehaviorUpdateNeeded = true;
}
if (scrollBehaviorUpdateNeeded)
......@@ -558,6 +559,8 @@ abstract class ScrollableWidgetListState<T extends ScrollableWidgetList> extends
}
double get _contentExtent {
if (itemCount == null)
return null;
double contentExtent = config.itemExtent * itemCount;
if (config.padding != null)
contentExtent += _leadingPadding + _trailingPadding;
......
......@@ -15,7 +15,6 @@ void main() {
routes: <String, RouteBuilder>{
'/': (RouteArguments args) { return new Column(<Widget>[
new Draggable(
navigator: args.navigator,
data: 1,
child: new Text('Source'),
feedback: new Text('Dragging')
......
......@@ -8,12 +8,12 @@ void main() {
test('Drawer control test', () {
testWidgets((WidgetTester tester) {
NavigatorState navigator;
BuildContext context;
tester.pumpWidget(
new MaterialApp(
routes: <String, RouteBuilder>{
'/': (RouteArguments args) {
navigator = args.navigator;
context = args.context;
return new Container();
}
}
......@@ -21,12 +21,12 @@ void main() {
);
tester.pump(); // no effect
expect(tester.findText('drawer'), isNull);
showDrawer(context: navigator.context, child: new Text('drawer'));
showDrawer(context: context, child: new Text('drawer'));
tester.pump(); // drawer should be starting to animate in
expect(tester.findText('drawer'), isNotNull);
tester.pump(new Duration(seconds: 1)); // animation done
expect(tester.findText('drawer'), isNotNull);
navigator.pop();
Navigator.of(context).pop();
tester.pump(); // drawer should be starting to animate away
expect(tester.findText('drawer'), isNotNull);
tester.pump(new Duration(seconds: 1)); // animation done
......@@ -36,13 +36,13 @@ void main() {
test('Drawer tap test', () {
testWidgets((WidgetTester tester) {
NavigatorState navigator;
BuildContext context;
tester.pumpWidget(new Container()); // throw away the old App and its Navigator
tester.pumpWidget(
new MaterialApp(
routes: <String, RouteBuilder>{
'/': (RouteArguments args) {
navigator = args.navigator;
context = args.context;
return new Container();
}
}
......@@ -50,7 +50,7 @@ void main() {
);
tester.pump(); // no effect
expect(tester.findText('drawer'), isNull);
showDrawer(context: navigator.context, child: new Text('drawer'));
showDrawer(context: context, child: new Text('drawer'));
tester.pump(); // drawer should be starting to animate in
expect(tester.findText('drawer'), isNotNull);
tester.pump(new Duration(seconds: 1)); // animation done
......
// 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 'package:flutter/widgets.dart';
import 'package:test/test.dart';
import 'widget_tester.dart';
List<String> ancestors = <String>[];
class TestComponent extends StatefulComponent {
TestComponentState createState() => new TestComponentState();
}
class TestComponentState extends State<TestComponent> {
void initState() {
super.initState();
context.visitAncestorElements((Element element) {
ancestors.add(element.widget.runtimeType.toString());
return true;
});
}
Widget build(BuildContext context) => new Container();
}
void main() {
test('initState() is called when we are in the tree', () {
testWidgets((WidgetTester tester) {
tester.pumpWidget(new Container(child: new TestComponent()));
expect(ancestors, equals(<String>['Container', 'RenderObjectToWidgetAdapter<RenderBox>']));
});
});
}
......@@ -4,14 +4,10 @@ import 'package:test/test.dart';
import 'widget_tester.dart';
class FirstComponent extends StatelessComponent {
FirstComponent(this.navigator);
final NavigatorState navigator;
Widget build(BuildContext context) {
return new GestureDetector(
onTap: () {
navigator.pushNamed('/second');
Navigator.of(context).pushNamed('/second');
},
child: new Container(
decoration: new BoxDecoration(
......@@ -24,17 +20,13 @@ class FirstComponent extends StatelessComponent {
}
class SecondComponent extends StatefulComponent {
SecondComponent(this.navigator);
final NavigatorState navigator;
SecondComponentState createState() => new SecondComponentState();
}
class SecondComponentState extends State<SecondComponent> {
Widget build(BuildContext context) {
return new GestureDetector(
onTap: config.navigator.pop,
onTap: Navigator.of(context).pop,
child: new Container(
decoration: new BoxDecoration(
backgroundColor: new Color(0xFFFF00FF)
......@@ -49,8 +41,8 @@ void main() {
test('Can navigator navigate to and from a stateful component', () {
testWidgets((WidgetTester tester) {
final Map<String, RouteBuilder> routes = <String, RouteBuilder>{
'/': (RouteArguments args) => new FirstComponent(args.navigator),
'/second': (RouteArguments args) => new SecondComponent(args.navigator),
'/': (RouteArguments args) => new FirstComponent(),
'/second': (RouteArguments args) => new SecondComponent(),
};
tester.pumpWidget(new Navigator(routes: routes));
......
// 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 'package:flutter/animation.dart';
import 'package:flutter/widgets.dart';
import 'package:test/test.dart';
import 'widget_tester.dart';
class ThePositiveNumbers extends ScrollableWidgetList {
ThePositiveNumbers() : super(itemExtent: 100.0);
ThePositiveNumbersState createState() => new ThePositiveNumbersState();
}
class ThePositiveNumbersState extends ScrollableWidgetListState<ThePositiveNumbers> {
ScrollBehavior createScrollBehavior() => new UnboundedBehavior();
int get itemCount => null;
List<Widget> buildItems(BuildContext context, int start, int count) {
List<Widget> result = new List<Widget>();
for (int index = start; index < start + count; index += 1)
result.add(new Text('$index', key: new ValueKey<int>(index)));
return result;
}
}
void main() {
test('whether we remember our scroll position', () {
testWidgets((WidgetTester tester) {
GlobalKey<NavigatorState> navigatorKey = new GlobalKey<NavigatorState>();
tester.pumpWidget(new Navigator(
key: navigatorKey,
routes: <String, RouteBuilder>{
'/': (RouteArguments args) => new Container(child: new ThePositiveNumbers()),
'/second': (RouteArguments args) => new Container(child: new ThePositiveNumbers()),
}
));
// we're 600 pixels high, each item is 100 pixels high, scroll position is
// zero, so we should have exactly 6 items, 0..5.
expect(tester.findText('0'), isNotNull);
expect(tester.findText('1'), isNotNull);
expect(tester.findText('2'), isNotNull);
expect(tester.findText('3'), isNotNull);
expect(tester.findText('4'), isNotNull);
expect(tester.findText('5'), isNotNull);
expect(tester.findText('6'), isNull);
expect(tester.findText('10'), isNull);
expect(tester.findText('100'), isNull);
StatefulComponentElement<ThePositiveNumbers, ThePositiveNumbersState> target =
tester.findElement((Element element) => element.widget is ThePositiveNumbers);
target.state.scrollTo(1000.0);
tester.pump(new Duration(seconds: 1));
// we're 600 pixels high, each item is 100 pixels high, scroll position is
// 1000, so we should have exactly 6 items, 10..15.
expect(tester.findText('0'), isNull);
expect(tester.findText('8'), isNull);
expect(tester.findText('9'), isNull);
expect(tester.findText('10'), isNotNull);
expect(tester.findText('11'), isNotNull);
expect(tester.findText('12'), isNotNull);
expect(tester.findText('13'), isNotNull);
expect(tester.findText('14'), isNotNull);
expect(tester.findText('15'), isNotNull);
expect(tester.findText('16'), isNull);
expect(tester.findText('100'), isNull);
navigatorKey.currentState.pushNamed('/second');
tester.pump(); // navigating always takes two frames
tester.pump(new Duration(seconds: 1));
// same as the first list again
expect(tester.findText('0'), isNotNull);
expect(tester.findText('1'), isNotNull);
expect(tester.findText('2'), isNotNull);
expect(tester.findText('3'), isNotNull);
expect(tester.findText('4'), isNotNull);
expect(tester.findText('5'), isNotNull);
expect(tester.findText('6'), isNull);
expect(tester.findText('10'), isNull);
expect(tester.findText('100'), isNull);
navigatorKey.currentState.pop();
tester.pump(); // navigating always takes two frames
tester.pump(new Duration(seconds: 1));
// we're 600 pixels high, each item is 100 pixels high, scroll position is
// 1000, so we should have exactly 6 items, 10..15.
expect(tester.findText('0'), isNull);
expect(tester.findText('8'), isNull);
expect(tester.findText('9'), isNull);
expect(tester.findText('10'), isNotNull);
expect(tester.findText('11'), isNotNull);
expect(tester.findText('12'), isNotNull);
expect(tester.findText('13'), isNotNull);
expect(tester.findText('14'), isNotNull);
expect(tester.findText('15'), isNotNull);
expect(tester.findText('16'), isNull);
expect(tester.findText('100'), isNull);
});
});
}
......@@ -16,7 +16,7 @@ void main() {
return new GestureDetector(
onTap: () {
showSnackBar(
context: args.navigator.context,
context: args.context,
placeholderKey: placeholderKey,
content: new Text(helloSnackBar)
);
......
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