Commit 03b117a5 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Remove the "most valuable keys" Hero feature (#5500)

parent f2afd05f
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:collection';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -21,12 +19,11 @@ class Photo { ...@@ -21,12 +19,11 @@ class Photo {
final String caption; final String caption;
bool isFavorite; bool isFavorite;
String get tag => assetName; // Assuming that all asset names are unique.
bool get isValid => assetName != null && title != null && caption != null && isFavorite != null; bool get isValid => assetName != null && title != null && caption != null && isFavorite != null;
} }
const String photoHeroTag = 'Photo';
typedef void BannerTapCallback(Photo photo); typedef void BannerTapCallback(Photo photo);
class GridDemoPhotoItem extends StatelessWidget { class GridDemoPhotoItem extends StatelessWidget {
...@@ -46,21 +43,14 @@ class GridDemoPhotoItem extends StatelessWidget { ...@@ -46,21 +43,14 @@ class GridDemoPhotoItem extends StatelessWidget {
final BannerTapCallback onBannerTap; // User taps on the photo's header or footer. final BannerTapCallback onBannerTap; // User taps on the photo's header or footer.
void showPhoto(BuildContext context) { void showPhoto(BuildContext context) {
Key photoKey = new Key(photo.assetName);
Set<Key> mostValuableKeys = new HashSet<Key>();
mostValuableKeys.add(photoKey);
Navigator.push(context, new MaterialPageRoute<Null>( Navigator.push(context, new MaterialPageRoute<Null>(
settings: new RouteSettings(
mostValuableKeys: mostValuableKeys
),
builder: (BuildContext context) { builder: (BuildContext context) {
return new Scaffold( return new Scaffold(
appBar: new AppBar( appBar: new AppBar(
title: new Text(photo.title) title: new Text(photo.title)
), ),
body: new Hero( body: new Hero(
tag: photoHeroTag, tag: photo.tag,
child: new Image.asset(photo.assetName, fit: ImageFit.cover) child: new Image.asset(photo.assetName, fit: ImageFit.cover)
) )
); );
...@@ -74,7 +64,7 @@ class GridDemoPhotoItem extends StatelessWidget { ...@@ -74,7 +64,7 @@ class GridDemoPhotoItem extends StatelessWidget {
onTap: () { showPhoto(context); }, onTap: () { showPhoto(context); },
child: new Hero( child: new Hero(
key: new Key(photo.assetName), key: new Key(photo.assetName),
tag: photoHeroTag, tag: photo.tag,
child: new Image.asset(photo.assetName, fit: ImageFit.cover) child: new Image.asset(photo.assetName, fit: ImageFit.cover)
) )
); );
......
...@@ -292,7 +292,7 @@ class ShrineHome extends StatefulWidget { ...@@ -292,7 +292,7 @@ class ShrineHome extends StatefulWidget {
} }
class _ShrineHomeState extends State<ShrineHome> { class _ShrineHomeState extends State<ShrineHome> {
static final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>(debugLabel: 'Order page'); static final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>(debugLabel: 'Shrine Home');
static final GlobalKey<ScrollableState> scrollableKey = new GlobalKey<ScrollableState>(); static final GlobalKey<ScrollableState> scrollableKey = new GlobalKey<ScrollableState>();
static final GridDelegate gridDelegate = new ShrineGridDelegate(); static final GridDelegate gridDelegate = new ShrineGridDelegate();
......
...@@ -136,7 +136,7 @@ class OrderPage extends StatefulWidget { ...@@ -136,7 +136,7 @@ class OrderPage extends StatefulWidget {
/// arranged in two columns. Enables the user to specify a quantity and add an /// arranged in two columns. Enables the user to specify a quantity and add an
/// order to the shopping cart. /// order to the shopping cart.
class _OrderPageState extends State<OrderPage> { class _OrderPageState extends State<OrderPage> {
static final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>(debugLabel: 'Order page'); static final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>(debugLabel: 'Shrine Order');
static final GlobalKey<ScrollableState> scrollableKey = new GlobalKey<ScrollableState>(); static final GlobalKey<ScrollableState> scrollableKey = new GlobalKey<ScrollableState>();
Order get currentOrder => ShrineOrderRoute.of(context).order; Order get currentOrder => ShrineOrderRoute.of(context).order;
......
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:collection';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' show debugDumpRenderTree, debugDumpLayerTree, debugDumpSemanticsTree; import 'package:flutter/rendering.dart' show debugDumpRenderTree, debugDumpLayerTree, debugDumpSemanticsTree;
import 'package:flutter/scheduler.dart' show timeDilation; import 'package:flutter/scheduler.dart' show timeDilation;
...@@ -245,7 +243,7 @@ class StockHomeState extends State<StockHome> { ...@@ -245,7 +243,7 @@ class StockHomeState extends State<StockHome> {
return stocks.where((Stock stock) => stock.symbol.contains(regexp)); return stocks.where((Stock stock) => stock.symbol.contains(regexp));
} }
void _buyStock(Stock stock, Key arrowKey) { void _buyStock(Stock stock) {
setState(() { setState(() {
stock.percentChange = 100.0 * (1.0 / stock.lastSale); stock.percentChange = 100.0 * (1.0 / stock.lastSale);
stock.lastSale += 1.0; stock.lastSale += 1.0;
...@@ -255,7 +253,7 @@ class StockHomeState extends State<StockHome> { ...@@ -255,7 +253,7 @@ class StockHomeState extends State<StockHome> {
action: new SnackBarAction( action: new SnackBarAction(
label: "BUY MORE", label: "BUY MORE",
onPressed: () { onPressed: () {
_buyStock(stock, arrowKey); _buyStock(stock);
} }
) )
)); ));
...@@ -263,15 +261,12 @@ class StockHomeState extends State<StockHome> { ...@@ -263,15 +261,12 @@ class StockHomeState extends State<StockHome> {
Widget _buildStockList(BuildContext context, Iterable<Stock> stocks, StockHomeTab tab) { Widget _buildStockList(BuildContext context, Iterable<Stock> stocks, StockHomeTab tab) {
return new StockList( return new StockList(
keySalt: tab,
stocks: stocks.toList(), stocks: stocks.toList(),
onAction: _buyStock, onAction: _buyStock,
onOpen: (Stock stock, Key arrowKey) { onOpen: (Stock stock) {
Set<Key> mostValuableKeys = new HashSet<Key>(); Navigator.pushNamed(context, '/stock/${stock.symbol}');
mostValuableKeys.add(arrowKey);
Navigator.pushNamed(context, '/stock/${stock.symbol}', mostValuableKeys: mostValuableKeys);
}, },
onShow: (Stock stock, Key arrowKey) { onShow: (Stock stock) {
_scaffoldKey.currentState.showBottomSheet((BuildContext context) => new StockSymbolBottomSheet(stock: stock)); _scaffoldKey.currentState.showBottomSheet((BuildContext context) => new StockSymbolBottomSheet(stock: stock));
} }
); );
......
...@@ -8,9 +8,8 @@ import 'stock_data.dart'; ...@@ -8,9 +8,8 @@ import 'stock_data.dart';
import 'stock_row.dart'; import 'stock_row.dart';
class StockList extends StatelessWidget { class StockList extends StatelessWidget {
StockList({ Key key, this.keySalt, this.stocks, this.onOpen, this.onShow, this.onAction }) : super(key: key); StockList({ Key key, this.stocks, this.onOpen, this.onShow, this.onAction }) : super(key: key);
final Object keySalt;
final List<Stock> stocks; final List<Stock> stocks;
final StockRowActionCallback onOpen; final StockRowActionCallback onOpen;
final StockRowActionCallback onShow; final StockRowActionCallback onShow;
...@@ -23,7 +22,6 @@ class StockList extends StatelessWidget { ...@@ -23,7 +22,6 @@ class StockList extends StatelessWidget {
itemExtent: StockRow.kHeight, itemExtent: StockRow.kHeight,
children: stocks.map((Stock stock) { children: stocks.map((Stock stock) {
return new StockRow( return new StockRow(
keySalt: keySalt,
stock: stock, stock: stock,
onPressed: onOpen, onPressed: onOpen,
onDoubleTap: onShow, onDoubleTap: onShow,
......
...@@ -7,58 +7,25 @@ import 'package:flutter/material.dart'; ...@@ -7,58 +7,25 @@ import 'package:flutter/material.dart';
import 'stock_data.dart'; import 'stock_data.dart';
import 'stock_arrow.dart'; import 'stock_arrow.dart';
enum StockRowPartKind { arrow } typedef void StockRowActionCallback(Stock stock);
class StockRowPartKey extends LocalKey {
const StockRowPartKey(this.keySalt, this.stock, this.part);
final Object keySalt;
final Stock stock;
final StockRowPartKind part;
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other.runtimeType != runtimeType)
return false;
final StockRowPartKey typedOther = other;
return keySalt == typedOther.keySalt
&& stock == typedOther.stock
&& part == typedOther.part;
}
@override
int get hashCode => hashValues(keySalt, stock, part);
@override
String toString() => '[$runtimeType ${keySalt.toString().split(".")[1]}:${stock.symbol}:${part.toString().split(".")[1]}]';
}
typedef void StockRowActionCallback(Stock stock, Key arrowKey);
class StockRow extends StatelessWidget { class StockRow extends StatelessWidget {
StockRow({ StockRow({
Stock stock, Stock stock,
Object keySalt,
this.onPressed, this.onPressed,
this.onDoubleTap, this.onDoubleTap,
this.onLongPressed this.onLongPressed
}) : this.stock = stock, }) : this.stock = stock, super(key: new ObjectKey(stock));
_arrowKey = new StockRowPartKey(keySalt, stock, StockRowPartKind.arrow),
super(key: new ObjectKey(stock));
final Stock stock; final Stock stock;
final StockRowActionCallback onPressed; final StockRowActionCallback onPressed;
final StockRowActionCallback onDoubleTap; final StockRowActionCallback onDoubleTap;
final StockRowActionCallback onLongPressed; final StockRowActionCallback onLongPressed;
final Key _arrowKey;
static const double kHeight = 79.0; static const double kHeight = 79.0;
GestureTapCallback _getHandler(StockRowActionCallback callback) { GestureTapCallback _getHandler(StockRowActionCallback callback) {
return callback == null ? null : () => callback(stock, _arrowKey); return callback == null ? null : () => callback(stock);
} }
@override @override
...@@ -83,8 +50,7 @@ class StockRow extends StatelessWidget { ...@@ -83,8 +50,7 @@ class StockRow extends StatelessWidget {
new Container( new Container(
margin: const EdgeInsets.only(right: 5.0), margin: const EdgeInsets.only(right: 5.0),
child: new Hero( child: new Hero(
tag: StockRowPartKind.arrow, tag: stock,
key: _arrowKey,
child: new StockArrow(percentChange: stock.percentChange) child: new StockArrow(percentChange: stock.percentChange)
) )
), ),
......
...@@ -6,12 +6,12 @@ import 'package:flutter/material.dart'; ...@@ -6,12 +6,12 @@ import 'package:flutter/material.dart';
import 'stock_data.dart'; import 'stock_data.dart';
import 'stock_arrow.dart'; import 'stock_arrow.dart';
import 'stock_row.dart';
class StockSymbolView extends StatelessWidget { class _StockSymbolView extends StatelessWidget {
StockSymbolView({ this.stock }); _StockSymbolView({ this.stock, this.arrow });
final Stock stock; final Stock stock;
final Widget arrow;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
...@@ -31,12 +31,7 @@ class StockSymbolView extends StatelessWidget { ...@@ -31,12 +31,7 @@ class StockSymbolView extends StatelessWidget {
'${stock.symbol}', '${stock.symbol}',
style: Theme.of(context).textTheme.display2 style: Theme.of(context).textTheme.display2
), ),
new Hero( arrow,
key: new ObjectKey(stock),
tag: StockRowPartKind.arrow,
turns: 2,
child: new StockArrow(percentChange: stock.percentChange)
),
], ],
mainAxisAlignment: MainAxisAlignment.spaceBetween mainAxisAlignment: MainAxisAlignment.spaceBetween
), ),
...@@ -82,7 +77,16 @@ class StockSymbolPage extends StatelessWidget { ...@@ -82,7 +77,16 @@ class StockSymbolPage extends StatelessWidget {
children: <Widget>[ children: <Widget>[
new Container( new Container(
margin: new EdgeInsets.all(20.0), margin: new EdgeInsets.all(20.0),
child: new Card(child: new StockSymbolView(stock: stock)) child: new Card(
child: new _StockSymbolView(
stock: stock,
arrow: new Hero(
tag: stock,
turns: 2,
child: new StockArrow(percentChange: stock.percentChange)
)
)
)
) )
] ]
) )
...@@ -102,7 +106,10 @@ class StockSymbolBottomSheet extends StatelessWidget { ...@@ -102,7 +106,10 @@ class StockSymbolBottomSheet extends StatelessWidget {
decoration: new BoxDecoration( decoration: new BoxDecoration(
border: new Border(top: new BorderSide(color: Colors.black26)) border: new Border(top: new BorderSide(color: Colors.black26))
), ),
child: new StockSymbolView(stock: stock) child: new _StockSymbolView(
stock: stock,
arrow: new StockArrow(percentChange: stock.percentChange)
)
); );
} }
} }
...@@ -20,14 +20,9 @@ import 'transitions.dart'; ...@@ -20,14 +20,9 @@ import 'transitions.dart';
// album's details view. In this context, a screen is a navigator ModalRoute. // album's details view. In this context, a screen is a navigator ModalRoute.
// To get this effect, all you have to do is wrap each hero on each route with a // To get this effect, all you have to do is wrap each hero on each route with a
// Hero widget, and give each hero a tag. Tag must either be unique within the // Hero widget, and give each hero a tag. The tag must either be unique within the
// current route's widget subtree, or all the Heroes with that tag on a // current route's widget subtree. When the app transitions from one route to
// particular route must have a key. When the app transitions from one route to // another, each hero is animated to its new location. If a hero is only
// another, each tag present is animated. When there's exactly one hero with
// that tag, that hero will be animated for that tag. When there are multiple
// heroes in a route with the same tag, then whichever hero has a key that
// matches one of the keys in the "most important key" list given to the
// navigator when the route was pushed will be animated. If a hero is only
// present on one of the routes and not the other, then it will be made to // present on one of the routes and not the other, then it will be made to
// appear or disappear as needed. // appear or disappear as needed.
...@@ -96,57 +91,35 @@ class Hero extends StatefulWidget { ...@@ -96,57 +91,35 @@ class Hero extends StatefulWidget {
final int turns; final int turns;
/// If true, the hero will always animate, even if it has no matching hero to /// If true, the hero will always animate, even if it has no matching hero to
/// animate to or from. (This only applies if the hero is relevant; if there /// animate to or from.
/// are multiple heroes with the same tag, only the one whose key matches the
/// "most valuable keys" will be used.)
final bool alwaysAnimate; final bool alwaysAnimate;
static Map<Object, HeroHandle> of(BuildContext context, Set<Key> mostValuableKeys) { /// Return a hero tag to HeroState map of all of the heroes within the given subtree.
mostValuableKeys ??= new HashSet<Key>(); static Map<Object, HeroHandle> of(BuildContext context) {
assert(!mostValuableKeys.contains(null)); final Map<Object, HeroHandle> result = <Object, HeroHandle>{};
// first we collect ALL the heroes, sorted by their tags
Map<Object, Map<Key, HeroState>> heroes = <Object, Map<Key, HeroState>>{};
void visitor(Element element) { void visitor(Element element) {
if (element.widget is Hero) { if (element.widget is Hero) {
StatefulElement hero = element; StatefulElement hero = element;
Hero heroWidget = element.widget; Hero heroWidget = element.widget;
Object tag = heroWidget.tag; Object tag = heroWidget.tag;
assert(tag != null); assert(tag != null);
Key key = heroWidget.key;
final Map<Key, HeroState> tagHeroes = heroes.putIfAbsent(tag, () => <Key, HeroState>{});
assert(() { assert(() {
if (tagHeroes.containsKey(key)) { if (result.containsKey(tag)) {
new FlutterError( new FlutterError(
'There are multiple heroes that share the same key within the same subtree.\n' 'There are multiple heroes that share the same tag within a subtree.\n'
'Within each subtree for which heroes are to be animated (typically a PageRoute subtree), ' '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 ' 'each Hero must have a unique non-null tag.\n'
'have different keys.\n' 'In this case, multiple heroes had the tag "$tag".'
'In this case, the tag "$tag" had multiple heroes with the key "$key".'
); );
} }
return true; return true;
}); });
tagHeroes[key] = hero.state; HeroState heroState = hero.state;
result[tag] = heroState;
} }
element.visitChildren(visitor); element.visitChildren(visitor);
} }
context.visitChildElements(visitor); context.visitChildElements(visitor);
// next, for each tag, we're going to decide on the one hero we care about for that tag
Map<Object, HeroHandle> result = <Object, HeroHandle>{};
for (Object tag in heroes.keys) {
assert(tag != null);
if (heroes[tag].length == 1) {
result[tag] = heroes[tag].values.first;
} else {
assert(heroes[tag].length > 1);
assert(!heroes[tag].containsKey(null));
assert(heroes[tag].keys.where((Key key) => mostValuableKeys.contains(key)).length <= 1);
Key mostValuableKey = mostValuableKeys.firstWhere((Key key) => heroes[tag].containsKey(key), orElse: () => null);
if (mostValuableKey != null)
result[tag] = heroes[tag][mostValuableKey];
}
}
assert(!result.containsKey(null));
return result; return result;
} }
...@@ -532,27 +505,15 @@ class HeroController extends NavigatorObserver { ...@@ -532,27 +505,15 @@ class HeroController extends NavigatorObserver {
return entry; return entry;
} }
Set<Key> _getMostValuableKeys() {
assert(_from != null);
assert(_to != null);
Set<Key> result = new HashSet<Key>();
if (_from.settings.mostValuableKeys != null)
result.addAll(_from.settings.mostValuableKeys);
if (_to.settings.mostValuableKeys != null)
result.addAll(_to.settings.mostValuableKeys);
return result;
}
void _updateQuest(Duration timeStamp) { void _updateQuest(Duration timeStamp) {
if (navigator == null) { if (navigator == null) {
// The navigator was removed before this end-of-frame callback was called. // The navigator was removed before this end-of-frame callback was called.
return; return;
} }
Set<Key> mostValuableKeys = _getMostValuableKeys();
Map<Object, HeroHandle> heroesFrom = _party.isEmpty ? Map<Object, HeroHandle> heroesFrom = _party.isEmpty ?
Hero.of(_from.subtreeContext, mostValuableKeys) : _party.getHeroesToAnimate(); Hero.of(_from.subtreeContext) : _party.getHeroesToAnimate();
Map<Object, HeroHandle> heroesTo = Hero.of(_to.subtreeContext, mostValuableKeys); Map<Object, HeroHandle> heroesTo = Hero.of(_to.subtreeContext);
_to.offstage = false; _to.offstage = false;
Animation<double> animation = _animation; Animation<double> animation = _animation;
......
...@@ -111,7 +111,6 @@ class RouteSettings { ...@@ -111,7 +111,6 @@ class RouteSettings {
/// Creates data used to construct routes. /// Creates data used to construct routes.
const RouteSettings({ const RouteSettings({
this.name, this.name,
this.mostValuableKeys,
this.isInitialRoute: false this.isInitialRoute: false
}); });
...@@ -120,29 +119,13 @@ class RouteSettings { ...@@ -120,29 +119,13 @@ class RouteSettings {
/// If null, the route is anonymous. /// If null, the route is anonymous.
final String name; final String name;
/// The set of keys that are most relevant for constructoring [Hero]
/// transitions. For example, if the current route contains a list of music
/// albums and the user triggered this navigation by tapping one of the
/// albums, the most valuable album cover is the one associated with the album
/// the user tapped and is the one that should heroically transition when
/// opening the details page for that album.
final Set<Key> mostValuableKeys;
/// Whether this route is the very first route being pushed onto this [Navigator]. /// Whether this route is the very first route being pushed onto this [Navigator].
/// ///
/// The initial route typically skips any entrance transition to speed startup. /// The initial route typically skips any entrance transition to speed startup.
final bool isInitialRoute; final bool isInitialRoute;
@override @override
String toString() { String toString() => '"$name"';
String result = '"$name"';
if (mostValuableKeys != null && mostValuableKeys.isNotEmpty) {
result += '; keys:';
for (Key key in mostValuableKeys)
result += ' $key';
}
return result;
}
} }
/// Creates a route for the given route settings. /// Creates a route for the given route settings.
...@@ -211,10 +194,9 @@ class Navigator extends StatefulWidget { ...@@ -211,10 +194,9 @@ class Navigator extends StatefulWidget {
/// Push a named route onto the navigator that most tightly encloses the given context. /// Push a named route onto the navigator that most tightly encloses the given context.
/// ///
/// The route name will be passed to that navigator's [onGenerateRoute] /// The route name will be passed to that navigator's [onGenerateRoute]
/// callback. The returned route will be pushed into the navigator. The set of /// callback. The returned route will be pushed into the navigator.
/// most valuable keys will be used to construct an appropriate [Hero] transition. static void pushNamed(BuildContext context, String routeName) {
static void pushNamed(BuildContext context, String routeName, { Set<Key> mostValuableKeys }) { Navigator.of(context).pushNamed(routeName);
Navigator.of(context).pushNamed(routeName, mostValuableKeys: mostValuableKeys);
} }
/// Push a route onto the navigator that most tightly encloses the given context. /// Push a route onto the navigator that most tightly encloses the given context.
...@@ -265,10 +247,10 @@ class Navigator extends StatefulWidget { ...@@ -265,10 +247,10 @@ class Navigator extends StatefulWidget {
/// Executes a simple transaction that both pops the current route off and /// Executes a simple transaction that both pops the current route off and
/// pushes a named route into the navigator that most tightly encloses the given context. /// pushes a named route into the navigator that most tightly encloses the given context.
static void popAndPushNamed(BuildContext context, String routeName, { Set<Key> mostValuableKeys }) { static void popAndPushNamed(BuildContext context, String routeName) {
Navigator.of(context) Navigator.of(context)
..pop() ..pop()
..pushNamed(routeName, mostValuableKeys: mostValuableKeys); ..pushNamed(routeName);
} }
static NavigatorState of(BuildContext context) { static NavigatorState of(BuildContext context) {
...@@ -340,13 +322,10 @@ class NavigatorState extends State<Navigator> { ...@@ -340,13 +322,10 @@ class NavigatorState extends State<Navigator> {
bool _debugLocked = false; // used to prevent re-entrant calls to push, pop, and friends bool _debugLocked = false; // used to prevent re-entrant calls to push, pop, and friends
void pushNamed(String name, { Set<Key> mostValuableKeys }) { void pushNamed(String name) {
assert(!_debugLocked); assert(!_debugLocked);
assert(name != null); assert(name != null);
RouteSettings settings = new RouteSettings( RouteSettings settings = new RouteSettings(name: name);
name: name,
mostValuableKeys: mostValuableKeys
);
Route<dynamic> route = config.onGenerateRoute(settings); Route<dynamic> route = config.onGenerateRoute(settings);
if (route == null) { if (route == null) {
assert(config.onUnknownRoute != null); assert(config.onUnknownRoute != null);
......
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