mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Heroes
This commit is contained in:
parent
8414b4c1a1
commit
fa8c45151f
@ -186,14 +186,16 @@ class StockHomeState extends State<StockHome> {
|
||||
Widget buildStockList(BuildContext context, Iterable<Stock> stocks) {
|
||||
return new StockList(
|
||||
stocks: stocks.toList(),
|
||||
onAction: (Stock stock, GlobalKey arrowKey) {
|
||||
onAction: (Stock stock, Key arrowKey) {
|
||||
setState(() {
|
||||
stock.percentChange = 100.0 * (1.0 / stock.lastSale);
|
||||
stock.lastSale += 1.0;
|
||||
});
|
||||
},
|
||||
onOpen: (Stock stock, GlobalKey arrowKey) {
|
||||
config.navigator.pushNamed('/stock/${stock.symbol}');
|
||||
onOpen: (Stock stock, Key arrowKey) {
|
||||
Set<Key> mostValuableKeys = new Set<Key>();
|
||||
mostValuableKeys.add(arrowKey);
|
||||
config.navigator.pushNamed('/stock/${stock.symbol}', mostValuableKeys: mostValuableKeys);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ part of stocks;
|
||||
|
||||
enum StockRowPartKind { arrow }
|
||||
|
||||
class StockRowPartKey extends GlobalKey {
|
||||
class StockRowPartKey extends Key {
|
||||
const StockRowPartKey(this.stock, this.part) : super.constructor();
|
||||
final Stock stock;
|
||||
final StockRowPartKind part;
|
||||
@ -21,7 +21,7 @@ class StockRowPartKey extends GlobalKey {
|
||||
String toString() => '[StockRowPartKey ${stock.symbol}:${part.toString().split(".")[1]})]';
|
||||
}
|
||||
|
||||
typedef void StockRowActionCallback(Stock stock, GlobalKey arrowKey);
|
||||
typedef void StockRowActionCallback(Stock stock, Key arrowKey);
|
||||
|
||||
class StockRow extends StatelessComponent {
|
||||
StockRow({
|
||||
@ -30,7 +30,7 @@ class StockRow extends StatelessComponent {
|
||||
this.onLongPressed
|
||||
}) : this.stock = stock,
|
||||
_arrowKey = new StockRowPartKey(stock, StockRowPartKind.arrow),
|
||||
super(key: new GlobalObjectKey(stock));
|
||||
super(key: new ObjectKey(stock));
|
||||
|
||||
final Stock stock;
|
||||
final StockRowActionCallback onPressed;
|
||||
@ -53,7 +53,7 @@ class StockRow extends StatelessComponent {
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
String lastSale = "\$${stock.lastSale.toStringAsFixed(2)}";
|
||||
final String lastSale = "\$${stock.lastSale.toStringAsFixed(2)}";
|
||||
String changeInPrice = "${stock.percentChange.toStringAsFixed(2)}%";
|
||||
if (stock.percentChange > 0)
|
||||
changeInPrice = "+" + changeInPrice;
|
||||
@ -69,9 +69,12 @@ class StockRow extends StatelessComponent {
|
||||
),
|
||||
child: new Row(<Widget>[
|
||||
new Container(
|
||||
key: _arrowKey,
|
||||
child: new StockArrow(percentChange: stock.percentChange),
|
||||
margin: const EdgeDims.only(right: 5.0)
|
||||
margin: const EdgeDims.only(right: 5.0),
|
||||
child: new Hero(
|
||||
tag: StockRowPartKind.arrow,
|
||||
key: _arrowKey,
|
||||
child: new StockArrow(percentChange: stock.percentChange)
|
||||
)
|
||||
),
|
||||
new Flexible(
|
||||
child: new Row(<Widget>[
|
||||
|
@ -36,7 +36,11 @@ class StockSymbolViewer extends StatelessComponent {
|
||||
'${stock.symbol}',
|
||||
style: Theme.of(context).text.display2
|
||||
),
|
||||
new StockArrow(percentChange: stock.percentChange)
|
||||
new Hero(
|
||||
tag: StockRowPartKind.arrow,
|
||||
turns: 2,
|
||||
child: new StockArrow(percentChange: stock.percentChange)
|
||||
),
|
||||
],
|
||||
justifyContent: FlexJustifyContent.spaceBetween
|
||||
),
|
||||
@ -51,7 +55,8 @@ class StockSymbolViewer extends StatelessComponent {
|
||||
)
|
||||
)
|
||||
)
|
||||
])
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
389
packages/flutter/lib/src/widgets/heroes.dart
Normal file
389
packages/flutter/lib/src/widgets/heroes.dart
Normal file
@ -0,0 +1,389 @@
|
||||
// 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/rendering.dart';
|
||||
|
||||
import 'basic.dart';
|
||||
import 'framework.dart';
|
||||
import 'navigator.dart';
|
||||
import 'transitions.dart';
|
||||
|
||||
// Heroes are the parts of an application's screen-to-screen transitions where a
|
||||
// component from one screen shifts to a position on the other. For example,
|
||||
// album art from a list of albums growing to become the centerpiece of the
|
||||
// album's details view. In this context, a screen is a navigator Route.
|
||||
|
||||
// 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
|
||||
// present on one of the routes and not the other, then it will be made to
|
||||
// appear or disappear as needed.
|
||||
|
||||
// TODO(ianh): Make the appear/disappear animations pretty. Right now they're
|
||||
// pretty crude (just rotate and shrink the constraints). They should probably
|
||||
// involve actually scaling and fading, at a minimum.
|
||||
|
||||
// Heroes and the Navigator's Stack must be axis-aligned for all this to work.
|
||||
// The top left and bottom right coordinates of each animated Hero will be
|
||||
// converted to global coordinates and then from there converted to the
|
||||
// Navigator Stack's coordinate space, and the entire Hero subtree will, for the
|
||||
// duration of the animation, be lifted out of its original place, and
|
||||
// positioned on that stack. If the Hero isn't axis aligned, this is going to
|
||||
// fail in a rather ugly fashion. Don't rotate your heroes!
|
||||
|
||||
// To make the animations look good, it's critical that the widget tree for the
|
||||
// hero in both locations be essentially identical. The widget of the target is
|
||||
// used to do the transition: when going from route A to route B, route B's
|
||||
// hero's widget is placed over route A's hero's widget, and route A's hero is
|
||||
// hidden. Then the widget is animated to route B's hero's position, and then
|
||||
// the widget is inserted into route B. When going back from B to A, route A's
|
||||
// hero's widget is placed over where route B's hero's widget was, and then the
|
||||
// animation goes the other way.
|
||||
|
||||
// TODO(ianh): If the widgets use Inherited properties, they are taken from the
|
||||
// Navigator's position in the widget hierarchy, not the source or target. We
|
||||
// should interpolate the inherited properties from their value at the source to
|
||||
// their value at the target. See: https://github.com/flutter/engine/issues/1698
|
||||
|
||||
final Object centerOfAttentionHeroTag = new Object();
|
||||
|
||||
class _HeroManifest {
|
||||
const _HeroManifest({
|
||||
this.key,
|
||||
this.config,
|
||||
this.sourceStates,
|
||||
this.currentRect,
|
||||
this.currentTurns
|
||||
});
|
||||
final GlobalKey key;
|
||||
final Widget config;
|
||||
final Set<HeroState> sourceStates;
|
||||
final RelativeRect currentRect;
|
||||
final double currentTurns;
|
||||
}
|
||||
|
||||
abstract class HeroHandle {
|
||||
_HeroManifest _takeChild(Rect animationArea);
|
||||
}
|
||||
|
||||
class Hero extends StatefulComponent {
|
||||
Hero({
|
||||
Key key,
|
||||
this.navigator,
|
||||
this.tag,
|
||||
this.child,
|
||||
this.turns: 1
|
||||
}) : super(key: key) {
|
||||
assert(tag != null);
|
||||
}
|
||||
|
||||
final NavigatorState navigator;
|
||||
final Object tag;
|
||||
final Widget child;
|
||||
final int turns;
|
||||
|
||||
static Map<Object, HeroHandle> of(BuildContext context, Set<Key> mostValuableKeys) {
|
||||
mostValuableKeys ??= new Set<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>>{};
|
||||
void visitor(Element element) {
|
||||
if (element.widget is Hero) {
|
||||
StatefulComponentElement<Hero, HeroState> hero = element;
|
||||
Object tag = hero.widget.tag;
|
||||
assert(tag != null);
|
||||
Key key = hero.widget.key;
|
||||
final Map<Key, HeroState> tagHeroes = heroes.putIfAbsent(tag, () => <Key, HeroState>{});
|
||||
assert(!tagHeroes.containsKey(key));
|
||||
tagHeroes[key] = hero.state;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
HeroState createState() => new HeroState();
|
||||
}
|
||||
|
||||
enum _HeroMode { constructing, initialized, measured, taken }
|
||||
|
||||
class HeroState extends State<Hero> implements HeroHandle {
|
||||
|
||||
void initState() {
|
||||
assert(_mode == _HeroMode.constructing);
|
||||
super.initState();
|
||||
_key = new GlobalKey();
|
||||
_mode = _HeroMode.initialized;
|
||||
}
|
||||
|
||||
GlobalKey _key;
|
||||
|
||||
_HeroMode _mode = _HeroMode.constructing;
|
||||
Size _size;
|
||||
|
||||
_HeroManifest _takeChild(Rect animationArea) {
|
||||
assert(_mode == _HeroMode.measured || _mode == _HeroMode.taken);
|
||||
final RenderBox renderObject = context.findRenderObject();
|
||||
final Point heroTopLeft = renderObject.localToGlobal(Point.origin);
|
||||
final Point heroBottomRight = renderObject.localToGlobal(renderObject.size.bottomRight(Point.origin));
|
||||
final Rect heroArea = new Rect.fromLTRB(heroTopLeft.x, heroTopLeft.y, heroBottomRight.x, heroBottomRight.y);
|
||||
final RelativeRect startRect = new RelativeRect.fromRect(heroArea, animationArea);
|
||||
_HeroManifest result = new _HeroManifest(
|
||||
key: _key,
|
||||
config: config,
|
||||
sourceStates: new Set<HeroState>.from(<HeroState>[this]),
|
||||
currentRect: startRect,
|
||||
currentTurns: config.turns.toDouble()
|
||||
);
|
||||
setState(() {
|
||||
_key = null;
|
||||
_mode = _HeroMode.taken;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
void _setChild(GlobalKey value) {
|
||||
assert(_mode == _HeroMode.taken);
|
||||
assert(_key == null);
|
||||
assert(_size != null);
|
||||
if (mounted)
|
||||
setState(() { _key = value; });
|
||||
_size = null;
|
||||
_mode = _HeroMode.initialized;
|
||||
}
|
||||
|
||||
void _resetChild() {
|
||||
assert(_mode == _HeroMode.taken);
|
||||
assert(_key == null);
|
||||
assert(_size != null);
|
||||
if (mounted)
|
||||
setState(() { _key = new GlobalKey(); });
|
||||
_size = null;
|
||||
_mode = _HeroMode.initialized;
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
switch (_mode) {
|
||||
case _HeroMode.constructing:
|
||||
assert(false);
|
||||
return null;
|
||||
case _HeroMode.initialized:
|
||||
case _HeroMode.measured:
|
||||
return new SizeObserver(
|
||||
onSizeChanged: (Size size) {
|
||||
assert(_mode == _HeroMode.initialized || _mode == _HeroMode.measured);
|
||||
_size = size;
|
||||
_mode = _HeroMode.measured;
|
||||
},
|
||||
child: new KeyedSubtree(
|
||||
key: _key,
|
||||
child: config.child
|
||||
)
|
||||
);
|
||||
case _HeroMode.taken:
|
||||
return new SizedBox(width: _size.width, height: _size.height);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class _HeroQuestState implements HeroHandle {
|
||||
_HeroQuestState({
|
||||
this.tag,
|
||||
this.key,
|
||||
this.child,
|
||||
this.sourceStates,
|
||||
this.targetRect,
|
||||
this.targetTurns,
|
||||
this.targetState,
|
||||
this.currentRect,
|
||||
this.currentTurns
|
||||
}) {
|
||||
assert(tag != null);
|
||||
}
|
||||
|
||||
final Object tag;
|
||||
final GlobalKey key;
|
||||
final Widget child;
|
||||
final Set<HeroState> sourceStates;
|
||||
final RelativeRect targetRect;
|
||||
final int targetTurns;
|
||||
final HeroState targetState;
|
||||
final AnimatedRelativeRectValue currentRect;
|
||||
final AnimatedValue<double> currentTurns;
|
||||
|
||||
bool get taken => _taken;
|
||||
bool _taken = false;
|
||||
_HeroManifest _takeChild(Rect animationArea) {
|
||||
assert(!taken);
|
||||
_taken = true;
|
||||
Set<HeroState> states = sourceStates;
|
||||
if (targetState != null)
|
||||
states = states.union(new Set<HeroState>.from(<HeroState>[targetState]));
|
||||
return new _HeroManifest(
|
||||
key: key,
|
||||
config: child,
|
||||
sourceStates: states,
|
||||
currentRect: currentRect.value,
|
||||
currentTurns: currentTurns.value
|
||||
);
|
||||
}
|
||||
|
||||
Widget build(BuildContext context, PerformanceView performance) {
|
||||
return new PositionedTransition(
|
||||
rect: currentRect,
|
||||
performance: performance,
|
||||
child: new RotationTransition(
|
||||
turns: currentTurns,
|
||||
performance: performance,
|
||||
child: new KeyedSubtree(
|
||||
key: key,
|
||||
child: child
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HeroMatch {
|
||||
const _HeroMatch(this.from, this.to, this.tag);
|
||||
final HeroHandle from;
|
||||
final HeroHandle to;
|
||||
final Object tag;
|
||||
}
|
||||
|
||||
typedef void QuestFinishedHandler();
|
||||
|
||||
class HeroParty {
|
||||
HeroParty({ this.onQuestFinished });
|
||||
|
||||
final QuestFinishedHandler onQuestFinished;
|
||||
|
||||
List<_HeroQuestState> _heroes = <_HeroQuestState>[];
|
||||
bool get isEmpty => _heroes.isEmpty;
|
||||
|
||||
Map<Object, HeroHandle> getHeroesToAnimate() {
|
||||
Map<Object, HeroHandle> result = new Map<Object, HeroHandle>();
|
||||
for (_HeroQuestState hero in _heroes)
|
||||
result[hero.tag] = hero;
|
||||
assert(!result.containsKey(null));
|
||||
return result;
|
||||
}
|
||||
|
||||
AnimatedRelativeRectValue createAnimatedRelativeRect(RelativeRect begin, RelativeRect end, Curve curve) {
|
||||
return new AnimatedRelativeRectValue(begin, end: end, curve: curve);
|
||||
}
|
||||
|
||||
AnimatedValue<double> createAnimatedTurns(double begin, double end, Curve curve) {
|
||||
assert(end.floor() == end);
|
||||
return new AnimatedValue<double>(begin, end: end, curve: curve);
|
||||
}
|
||||
|
||||
void animate(Map<Object, HeroHandle> heroesFrom, Map<Object, HeroHandle> heroesTo, Rect animationArea, Curve curve) {
|
||||
assert(!heroesFrom.containsKey(null));
|
||||
assert(!heroesTo.containsKey(null));
|
||||
|
||||
// make a list of pairs of heroes, based on the from and to lists
|
||||
Map<Object, _HeroMatch> heroes = <Object, _HeroMatch>{};
|
||||
for (Object tag in heroesFrom.keys)
|
||||
heroes[tag] = new _HeroMatch(heroesFrom[tag], heroesTo[tag], tag);
|
||||
for (Object tag in heroesTo.keys) {
|
||||
if (!heroes.containsKey(tag))
|
||||
heroes[tag] = new _HeroMatch(heroesFrom[tag], heroesTo[tag], tag);
|
||||
}
|
||||
|
||||
// create a heroating hero out of each pair
|
||||
final List<_HeroQuestState> _newHeroes = <_HeroQuestState>[];
|
||||
for (_HeroMatch heroPair in heroes.values) {
|
||||
assert(heroPair.from != null || heroPair.to != null);
|
||||
_HeroManifest from = heroPair.from?._takeChild(animationArea);
|
||||
assert(heroPair.to == null || heroPair.to is HeroState);
|
||||
_HeroManifest to = heroPair.to?._takeChild(animationArea);
|
||||
assert(from != null || to != null);
|
||||
assert(to == null || to.sourceStates.length == 1);
|
||||
assert(to == null || to.currentTurns.floor() == to.currentTurns);
|
||||
HeroState targetState = to != null ? to.sourceStates.elementAt(0) : null;
|
||||
Set<HeroState> sourceStates = from != null ? from.sourceStates : new Set<HeroState>();
|
||||
sourceStates.remove(targetState);
|
||||
RelativeRect sourceRect = from != null ? from.currentRect :
|
||||
new RelativeRect.fromRect(to.currentRect.toRect(animationArea).center & Size.zero, animationArea);
|
||||
RelativeRect targetRect = to != null ? to.currentRect :
|
||||
new RelativeRect.fromRect(from.currentRect.toRect(animationArea).center & Size.zero, animationArea);
|
||||
double sourceTurns = from != null ? from.currentTurns : 0.0;
|
||||
double targetTurns = to != null ? to.currentTurns : 0.0;
|
||||
_newHeroes.add(new _HeroQuestState(
|
||||
tag: heroPair.tag,
|
||||
key: from != null ? from.key : to.key,
|
||||
child: to != null ? to.config : from.config,
|
||||
sourceStates: sourceStates,
|
||||
targetRect: targetRect,
|
||||
targetTurns: targetTurns.floor(),
|
||||
targetState: targetState,
|
||||
currentRect: createAnimatedRelativeRect(sourceRect, targetRect, curve),
|
||||
currentTurns: createAnimatedTurns(sourceTurns, targetTurns, curve)
|
||||
));
|
||||
}
|
||||
|
||||
assert(!_heroes.any((_HeroQuestState hero) => !hero.taken));
|
||||
_heroes = _newHeroes;
|
||||
}
|
||||
|
||||
PerformanceView _currentPerformance;
|
||||
|
||||
Iterable<Widget> getWidgets(BuildContext context, PerformanceView performance) sync* {
|
||||
assert(performance != null || _heroes.length == 0);
|
||||
if (performance != _currentPerformance) {
|
||||
if (_currentPerformance != null)
|
||||
_currentPerformance.removeStatusListener(_handleUpdate);
|
||||
_currentPerformance = performance;
|
||||
if (_currentPerformance != null)
|
||||
_currentPerformance.addStatusListener(_handleUpdate);
|
||||
}
|
||||
for (_HeroQuestState hero in _heroes)
|
||||
yield hero.build(context, performance);
|
||||
}
|
||||
|
||||
void _handleUpdate(PerformanceStatus status) {
|
||||
if (status == PerformanceStatus.completed ||
|
||||
status == PerformanceStatus.dismissed) {
|
||||
for (_HeroQuestState hero in _heroes) {
|
||||
if (hero.targetState != null)
|
||||
hero.targetState._setChild(hero.key);
|
||||
for (HeroState source in hero.sourceStates)
|
||||
source._resetChild();
|
||||
if (onQuestFinished != null)
|
||||
onQuestFinished();
|
||||
}
|
||||
_heroes.clear();
|
||||
_currentPerformance = null;
|
||||
}
|
||||
}
|
||||
|
||||
String toString() => '$_heroes';
|
||||
}
|
@ -8,6 +8,7 @@ import 'package:flutter/rendering.dart';
|
||||
import 'basic.dart';
|
||||
import 'focus.dart';
|
||||
import 'framework.dart';
|
||||
import 'heroes.dart';
|
||||
import 'transitions.dart';
|
||||
import 'gridpaper.dart';
|
||||
|
||||
@ -51,6 +52,40 @@ class Navigator extends StatefulComponent {
|
||||
|
||||
// The navigator tracks which "page" we are on.
|
||||
// It also animates between these pages.
|
||||
// Pages can have "heroes", which are UI elements that animate from point to point.
|
||||
// These animations are called journeys.
|
||||
//
|
||||
// Journeys can start in two conditions:
|
||||
// - Everything is calm, and we have no heroes in flight. In this case, we will
|
||||
// have to collect the heroes from the route we're starting at and the route
|
||||
// we're going to, and try to transition from one set to the other.
|
||||
// - We already have heroes in flight. In that case, we just want to look at
|
||||
// the heroes of our destination, and then try to transition to them from the
|
||||
// in-flight heroes.
|
||||
|
||||
class _HeroTransitionInstruction {
|
||||
Route from;
|
||||
Route to;
|
||||
void update(Route newFrom, Route newTo) {
|
||||
assert(newFrom != null);
|
||||
assert(newTo != null);
|
||||
if (!newFrom.canHaveHeroes || !newTo.canHaveHeroes)
|
||||
return;
|
||||
assert(newFrom.performance != null);
|
||||
assert(newTo.performance != null);
|
||||
if (from == null)
|
||||
from = newFrom;
|
||||
to = newTo;
|
||||
if (from == to)
|
||||
reset();
|
||||
}
|
||||
void reset() {
|
||||
assert(hasInstructions);
|
||||
from = null;
|
||||
to = null;
|
||||
}
|
||||
bool get hasInstructions => from != null || to != null;
|
||||
}
|
||||
|
||||
class NavigatorState extends State<Navigator> {
|
||||
|
||||
@ -62,6 +97,7 @@ class NavigatorState extends State<Navigator> {
|
||||
|
||||
void initState() {
|
||||
super.initState();
|
||||
_activeHeroes = new HeroParty(onQuestFinished: _handleHeroQuestFinished);
|
||||
PageRoute route = new PageRoute(config.routes[kDefaultRouteName], name: kDefaultRouteName);
|
||||
assert(route.hasContent);
|
||||
assert(!route.ephemeral);
|
||||
@ -76,14 +112,22 @@ class NavigatorState extends State<Navigator> {
|
||||
));
|
||||
}
|
||||
|
||||
void pushNamed(String name) {
|
||||
void pushNamed(String name, { Set<Key> mostValuableKeys }) {
|
||||
RouteBuilder generateRoute() {
|
||||
assert(config.onGenerateRoute != null);
|
||||
return config.onGenerateRoute(name);
|
||||
}
|
||||
final RouteBuilder builder = config.routes[name] ?? generateRoute() ?? config.onUnknownRoute;
|
||||
assert(builder != null); // 404 getting your 404!
|
||||
push(new PageRoute(builder, name: name));
|
||||
push(new PageRoute(builder, name: name, mostValuableKeys: mostValuableKeys));
|
||||
}
|
||||
|
||||
final _HeroTransitionInstruction _desiredHeroes = new _HeroTransitionInstruction();
|
||||
HeroParty _activeHeroes;
|
||||
|
||||
void _handleHeroQuestFinished() {
|
||||
for (Route route in _history)
|
||||
route._hasActiveHeroes = false;
|
||||
}
|
||||
|
||||
void push(Route route) {
|
||||
@ -94,6 +138,14 @@ class NavigatorState extends State<Navigator> {
|
||||
currentRoute.didPop(null);
|
||||
_currentPosition -= 1;
|
||||
}
|
||||
// find the most recent active route that might have heroes
|
||||
if (route.hasContent) {
|
||||
int index = _currentPosition;
|
||||
while (index > 0 && !_history[index].hasContent)
|
||||
index -= 1;
|
||||
assert(_history[index].hasContent);
|
||||
_desiredHeroes.update(_history[index], route);
|
||||
}
|
||||
// add the new route
|
||||
_currentPosition += 1;
|
||||
_insertRoute(route);
|
||||
@ -118,6 +170,14 @@ class NavigatorState extends State<Navigator> {
|
||||
void pop([dynamic result]) {
|
||||
setState(() {
|
||||
assert(_currentPosition > 0);
|
||||
// find the most recent previous route that might have heroes
|
||||
if (currentRoute.hasContent) {
|
||||
int index = _currentPosition - 1;
|
||||
while (index > 0 && !_history[index].hasContent)
|
||||
index -= 1;
|
||||
assert(_history[index].hasContent);
|
||||
_desiredHeroes.update(currentRoute, _history[index]);
|
||||
}
|
||||
// pop the route
|
||||
currentRoute.didPop(result);
|
||||
_currentPosition -= 1;
|
||||
@ -153,11 +213,17 @@ class NavigatorState extends State<Navigator> {
|
||||
void _removeRoute(Route route) {
|
||||
assert(_history.contains(route));
|
||||
setState(() {
|
||||
if (_desiredHeroes.hasInstructions) {
|
||||
if (_desiredHeroes.from == route || _desiredHeroes.to == route)
|
||||
_desiredHeroes.reset();
|
||||
}
|
||||
_history.remove(route);
|
||||
});
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
PerformanceView _currentHeroPerformance;
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> visibleRoutes = <Widget>[];
|
||||
|
||||
assert(() {
|
||||
@ -166,22 +232,35 @@ class NavigatorState extends State<Navigator> {
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
bool alreadyInsertedHeroes = false;
|
||||
bool alreadyInsertedModalBarrier = false;
|
||||
Route nextContentRoute;
|
||||
PerformanceView nextHeroPerformance;
|
||||
for (int i = _history.length-1; i >= 0; i -= 1) {
|
||||
Route route = _history[i];
|
||||
final Route route = _history[i];
|
||||
if (!route.hasContent) {
|
||||
assert(!route.modal);
|
||||
assert(!_desiredHeroes.hasInstructions || (_desiredHeroes.from != route && _desiredHeroes.to != route));
|
||||
assert(!route._hasActiveHeroes);
|
||||
continue;
|
||||
}
|
||||
visibleRoutes.add(
|
||||
new KeyedSubtree(
|
||||
key: new ObjectKey(route),
|
||||
child: route._internalBuild(nextContentRoute)
|
||||
)
|
||||
);
|
||||
if (route.isActuallyOpaque)
|
||||
if (route._hasActiveHeroes && !alreadyInsertedHeroes) {
|
||||
visibleRoutes.addAll(_activeHeroes.getWidgets(context, _currentHeroPerformance));
|
||||
alreadyInsertedHeroes = true;
|
||||
}
|
||||
if (_desiredHeroes.hasInstructions) {
|
||||
if ((_desiredHeroes.to == route || _desiredHeroes.from == route) && nextHeroPerformance == null)
|
||||
nextHeroPerformance = route.performance;
|
||||
visibleRoutes.add(route._internalBuild(nextContentRoute, buildTargetHeroes: _desiredHeroes.to == route));
|
||||
} else {
|
||||
visibleRoutes.add(route._internalBuild(nextContentRoute));
|
||||
}
|
||||
if (route.isActuallyOpaque) {
|
||||
assert(!_desiredHeroes.hasInstructions ||
|
||||
(_history.indexOf(_desiredHeroes.from) >= i && _history.indexOf(_desiredHeroes.to) >= i));
|
||||
break;
|
||||
}
|
||||
assert(route.modal || route.ephemeral);
|
||||
if (route.modal && i > 0 && !alreadyInsertedModalBarrier) {
|
||||
visibleRoutes.add(new Listener(
|
||||
@ -192,12 +271,54 @@ class NavigatorState extends State<Navigator> {
|
||||
}
|
||||
nextContentRoute = route;
|
||||
}
|
||||
|
||||
if (_desiredHeroes.hasInstructions) {
|
||||
assert(nextHeroPerformance != null);
|
||||
scheduler.requestPostFrameCallback((Duration timestamp) {
|
||||
Map<Object, HeroHandle> heroesFrom;
|
||||
Map<Object, HeroHandle> heroesTo;
|
||||
Set<Key> mostValuableKeys = new Set<Key>();
|
||||
if (_desiredHeroes.from.mostValuableKeys != null)
|
||||
mostValuableKeys.addAll(_desiredHeroes.from.mostValuableKeys);
|
||||
if (_desiredHeroes.to.mostValuableKeys != null)
|
||||
mostValuableKeys.addAll(_desiredHeroes.to.mostValuableKeys);
|
||||
if (_activeHeroes.isEmpty) {
|
||||
assert(!_desiredHeroes.from._hasActiveHeroes);
|
||||
heroesFrom = _desiredHeroes.from.getHeroesToAnimate(mostValuableKeys);
|
||||
_desiredHeroes.from._hasActiveHeroes = heroesFrom.length > 0;
|
||||
} else {
|
||||
assert(_desiredHeroes.from._hasActiveHeroes);
|
||||
heroesFrom = _activeHeroes.getHeroesToAnimate();
|
||||
}
|
||||
heroesTo = _desiredHeroes.to.getHeroesToAnimate(mostValuableKeys);
|
||||
_desiredHeroes.to._hasActiveHeroes = heroesTo.length > 0;
|
||||
_desiredHeroes.reset();
|
||||
setState(() {
|
||||
final RenderBox renderObject = context.findRenderObject();
|
||||
final Point animationTopLeft = renderObject.localToGlobal(Point.origin);
|
||||
final Point animationBottomRight = renderObject.localToGlobal(renderObject.size.bottomRight(Point.origin));
|
||||
final Rect animationArea = new Rect.fromLTRB(animationTopLeft.x, animationTopLeft.y, animationBottomRight.x, animationBottomRight.y);
|
||||
Curve curve = Curves.ease;
|
||||
if (nextHeroPerformance.status == PerformanceStatus.reverse) {
|
||||
nextHeroPerformance = new ReversePerformance(nextHeroPerformance);
|
||||
curve = new Interval(nextHeroPerformance.progress, 1.0, curve: curve);
|
||||
}
|
||||
_activeHeroes.animate(heroesFrom, heroesTo, animationArea, curve);
|
||||
_currentHeroPerformance = nextHeroPerformance;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return new Focus(child: new Stack(visibleRoutes.reversed.toList()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
abstract class Route {
|
||||
Route() {
|
||||
_subtreeKey = new GlobalKey(label: debugLabel);
|
||||
}
|
||||
|
||||
/// If hasContent is true, then the route represents some on-screen state.
|
||||
///
|
||||
/// If hasContent is false, then no performance will be created, and the values of
|
||||
@ -284,21 +405,56 @@ abstract class Route {
|
||||
|
||||
/// Called by the navigator.build() function if hasContent is true, to get the
|
||||
/// subtree for this route.
|
||||
Widget _internalBuild(Route nextRoute) {
|
||||
///
|
||||
/// 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 }) {
|
||||
assert(navigator != null);
|
||||
return build(new RouteArguments(
|
||||
return keySubtree(build(new RouteArguments(
|
||||
navigator,
|
||||
previousPerformance: performance,
|
||||
nextPerformance: nextRoute?.performance
|
||||
));
|
||||
)));
|
||||
}
|
||||
|
||||
bool get canHaveHeroes => hasContent && modal && opaque;
|
||||
Set<Key> get mostValuableKeys => null;
|
||||
|
||||
/// Return a party of heroes (one per tag) to animate. This is called by the
|
||||
/// navigator when hasContent is true just after this route, the previous
|
||||
/// route, or the next route, has been pushed or popped, to figure out which
|
||||
/// heroes it should be trying to animate.
|
||||
Map<Object, HeroHandle> getHeroesToAnimate([Set<Key> mostValuableKeys]) => const <Object, HeroHandle>{};
|
||||
bool _hasActiveHeroes = false;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// Wraps the given subtree in a route-specific GlobalKey.
|
||||
Widget keySubtree(Widget child) {
|
||||
return new KeyedSubtree(
|
||||
key: _subtreeKey,
|
||||
child: child
|
||||
);
|
||||
}
|
||||
|
||||
/// Called by internalBuild. This is the method to override if you want to
|
||||
/// change what subtree is built for this route.
|
||||
Widget build(RouteArguments args);
|
||||
|
||||
String get debugLabel => '$runtimeType';
|
||||
String toString() => '$runtimeType(performance: $performance)';
|
||||
|
||||
String toString() => '$runtimeType(performance: $performance; key: $_subtreeKey)';
|
||||
}
|
||||
|
||||
|
||||
abstract class PerformanceRoute extends Route {
|
||||
PerformanceRoute() {
|
||||
_performance = createPerformance();
|
||||
@ -315,6 +471,22 @@ abstract class PerformanceRoute extends Route {
|
||||
|
||||
Duration get transitionDuration;
|
||||
|
||||
Widget _internalBuild(Route nextRoute, { bool buildTargetHeroes: false }) {
|
||||
assert(hasContent);
|
||||
assert(transitionDuration > Duration.ZERO);
|
||||
if (buildTargetHeroes && performance.progress != 1.0) {
|
||||
Performance fakePerformance = createPerformance();
|
||||
assert(fakePerformance != null);
|
||||
fakePerformance.progress = 1.0;
|
||||
return new OffStage(
|
||||
child: keySubtree(
|
||||
build(new RouteArguments(navigator, previousPerformance: fakePerformance))
|
||||
)
|
||||
);
|
||||
}
|
||||
return super._internalBuild(nextRoute, buildTargetHeroes: buildTargetHeroes);
|
||||
}
|
||||
|
||||
void didPush(NavigatorState navigator) {
|
||||
super.didPush(navigator);
|
||||
_performance?.forward();
|
||||
@ -330,27 +502,39 @@ const Duration _kTransitionDuration = const Duration(milliseconds: 150);
|
||||
const Point _kTransitionStartPoint = const Point(0.0, 75.0);
|
||||
|
||||
/// A route that represents a page in an application.
|
||||
///
|
||||
/// PageRoutes try to animate between themselves in a fashion that is aware of
|
||||
/// any Heroes.
|
||||
class PageRoute extends PerformanceRoute {
|
||||
PageRoute(this._builder, {
|
||||
this.name: '<anonymous>'
|
||||
}) {
|
||||
this.name: '<anonymous>',
|
||||
Set<Key> mostValuableKeys
|
||||
}) : _mostValuableKeys = mostValuableKeys {
|
||||
assert(_builder != null);
|
||||
}
|
||||
|
||||
final RouteBuilder _builder;
|
||||
final String name;
|
||||
final Set<Key> _mostValuableKeys;
|
||||
|
||||
Set<Key> get mostValuableKeys => _mostValuableKeys;
|
||||
|
||||
bool get opaque => true;
|
||||
Duration get transitionDuration => _kTransitionDuration;
|
||||
|
||||
Map<Object, HeroHandle> getHeroesToAnimate([Set<Key> mostValuableKeys]) {
|
||||
return Hero.of(context, mostValuableKeys);
|
||||
}
|
||||
|
||||
Widget build(RouteArguments args) {
|
||||
// TODO(jackson): Hit testing should ignore transform
|
||||
// TODO(jackson): Block input unless content is interactive
|
||||
// TODO(ianh): Support having different transitions, e.g. when heroes are around.
|
||||
return new SlideTransition(
|
||||
performance: performance,
|
||||
performance: args.previousPerformance,
|
||||
position: new AnimatedValue<Point>(_kTransitionStartPoint, end: Point.origin, curve: Curves.easeOut),
|
||||
child: new FadeTransition(
|
||||
performance: performance,
|
||||
performance: args.previousPerformance,
|
||||
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: Curves.easeOut),
|
||||
child: invokeBuilder(args)
|
||||
)
|
||||
|
@ -16,6 +16,7 @@ export 'src/widgets/focus.dart';
|
||||
export 'src/widgets/framework.dart';
|
||||
export 'src/widgets/gesture_detector.dart';
|
||||
export 'src/widgets/gridpaper.dart';
|
||||
export 'src/widgets/heroes.dart';
|
||||
export 'src/widgets/homogeneous_viewport.dart';
|
||||
export 'src/widgets/mimic.dart';
|
||||
export 'src/widgets/mixed_viewport.dart';
|
||||
|
Loading…
Reference in New Issue
Block a user