提交 e2b78f7b 编写于 作者: I Ian Hickson

Merge pull request #1720 from Hixie/heroes

Heroes
......@@ -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 {
)
)
)
])
]
)
);
}
......
// 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';
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册