Commit 7dc7f16a authored by Collin Jackson's avatar Collin Jackson

Merge pull request #741 from collinjackson/hero_wip

Support for hero transitions underneath the target page
parents 519b190c c253ca62
// Copyright (c) 2015, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:ui' as ui;
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
void main() {
timeDilation = 8.0;
runApp(
new MaterialApp(
title: "Hero Under",
routes: {
'/': (RouteArguments args) => new HeroDemo()
}
)
);
}
const String kImageSrc = 'http://uploads0.wikiart.org/images/m-c-escher/crab-canon.jpg!Blog.jpg';
const String kText = """
Low-crab diets are dietary programs that restrict crustacean consumption, often for the treatment of obesity or diabetes. Foods high in easily digestible crustaceans (e.g., crab, lobster, shrimp) are limited or replaced with foods made from other animals (e.g., poultry, beef, pork) and other crustaceans that are hard to digest (e.g., barnacles), although krill are often allowed. The amount of crab allowed varies with different low-crab diets.
""";
class HeroImage extends StatelessComponent {
HeroImage({ this.size });
final Size size;
Widget build(BuildContext context) {
return new Hero(
child: new Container(
width: size.width,
height: size.height,
decoration: new BoxDecoration(
backgroundImage: new BackgroundImage(
fit: ImageFit.cover,
image: imageCache.load(kImageSrc)
)
)
),
tag: HeroImage
);
}
}
class HeroDemo extends StatelessComponent {
Widget build(BuildContext context) {
return new Scaffold(
toolBar: new ToolBar(
left: new IconButton(icon: "navigation/menu"),
center: new Text("Diets")
),
body: new Center(
child: new GestureDetector(
onTap: () => Navigator.push(context, new CrabRoute()),
child: new Card(
child: new Row(<Widget>[
new HeroImage(
size: const Size(100.0, 100.0)
),
new Flexible(
child: new Container(
padding: const EdgeDims.all(10.0),
child: new Text(
"Low Crab Diet",
style: Theme.of(context).text.title
)
)
)
])
)
)
)
);
}
}
class CrabRoute extends MaterialPageRoute {
CrabRoute() : super(builder: (BuildContext context) => new CrabPage());
void insertHeroOverlayEntry(OverlayEntry entry, Object tag, OverlayState overlay) {
overlay.insert(entry, above: overlayEntries.first);
}
}
class CrabPage extends StatelessComponent {
Widget build(BuildContext context) {
TextStyle titleStyle = Typography.white.display2.copyWith(color: Colors.white);
return new Material(
color: const Color(0x00000000),
child: new Block(
<Widget>[
new Stack(<Widget>[
new HeroImage(
size: new Size(ui.window.size.width, ui.window.size.width)
),
new ToolBar(
padding: new EdgeDims.only(top: ui.window.padding.top),
backgroundColor: const Color(0x00000000),
left: new IconButton(
icon: "navigation/arrow_back",
onPressed: () => Navigator.pop(context)
),
right: <Widget>[
new IconButton(icon: "navigation/more_vert")
]
),
new Positioned(
bottom: 10.0,
left: 10.0,
child: new Text("Low Crab Diet", style: titleStyle)
)
]),
new Material(
child: new Container(
padding: const EdgeDims.all(10.0),
child: new Column(<Widget>[
new Text(kText, style: Theme.of(context).text.body1),
new Container(height: 800.0),
], alignItems: FlexAlignItems.start)
)
)
]
)
);
}
}
// 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 'package:flutter/scheduler.dart';
import 'basic.dart';
import 'framework.dart';
import 'heroes.dart';
import 'navigator.dart';
import 'overlay.dart';
import 'routes.dart';
class HeroController extends NavigatorObserver {
HeroController() {
_party = new HeroParty(onQuestFinished: _handleQuestFinished);
}
HeroParty _party;
PerformanceView _performance;
ModalRoute _from;
ModalRoute _to;
final List<OverlayEntry> _overlayEntries = new List<OverlayEntry>();
void didPush(Route route, Route previousRoute) {
assert(navigator != null);
assert(route != null);
if (route is PageRoute) {
assert(route.performance != null);
if (previousRoute is PageRoute) // could be null
_from = previousRoute;
_to = route;
_performance = route.performance;
_checkForHeroQuest();
}
}
void didPop(Route route, Route previousRoute) {
assert(navigator != null);
assert(route != null);
if (route is PageRoute) {
assert(route.performance != null);
if (previousRoute is PageRoute) {
_to = previousRoute;
_from = route;
_performance = route.performance;
_checkForHeroQuest();
}
}
}
void _checkForHeroQuest() {
if (_from != null && _to != null && _from != _to) {
_to.offstage = _to.performance.status != PerformanceStatus.completed;
scheduler.addPostFrameCallback(_updateQuest);
}
}
void _handleQuestFinished() {
_removeHeroesFromOverlay();
_from = null;
_to = null;
_performance = null;
}
Rect _getAnimationArea(BuildContext context) {
RenderBox box = context.findRenderObject();
Point topLeft = box.localToGlobal(Point.origin);
Point bottomRight = box.localToGlobal(box.size.bottomRight(Point.origin));
return new Rect.fromLTRB(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y);
}
void _removeHeroesFromOverlay() {
for (OverlayEntry entry in _overlayEntries)
entry.remove();
_overlayEntries.clear();
}
void _addHeroesToOverlay(Iterable<Widget> heroes, OverlayState overlay) {
for (Widget hero in heroes) {
OverlayEntry entry = new OverlayEntry(builder: (_) => hero);
overlay.insert(entry);
_overlayEntries.add(entry);
}
}
Set<Key> _getMostValuableKeys() {
assert(_from != null);
assert(_to != null);
Set<Key> result = new Set<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) {
Set<Key> mostValuableKeys = _getMostValuableKeys();
Map<Object, HeroHandle> heroesFrom = _party.isEmpty ?
Hero.of(_from.subtreeContext, mostValuableKeys) : _party.getHeroesToAnimate();
Map<Object, HeroHandle> heroesTo = Hero.of(_to.subtreeContext, mostValuableKeys);
_to.offstage = false;
PerformanceView performance = _performance;
Curve curve = Curves.ease;
if (performance.status == PerformanceStatus.reverse) {
performance = new ReversePerformance(performance);
curve = new Interval(performance.progress, 1.0, curve: curve);
}
_party.animate(heroesFrom, heroesTo, _getAnimationArea(navigator.context), curve);
_removeHeroesFromOverlay();
Iterable<Widget> heroes = _party.getWidgets(navigator.context, performance);
_addHeroesToOverlay(heroes, navigator.overlay);
}
}
......@@ -4,9 +4,13 @@
import 'package:flutter/animation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'basic.dart';
import 'framework.dart';
import 'navigator.dart';
import 'overlay.dart';
import 'pages.dart';
import 'transitions.dart';
// Heroes are the parts of an application's screen-to-screen transitions where a
......@@ -383,15 +387,13 @@ class HeroParty {
_currentPerformance = null;
}
Iterable<Widget> getWidgets(BuildContext context, PerformanceView performance) sync* {
void setPerformance(PerformanceView performance) {
assert(performance != null || _heroes.length == 0);
if (performance != _currentPerformance) {
_clearCurrentPerformance();
_currentPerformance = performance;
_currentPerformance?.addStatusListener(_handleUpdate);
}
for (_HeroQuestState hero in _heroes)
yield hero.build(context, performance);
}
void _handleUpdate(PerformanceStatus status) {
......@@ -412,3 +414,114 @@ class HeroParty {
String toString() => '$_heroes';
}
class HeroController extends NavigatorObserver {
HeroController() {
_party = new HeroParty(onQuestFinished: _handleQuestFinished);
}
HeroParty _party;
PerformanceView _performance;
PageRoute _from;
PageRoute _to;
final List<OverlayEntry> _overlayEntries = new List<OverlayEntry>();
void didPush(Route route, Route previousRoute) {
assert(navigator != null);
assert(route != null);
if (route is PageRoute) {
assert(route.performance != null);
if (previousRoute is PageRoute) // could be null
_from = previousRoute;
_to = route;
_performance = route.performance;
_checkForHeroQuest();
}
}
void didPop(Route route, Route previousRoute) {
assert(navigator != null);
assert(route != null);
if (route is PageRoute) {
assert(route.performance != null);
if (previousRoute is PageRoute) {
_to = previousRoute;
_from = route;
_performance = route.performance;
_checkForHeroQuest();
}
}
}
void _checkForHeroQuest() {
if (_from != null && _to != null && _from != _to) {
_to.offstage = _to.performance.status != PerformanceStatus.completed;
scheduler.addPostFrameCallback(_updateQuest);
}
}
void _handleQuestFinished() {
_removeHeroesFromOverlay();
_from = null;
_to = null;
_performance = null;
}
Rect _getAnimationArea(BuildContext context) {
RenderBox box = context.findRenderObject();
Point topLeft = box.localToGlobal(Point.origin);
Point bottomRight = box.localToGlobal(box.size.bottomRight(Point.origin));
return new Rect.fromLTRB(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y);
}
void _removeHeroesFromOverlay() {
for (OverlayEntry entry in _overlayEntries)
entry.remove();
_overlayEntries.clear();
}
void _addHeroToOverlay(Widget hero, Object tag, OverlayState overlay) {
OverlayEntry entry = new OverlayEntry(builder: (_) => hero);
if (_performance.direction == AnimationDirection.forward)
_to.insertHeroOverlayEntry(entry, tag, overlay);
else
_from.insertHeroOverlayEntry(entry, tag, overlay);
_overlayEntries.add(entry);
}
Set<Key> _getMostValuableKeys() {
assert(_from != null);
assert(_to != null);
Set<Key> result = new Set<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) {
Set<Key> mostValuableKeys = _getMostValuableKeys();
Map<Object, HeroHandle> heroesFrom = _party.isEmpty ?
Hero.of(_from.subtreeContext, mostValuableKeys) : _party.getHeroesToAnimate();
Map<Object, HeroHandle> heroesTo = Hero.of(_to.subtreeContext, mostValuableKeys);
_to.offstage = false;
PerformanceView performance = _performance;
Curve curve = Curves.ease;
if (performance.status == PerformanceStatus.reverse) {
performance = new ReversePerformance(performance);
curve = new Interval(performance.progress, 1.0, curve: curve);
}
_party.animate(heroesFrom, heroesTo, _getAnimationArea(navigator.context), curve);
_removeHeroesFromOverlay();
_party.setPerformance(performance);
for (_HeroQuestState hero in _party._heroes) {
Widget widget = hero.build(navigator.context, performance);
_addHeroToOverlay(widget, hero.tag, navigator.overlay);
}
}
}
// 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 'dart:async';
import 'navigator.dart';
import 'overlay.dart';
import 'routes.dart';
/// A modal route that replaces the entire screen.
abstract class PageRoute<T> extends ModalRoute<T> {
PageRoute({
Completer<T> completer,
NamedRouteSettings settings: const NamedRouteSettings()
}) : super(completer: completer, settings: settings);
bool get opaque => true;
bool get barrierDismissable => false;
bool canTransitionTo(TransitionRoute nextRoute) => nextRoute is PageRoute;
bool canTransitionFrom(TransitionRoute nextRoute) => nextRoute is PageRoute;
// Subclasses can override this method to customize way heroes are inserted
void insertHeroOverlayEntry(OverlayEntry entry, Object tag, OverlayState overlay) {
overlay.insert(entry);
}
}
......@@ -13,6 +13,7 @@ import 'modal_barrier.dart';
import 'navigator.dart';
import 'overlay.dart';
import 'page_storage.dart';
import 'pages.dart';
const _kTransparent = const Color(0x00000000);
......@@ -458,15 +459,3 @@ abstract class PopupRoute<T> extends ModalRoute<T> {
super.didPushNext(nextRoute);
}
}
/// A modal route that replaces the entire screen.
abstract class PageRoute<T> extends ModalRoute<T> {
PageRoute({
Completer<T> completer,
NamedRouteSettings settings: const NamedRouteSettings()
}) : super(completer: completer, settings: settings);
bool get opaque => true;
bool get barrierDismissable => false;
bool canTransitionTo(TransitionRoute nextRoute) => nextRoute is PageRoute;
bool canTransitionFrom(TransitionRoute nextRoute) => nextRoute is PageRoute;
}
......@@ -17,7 +17,6 @@ export 'src/widgets/focus.dart';
export 'src/widgets/framework.dart';
export 'src/widgets/gesture_detector.dart';
export 'src/widgets/gridpaper.dart';
export 'src/widgets/hero_controller.dart';
export 'src/widgets/heroes.dart';
export 'src/widgets/homogeneous_viewport.dart';
export 'src/widgets/media_query.dart';
......@@ -28,6 +27,7 @@ export 'src/widgets/navigator.dart';
export 'src/widgets/notification_listener.dart';
export 'src/widgets/overlay.dart';
export 'src/widgets/page_storage.dart';
export 'src/widgets/pages.dart';
export 'src/widgets/placeholder.dart';
export 'src/widgets/routes.dart';
export 'src/widgets/scrollable.dart';
......
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