buildForwardTransition()

For those times when you want to do something as you move away from a
route into the next one, as well as when you move into it from the
previous one.
This commit is contained in:
Hixie 2015-12-02 10:00:26 -08:00
parent 9d86ac5e35
commit bc5307f5af
11 changed files with 465 additions and 16 deletions

View file

@ -8,6 +8,7 @@ import 'dart:async';
import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:flutter/animation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/painting.dart';

View file

@ -146,13 +146,22 @@ class StockHomeState extends State<StockHome> {
}
Widget buildToolBar() {
PageRoute page = ModalRoute.of(context);
return new ToolBar(
elevation: 0,
left: new IconButton(
icon: "navigation/menu",
onPressed: _showDrawer
),
center: new Text('Stocks'),
center: new FadeTransition(
opacity: new AnimatedValue<double>(
1.0,
end: 0.0,
curve: const Interval(0.0, 0.5)
),
performance: page.forwardPerformance,
child: new Text('Stocks')
),
right: <Widget>[
new IconButton(
icon: "action/search",

View file

@ -53,6 +53,7 @@ class StockSymbolPage extends StatelessComponent {
final Stock stock;
Widget build(BuildContext context) {
PageRoute page = ModalRoute.of(context);
return new Scaffold(
toolBar: new ToolBar(
left: new IconButton(
@ -61,7 +62,15 @@ class StockSymbolPage extends StatelessComponent {
Navigator.pop(context);
}
),
center: new Text(stock.name)
center: new FadeTransition(
opacity: new AnimatedValue<double>(
0.0,
end: 1.0,
curve: const Interval(0.5, 1.0)
),
performance: page.performance,
child: new Text(stock.name)
)
),
body: new Block(<Widget>[
new Container(

View file

@ -45,6 +45,7 @@ class Interval implements Curve {
assert(start <= 1.0);
assert(end >= 0.0);
assert(end <= 1.0);
assert(end >= start);
t = ((t - start) / (end - start)).clamp(0.0, 1.0);
if (t == 0.0 || t == 1.0)
return t;

View file

@ -109,12 +109,10 @@ class ReversePerformance extends PerformanceView {
final List<PerformanceStatusListener> _statusListeners = new List<PerformanceStatusListener>();
/// Calls listener every time the status of this performance changes
void addStatusListener(PerformanceStatusListener listener) {
_statusListeners.add(listener);
}
/// Stops calling the listener every time the status of this performance changes
void removeStatusListener(PerformanceStatusListener listener) {
_statusListeners.remove(listener);
}
@ -148,6 +146,217 @@ class ReversePerformance extends PerformanceView {
}
}
enum _TrainHoppingMode { minimize, maximize }
/// This performance starts by proxying one performance, but can be given a
/// second performance. When their times cross (either because the second is
/// going in the opposite direction, or because the one overtakes the other),
/// the performance hops over to proxying the second performance, and the second
/// performance becomes the new "first" performance.
class TrainHoppingPerformance extends PerformanceView {
TrainHoppingPerformance(this._currentTrain, this._nextTrain, { this.onSwitchedTrain }) {
assert(_currentTrain != null);
if (_nextTrain != null) {
if (_currentTrain.progress > _nextTrain.progress) {
_mode = _TrainHoppingMode.maximize;
} else {
_mode = _TrainHoppingMode.minimize;
if (_currentTrain.progress == _nextTrain.progress) {
_currentTrain = _nextTrain;
_nextTrain = null;
}
}
}
_currentTrain.addStatusListener(_statusChangeHandler);
_currentTrain.addListener(_valueChangeHandler);
if (_nextTrain != null)
_nextTrain.addListener(_valueChangeHandler);
assert(_mode != null);
}
PerformanceView get currentTrain => _currentTrain;
PerformanceView _currentTrain;
PerformanceView _nextTrain;
_TrainHoppingMode _mode;
VoidCallback onSwitchedTrain;
void updateVariable(Animatable variable) {
assert(_currentTrain != null);
variable.setProgress(progress, curveDirection);
}
final List<VoidCallback> _listeners = new List<VoidCallback>();
void addListener(VoidCallback listener) {
assert(_currentTrain != null);
_listeners.add(listener);
}
void removeListener(VoidCallback listener) {
assert(_currentTrain != null);
_listeners.remove(listener);
}
final List<PerformanceStatusListener> _statusListeners = new List<PerformanceStatusListener>();
void addStatusListener(PerformanceStatusListener listener) {
assert(_currentTrain != null);
_statusListeners.add(listener);
}
void removeStatusListener(PerformanceStatusListener listener) {
assert(_currentTrain != null);
_statusListeners.remove(listener);
}
PerformanceStatus _lastStatus;
void _statusChangeHandler(PerformanceStatus status) {
assert(_currentTrain != null);
if (status != _lastStatus) {
List<PerformanceStatusListener> localListeners = new List<PerformanceStatusListener>.from(_statusListeners);
for (PerformanceStatusListener listener in localListeners)
listener(status);
_lastStatus = status;
}
assert(_lastStatus != null);
}
PerformanceStatus get status => _currentTrain.status;
AnimationDirection get direction => _currentTrain.direction;
AnimationDirection get curveDirection => _currentTrain.curveDirection;
double _lastProgress;
void _valueChangeHandler() {
assert(_currentTrain != null);
bool hop = false;
if (_nextTrain != null) {
switch (_mode) {
case _TrainHoppingMode.minimize:
hop = _nextTrain.progress <= _currentTrain.progress;
break;
case _TrainHoppingMode.maximize:
hop = _nextTrain.progress >= _currentTrain.progress;
break;
}
if (hop) {
_currentTrain.removeStatusListener(_statusChangeHandler);
_currentTrain.removeListener(_valueChangeHandler);
_currentTrain = _nextTrain;
_nextTrain.addListener(_valueChangeHandler);
_statusChangeHandler(_nextTrain.status);
}
}
double newProgress = progress;
if (newProgress != _lastProgress) {
List<VoidCallback> localListeners = new List<VoidCallback>.from(_listeners);
for (VoidCallback listener in localListeners)
listener();
_lastProgress = newProgress;
}
assert(_lastProgress != null);
if (hop && onSwitchedTrain != null)
onSwitchedTrain();
}
double get progress => _currentTrain.progress;
/// Frees all the resources used by this performance.
/// After this is called, this object is no longer usable.
void dispose() {
assert(_currentTrain != null);
_currentTrain.removeStatusListener(_statusChangeHandler);
_currentTrain.removeListener(_valueChangeHandler);
_currentTrain = null;
if (_nextTrain != null) {
_nextTrain.removeListener(_valueChangeHandler);
_nextTrain = null;
}
}
}
class ProxyPerformance extends PerformanceView {
ProxyPerformance([PerformanceView performance]) {
masterPerformance = performance;
}
PerformanceView get masterPerformance => _masterPerformance;
PerformanceView _masterPerformance;
void set masterPerformance(PerformanceView value) {
if (value == _masterPerformance)
return;
if (_masterPerformance != null) {
_masterPerformance.removeStatusListener(_statusChangeHandler);
_masterPerformance.removeListener(_valueChangeHandler);
}
_masterPerformance = value;
if (_masterPerformance != null) {
_masterPerformance.addListener(_valueChangeHandler);
_masterPerformance.addStatusListener(_statusChangeHandler);
_valueChangeHandler();
_statusChangeHandler(_masterPerformance.status);
}
}
void updateVariable(Animatable variable) {
variable.setProgress(progress, curveDirection);
}
final List<VoidCallback> _listeners = new List<VoidCallback>();
void addListener(VoidCallback listener) {
_listeners.add(listener);
}
void removeListener(VoidCallback listener) {
_listeners.remove(listener);
}
final List<PerformanceStatusListener> _statusListeners = new List<PerformanceStatusListener>();
void addStatusListener(PerformanceStatusListener listener) {
_statusListeners.add(listener);
}
void removeStatusListener(PerformanceStatusListener listener) {
_statusListeners.remove(listener);
}
PerformanceStatus _status = PerformanceStatus.dismissed;
AnimationDirection _direction = AnimationDirection.forward;
AnimationDirection _curveDirection = AnimationDirection.forward;
void _statusChangeHandler(PerformanceStatus status) {
assert(_masterPerformance != null);
if (status != _status) {
_status = status;
_direction = _masterPerformance.direction;
List<PerformanceStatusListener> localListeners = new List<PerformanceStatusListener>.from(_statusListeners);
for (PerformanceStatusListener listener in localListeners)
listener(status);
}
}
PerformanceStatus get status => _status;
AnimationDirection get direction => _direction;
AnimationDirection get curveDirection => _curveDirection;
double _progress = 0.0;
void _valueChangeHandler() {
assert(_masterPerformance != null);
double newProgress = _masterPerformance.progress;
if (newProgress != _progress) {
_progress = newProgress;
_curveDirection = _masterPerformance.curveDirection;
List<VoidCallback> localListeners = new List<VoidCallback>.from(_listeners);
for (VoidCallback listener in localListeners)
listener();
}
}
double get progress => _progress;
}
class _RepeatingSimulation extends Simulation {
_RepeatingSimulation(this.min, this.max, Duration period)
: _periodInSeconds = period.inMicroseconds.toDouble() / Duration.MICROSECONDS_PER_SECOND {
@ -280,12 +489,10 @@ class Performance extends PerformanceView {
final List<VoidCallback> _listeners = new List<VoidCallback>();
/// Calls the listener every time the progress of this performance changes
void addListener(VoidCallback listener) {
_listeners.add(listener);
}
/// Stop calling the listener every time the progress of this performance changes
void removeListener(VoidCallback listener) {
_listeners.remove(listener);
}
@ -298,12 +505,10 @@ class Performance extends PerformanceView {
final List<PerformanceStatusListener> _statusListeners = new List<PerformanceStatusListener>();
/// Calls listener every time the status of this performance changes
void addStatusListener(PerformanceStatusListener listener) {
_statusListeners.add(listener);
}
/// Stops calling the listener every time the status of this performance changes
void removeStatusListener(PerformanceStatusListener listener) {
_statusListeners.remove(listener);
}

View file

@ -38,6 +38,8 @@ class _MaterialPageTransition extends TransitionWithChild {
}
}
const Duration kMaterialPageRouteTransitionDuration = const Duration(milliseconds: 150);
class MaterialPageRoute<T> extends PageRoute<T> {
MaterialPageRoute({
this.builder,
@ -49,7 +51,7 @@ class MaterialPageRoute<T> extends PageRoute<T> {
final WidgetBuilder builder;
Duration get transitionDuration => const Duration(milliseconds: 150);
Duration get transitionDuration => kMaterialPageRouteTransitionDuration;
bool get barrierDismissable => false;
Color get barrierColor => Colors.black54;

View file

@ -72,6 +72,9 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
/// popped.
Future<T> get popped => _popCompleter?.future;
final Completer<T> _popCompleter;
/// This future completes only once the transition itself has finished, after
/// the overlay entries have been removed from the navigator's overlay.
Future<T> get completed => _transitionCompleter?.future;
final Completer<T> _transitionCompleter;
@ -143,6 +146,52 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
super.dispose();
}
final ProxyPerformance forwardPerformance = new ProxyPerformance();
void didPushNext(Route nextRoute) {
if (nextRoute is TransitionRoute) {
PerformanceView current = forwardPerformance.masterPerformance;
if (current != null) {
if (current is TrainHoppingPerformance) {
TrainHoppingPerformance newPerformance;
newPerformance = new TrainHoppingPerformance(
current.currentTrain,
nextRoute.performance,
onSwitchedTrain: () {
assert(forwardPerformance.masterPerformance == newPerformance);
assert(newPerformance.currentTrain == nextRoute.performance);
forwardPerformance.masterPerformance = newPerformance.currentTrain;
newPerformance.dispose();
}
);
forwardPerformance.masterPerformance = newPerformance;
current.dispose();
} else {
forwardPerformance.masterPerformance = new TrainHoppingPerformance(current, nextRoute.performance);
}
} else {
forwardPerformance.masterPerformance = nextRoute.performance;
}
}
super.didPushNext(nextRoute);
}
Widget wrapTransition(BuildContext context, Widget child) {
return buildForwardTransition(
context,
forwardPerformance,
buildTransition(
context,
performance,
child
)
);
}
Widget buildTransition(BuildContext context, PerformanceView performance, Widget child) => child;
Widget buildForwardTransition(BuildContext context, PerformanceView performance, Widget child) => child;
String get debugLabel => '$runtimeType';
String toString() => '$runtimeType(performance: $_performance)';
}
@ -246,7 +295,7 @@ class _ModalScope extends StatusTransitionComponent {
key: new GlobalObjectKey(route),
child: new IgnorePointer(
ignoring: performance.status == PerformanceStatus.reverse,
child: route.buildTransition(context, performance, contents)
child: route.wrapTransition(context, contents)
)
);
}
@ -354,7 +403,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
performance: performance,
current: isCurrent,
route: this
// calls buildTransition() and buildPage(), defined above
// calls buildTransition()/buildForwardTransition() and buildPage(), defined above
);
}

View file

@ -27,6 +27,11 @@ abstract class TransitionComponent extends StatefulComponent {
Widget build(BuildContext context);
_TransitionState createState() => new _TransitionState();
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('performance: $performance');
}
}
class _TransitionState extends State<TransitionComponent> {

View file

@ -0,0 +1,165 @@
// 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_test/flutter_test.dart';
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
import 'package:test/test.dart';
import 'test_widgets.dart';
class TestTransition extends TransitionComponent {
TestTransition({
Key key,
this.childFirstHalf,
this.childSecondHalf,
PerformanceView performance
}) : super(key: key, performance: performance);
final Widget childFirstHalf;
final Widget childSecondHalf;
Widget build(BuildContext context) {
if (performance.progress >= 0.5)
return childSecondHalf;
return childFirstHalf;
}
}
void main() {
final Duration kTwoTenthsOfTheTransitionDuration = kMaterialPageRouteTransitionDuration * 0.2;
final Duration kFourTenthsOfTheTransitionDuration = kMaterialPageRouteTransitionDuration * 0.4;
test('Check onstage/offstage handling around transitions', () {
testWidgets((WidgetTester tester) {
GlobalKey insideKey = new GlobalKey();
String state() {
String result = '';
if (tester.findText('A') != null)
result += 'A';
if (tester.findText('B') != null)
result += 'B';
if (tester.findText('C') != null)
result += 'C';
if (tester.findText('D') != null)
result += 'D';
if (tester.findText('E') != null)
result += 'E';
if (tester.findText('F') != null)
result += 'F';
if (tester.findText('G') != null)
result += 'G';
return result;
}
tester.pumpWidget(
new MaterialApp(
routes: <String, RouteBuilder>{
'/': (RouteArguments args) {
return new Builder(
key: insideKey,
builder: (BuildContext context) {
PageRoute route = ModalRoute.of(context);
return new Column([
new TestTransition(
childFirstHalf: new Text('A'),
childSecondHalf: new Text('B'),
performance: route.performance
),
new TestTransition(
childFirstHalf: new Text('C'),
childSecondHalf: new Text('D'),
performance: route.forwardPerformance
),
]);
}
);
},
'/2': (RouteArguments args) => new Text('E'),
'/3': (RouteArguments args) => new Text('F'),
'/4': (RouteArguments args) => new Text('G'),
}
)
);
// TODO(ianh): Remove the first part of this test once the first page doesn't animate in
NavigatorState navigator = insideKey.currentContext.ancestorStateOfType(NavigatorState);
expect(state(), equals('AC')); // transition ->1 is at 0.0
tester.pump(kFourTenthsOfTheTransitionDuration);
expect(state(), equals('AC')); // transition ->1 is at 0.4
tester.pump(kFourTenthsOfTheTransitionDuration);
expect(state(), equals('BC')); // transition ->1 is at 0.8
tester.pump(kFourTenthsOfTheTransitionDuration);
expect(state(), equals('BC')); // transition ->1 is at 1.0
navigator.openTransaction((NavigatorTransaction transaction) => transaction.pushNamed('/2'));
expect(state(), equals('BC')); // transition 1->2 is not yet built
tester.pump();
expect(state(), equals('BCE')); // transition 1->2 is at 0.0
tester.pump(kFourTenthsOfTheTransitionDuration);
expect(state(), equals('BCE')); // transition 1->2 is at 0.4
tester.pump(kFourTenthsOfTheTransitionDuration);
expect(state(), equals('BDE')); // transition 1->2 is at 0.8
tester.pump(kFourTenthsOfTheTransitionDuration);
expect(state(), equals('E')); // transition 1->2 is at 1.0
navigator.openTransaction((NavigatorTransaction transaction) => transaction.pop());
expect(state(), equals('E')); // transition 1<-2 is at 1.0, just reversed
tester.pump();
expect(state(), equals('BDE')); // transition 1<-2 is at 1.0
tester.pump(kFourTenthsOfTheTransitionDuration);
expect(state(), equals('BDE')); // transition 1<-2 is at 0.6
navigator.openTransaction((NavigatorTransaction transaction) => transaction.pushNamed('/3'));
expect(state(), equals('BDE')); // transition 1<-2 is at 0.6
tester.pump();
expect(state(), equals('BDEF')); // transition 1<-2 is at 0.6, 1->3 is at 0.0
tester.pump(kFourTenthsOfTheTransitionDuration);
expect(state(), equals('BCEF')); // transition 1<-2 is at 0.2, 1->3 is at 0.4
tester.pump(kFourTenthsOfTheTransitionDuration);
expect(state(), equals('BDF')); // transition 1<-2 is done, 1->3 is at 0.8
navigator.openTransaction((NavigatorTransaction transaction) => transaction.pop());
expect(state(), equals('BDF')); // transition 1<-3 is at 0.8, just reversed
tester.pump();
expect(state(), equals('BDF')); // transition 1<-3 is at 0.8
tester.pump(kTwoTenthsOfTheTransitionDuration); // notice that dT=0.2 here, not 0.4
expect(state(), equals('BDF')); // transition 1<-3 is at 0.6
tester.pump(kFourTenthsOfTheTransitionDuration);
expect(state(), equals('BCF')); // transition 1<-3 is at 0.2
navigator.openTransaction((NavigatorTransaction transaction) => transaction.pushNamed('/4'));
expect(state(), equals('BCF')); // transition 1<-3 is at 0.2, 1->4 is not yet built
tester.pump();
expect(state(), equals('BCFG')); // transition 1<-3 is at 0.2, 1->4 is at 0.0
tester.pump(kFourTenthsOfTheTransitionDuration);
expect(state(), equals('BCG')); // transition 1<-3 is done, 1->4 is at 0.4
tester.pump(kFourTenthsOfTheTransitionDuration);
expect(state(), equals('BDG')); // transition 1->4 is at 0.8
tester.pump(kFourTenthsOfTheTransitionDuration);
expect(state(), equals('G')); // transition 1->4 is done
});
});
}

View file

@ -6,11 +6,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:test/test.dart';
class Builder extends StatelessComponent {
Builder({ this.builder });
final WidgetBuilder builder;
Widget build(BuildContext context) => builder(context);
}
import 'test_widgets.dart';
void main() {
test('SnackBar control test', () {

View file

@ -59,3 +59,10 @@ void flipStatefulComponent(WidgetTester tester) {
FlipComponentState state = stateElement.state;
state.flip();
}
class Builder extends StatelessComponent {
Builder({ Key key, this.builder }) : super(key: key);
final WidgetBuilder builder;
Widget build(BuildContext context) => builder(context);
}