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 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
......@@ -21,12 +19,11 @@ class Photo {
final String caption;
bool isFavorite;
String get tag => assetName; // Assuming that all asset names are unique.
bool get isValid => assetName != null && title != null && caption != null && isFavorite != null;
}
const String photoHeroTag = 'Photo';
typedef void BannerTapCallback(Photo photo);
class GridDemoPhotoItem extends StatelessWidget {
......@@ -46,21 +43,14 @@ class GridDemoPhotoItem extends StatelessWidget {
final BannerTapCallback onBannerTap; // User taps on the photo's header or footer.
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>(
settings: new RouteSettings(
mostValuableKeys: mostValuableKeys
),
builder: (BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(photo.title)
),
body: new Hero(
tag: photoHeroTag,
tag: photo.tag,
child: new Image.asset(photo.assetName, fit: ImageFit.cover)
)
);
......@@ -74,7 +64,7 @@ class GridDemoPhotoItem extends StatelessWidget {
onTap: () { showPhoto(context); },
child: new Hero(
key: new Key(photo.assetName),
tag: photoHeroTag,
tag: photo.tag,
child: new Image.asset(photo.assetName, fit: ImageFit.cover)
)
);
......
......@@ -292,7 +292,7 @@ class ShrineHome extends StatefulWidget {
}
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 GridDelegate gridDelegate = new ShrineGridDelegate();
......
......@@ -136,7 +136,7 @@ class OrderPage extends StatefulWidget {
/// arranged in two columns. Enables the user to specify a quantity and add an
/// order to the shopping cart.
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>();
Order get currentOrder => ShrineOrderRoute.of(context).order;
......
......@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' show debugDumpRenderTree, debugDumpLayerTree, debugDumpSemanticsTree;
import 'package:flutter/scheduler.dart' show timeDilation;
......@@ -245,7 +243,7 @@ class StockHomeState extends State<StockHome> {
return stocks.where((Stock stock) => stock.symbol.contains(regexp));
}
void _buyStock(Stock stock, Key arrowKey) {
void _buyStock(Stock stock) {
setState(() {
stock.percentChange = 100.0 * (1.0 / stock.lastSale);
stock.lastSale += 1.0;
......@@ -255,7 +253,7 @@ class StockHomeState extends State<StockHome> {
action: new SnackBarAction(
label: "BUY MORE",
onPressed: () {
_buyStock(stock, arrowKey);
_buyStock(stock);
}
)
));
......@@ -263,15 +261,12 @@ class StockHomeState extends State<StockHome> {
Widget _buildStockList(BuildContext context, Iterable<Stock> stocks, StockHomeTab tab) {
return new StockList(
keySalt: tab,
stocks: stocks.toList(),
onAction: _buyStock,
onOpen: (Stock stock, Key arrowKey) {
Set<Key> mostValuableKeys = new HashSet<Key>();
mostValuableKeys.add(arrowKey);
Navigator.pushNamed(context, '/stock/${stock.symbol}', mostValuableKeys: mostValuableKeys);
onOpen: (Stock stock) {
Navigator.pushNamed(context, '/stock/${stock.symbol}');
},
onShow: (Stock stock, Key arrowKey) {
onShow: (Stock stock) {
_scaffoldKey.currentState.showBottomSheet((BuildContext context) => new StockSymbolBottomSheet(stock: stock));
}
);
......
......@@ -8,9 +8,8 @@ import 'stock_data.dart';
import 'stock_row.dart';
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 StockRowActionCallback onOpen;
final StockRowActionCallback onShow;
......@@ -23,7 +22,6 @@ class StockList extends StatelessWidget {
itemExtent: StockRow.kHeight,
children: stocks.map((Stock stock) {
return new StockRow(
keySalt: keySalt,
stock: stock,
onPressed: onOpen,
onDoubleTap: onShow,
......
......@@ -7,58 +7,25 @@ import 'package:flutter/material.dart';
import 'stock_data.dart';
import 'stock_arrow.dart';
enum StockRowPartKind { arrow }
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);
typedef void StockRowActionCallback(Stock stock);
class StockRow extends StatelessWidget {
StockRow({
Stock stock,
Object keySalt,
this.onPressed,
this.onDoubleTap,
this.onLongPressed
}) : this.stock = stock,
_arrowKey = new StockRowPartKey(keySalt, stock, StockRowPartKind.arrow),
super(key: new ObjectKey(stock));
}) : this.stock = stock, super(key: new ObjectKey(stock));
final Stock stock;
final StockRowActionCallback onPressed;
final StockRowActionCallback onDoubleTap;
final StockRowActionCallback onLongPressed;
final Key _arrowKey;
static const double kHeight = 79.0;
GestureTapCallback _getHandler(StockRowActionCallback callback) {
return callback == null ? null : () => callback(stock, _arrowKey);
return callback == null ? null : () => callback(stock);
}
@override
......@@ -83,8 +50,7 @@ class StockRow extends StatelessWidget {
new Container(
margin: const EdgeInsets.only(right: 5.0),
child: new Hero(
tag: StockRowPartKind.arrow,
key: _arrowKey,
tag: stock,
child: new StockArrow(percentChange: stock.percentChange)
)
),
......
......@@ -6,12 +6,12 @@ import 'package:flutter/material.dart';
import 'stock_data.dart';
import 'stock_arrow.dart';
import 'stock_row.dart';
class StockSymbolView extends StatelessWidget {
StockSymbolView({ this.stock });
class _StockSymbolView extends StatelessWidget {
_StockSymbolView({ this.stock, this.arrow });
final Stock stock;
final Widget arrow;
@override
Widget build(BuildContext context) {
......@@ -31,12 +31,7 @@ class StockSymbolView extends StatelessWidget {
'${stock.symbol}',
style: Theme.of(context).textTheme.display2
),
new Hero(
key: new ObjectKey(stock),
tag: StockRowPartKind.arrow,
turns: 2,
child: new StockArrow(percentChange: stock.percentChange)
),
arrow,
],
mainAxisAlignment: MainAxisAlignment.spaceBetween
),
......@@ -82,7 +77,16 @@ class StockSymbolPage extends StatelessWidget {
children: <Widget>[
new Container(
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 {
decoration: new BoxDecoration(
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';
// 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
// Hero widget, and give each hero a tag. Tag must either be unique within the
// current route's widget subtree, or all the Heroes with that tag on a
// particular route must have a key. When the app transitions from one route to
// 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
// Hero widget, and give each hero a tag. The tag must either be unique within the
// current route's widget subtree. When the app transitions from one route to
// another, each hero is animated to its new location. If a hero is only
// present on one of the routes and not the other, then it will be made to
// appear or disappear as needed.
......@@ -96,57 +91,35 @@ class Hero extends StatefulWidget {
final int turns;
/// 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
/// are multiple heroes with the same tag, only the one whose key matches the
/// "most valuable keys" will be used.)
/// animate to or from.
final bool alwaysAnimate;
static Map<Object, HeroHandle> of(BuildContext context, Set<Key> mostValuableKeys) {
mostValuableKeys ??= new HashSet<Key>();
assert(!mostValuableKeys.contains(null));
// first we collect ALL the heroes, sorted by their tags
Map<Object, Map<Key, HeroState>> heroes = <Object, Map<Key, HeroState>>{};
/// Return a hero tag to HeroState map of all of the heroes within the given subtree.
static Map<Object, HeroHandle> of(BuildContext context) {
final Map<Object, HeroHandle> result = <Object, HeroHandle>{};
void visitor(Element element) {
if (element.widget is Hero) {
StatefulElement hero = element;
Hero heroWidget = element.widget;
Object tag = heroWidget.tag;
assert(tag != null);
Key key = heroWidget.key;
final Map<Key, HeroState> tagHeroes = heroes.putIfAbsent(tag, () => <Key, HeroState>{});
assert(() {
if (tagHeroes.containsKey(key)) {
if (result.containsKey(tag)) {
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), '
'either each Hero must have a unique tag, or, all the heroes with a particular tag must '
'have different keys.\n'
'In this case, the tag "$tag" had multiple heroes with the key "$key".'
'each Hero must have a unique non-null tag.\n'
'In this case, multiple heroes had the tag "$tag".'
);
}
return true;
});
tagHeroes[key] = hero.state;
HeroState heroState = hero.state;
result[tag] = heroState;
}
element.visitChildren(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;
}
......@@ -532,27 +505,15 @@ class HeroController extends NavigatorObserver {
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) {
if (navigator == null) {
// The navigator was removed before this end-of-frame callback was called.
return;
}
Set<Key> mostValuableKeys = _getMostValuableKeys();
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;
Animation<double> animation = _animation;
......
......@@ -111,7 +111,6 @@ class RouteSettings {
/// Creates data used to construct routes.
const RouteSettings({
this.name,
this.mostValuableKeys,
this.isInitialRoute: false
});
......@@ -120,29 +119,13 @@ class RouteSettings {
/// If null, the route is anonymous.
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].
///
/// The initial route typically skips any entrance transition to speed startup.
final bool isInitialRoute;
@override
String toString() {
String result = '"$name"';
if (mostValuableKeys != null && mostValuableKeys.isNotEmpty) {
result += '; keys:';
for (Key key in mostValuableKeys)
result += ' $key';
}
return result;
}
String toString() => '"$name"';
}
/// Creates a route for the given route settings.
......@@ -211,10 +194,9 @@ class Navigator extends StatefulWidget {
/// 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]
/// callback. The returned route will be pushed into the navigator. The set of
/// most valuable keys will be used to construct an appropriate [Hero] transition.
static void pushNamed(BuildContext context, String routeName, { Set<Key> mostValuableKeys }) {
Navigator.of(context).pushNamed(routeName, mostValuableKeys: mostValuableKeys);
/// callback. The returned route will be pushed into the navigator.
static void pushNamed(BuildContext context, String routeName) {
Navigator.of(context).pushNamed(routeName);
}
/// Push a route onto the navigator that most tightly encloses the given context.
......@@ -265,10 +247,10 @@ class Navigator extends StatefulWidget {
/// 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.
static void popAndPushNamed(BuildContext context, String routeName, { Set<Key> mostValuableKeys }) {
static void popAndPushNamed(BuildContext context, String routeName) {
Navigator.of(context)
..pop()
..pushNamed(routeName, mostValuableKeys: mostValuableKeys);
..pushNamed(routeName);
}
static NavigatorState of(BuildContext context) {
......@@ -340,13 +322,10 @@ class NavigatorState extends State<Navigator> {
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(name != null);
RouteSettings settings = new RouteSettings(
name: name,
mostValuableKeys: mostValuableKeys
);
RouteSettings settings = new RouteSettings(name: name);
Route<dynamic> route = config.onGenerateRoute(settings);
if (route == 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