Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/animations/lib/animations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
// found in the LICENSE file.

export 'src/open_container.dart';
export 'src/page_transition_switcher.dart';
317 changes: 317 additions & 0 deletions packages/animations/lib/src/page_transition_switcher.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
// Copyright 2019 The Flutter 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/foundation.dart';
import 'package:flutter/widgets.dart';

// Internal representation of a child that, now or in the past, was set on the
// PageTransitionSwitcher.child field, but is now in the process of
// transitioning. The internal representation includes fields that we don't want
// to expose to the public API (like the controllers).
class _ChildEntry {
_ChildEntry({
@required this.primaryController,
@required this.secondaryController,
@required this.transition,
@required this.widgetChild,
}) : assert(primaryController != null),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
}) : assert(primaryController != null),
}) : assert(primaryController != null),
assert(secondaryController != null),
assert(widgetChild != null),
assert(transition != null);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that would be better! However, unfortunately, dartfmt is enabled for this repository giving me no control over the formatting here :(

assert(secondaryController != null),
assert(widgetChild != null),
assert(transition != null);

final AnimationController primaryController;

final AnimationController secondaryController;

// The currently built transition for this child.
Widget transition;

// The widget's child at the time this entry was created or updated.
// Used to rebuild the transition if necessary.
Widget widgetChild;

void dispose() {
primaryController.dispose();
secondaryController.dispose();
}

@override
String toString() => 'Entry#${shortHash(this)}($widgetChild)';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If someone sees this string (maybe in an error message) - will they be confused by "Entry"?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to give more context.

}

/// Signature for builders used to generate custom transitions for
/// [PageTransitionSwitcher].
///
/// The function should return a widget which wraps the given `child`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be worthwhile to reiterate how the animations run during a transition. This is the same text as I suggested below.

When a PageTransitionSwitcher's child is replaced,
the new child's primaryAnimation runs forward and the value of its secondary animation is fixed at 0.0. At the same time, the old child's secondaryAnimation runs forward, and the value of its primaryAnimation is fixed at 1.0. The widget returned by the [transitionBuilder] can incorporate both animations. It will use the primary animation to define how its child appears, and the secondary animation to define how its child disappears.

typedef PageTransitionSwitcherTransitionBuilder = Widget Function(
Widget child,
Animation<double> primaryAnimation,
Animation<double> secondaryAnimation,
);

/// A widget that transitions from a previously set child to a newly set child

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from the current child to a new child?

/// using an animation specified by [transitionBuilder].
///
/// This is a variation of an [AnimatedSwitcher], but instead of using the
/// same transition for enter and exit, two separate transitions can be
/// specified, similar to how the enter and exit transitions of a [PageRoute]
/// are defined.
///
/// The transitions returned by the [transitionBuilder] are driven by two

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the difficult part of explaining route transitions. I'll take a crack at the whole thing:

When a new [child] is specified, the [transitionBuilder] is effectively applied twice, once to the old child and once to the new one. The old child's secondaryAnimation runs forward, and the value of its primaryAnimation is fixed at 1.0. The new child's primaryAnimation runs forward and the value of its secondary animation is fixed at 0.0. The widget returned by the [transitionBuilder] can incorporate both animations. It will use the primary animation to define how its child appears, and the secondary animation to define how its child disappears. This process is the same as the one used by [PageRoute.buildTransitions].

[And then include simple transitionBuilder example. There's a simple example here: https://api.flutter.dev/flutter/material/MaterialPageRoute/buildTransitions.html; prefer one that uses both animations]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to add the example, working on that now.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

/// animations: a primary one and a secondary one. When a new child is
/// transitioning in while [reverse] is false, the primary animation of the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to add one example for when reverse is false? I was thinking maybe a few more for the other cases, like reverse = true and when there are two widgets of the same type, but those can be a follow-up

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What kind of examples are you looking for?

@shihaohong shihaohong Dec 4, 2019

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking a simple default example of PageTransitionSwitcher being used, but I just realized that I don't really know how we could make that a code sample, since you'd have to change out the existing child widget with a new child widget :)

/// transition associated with that new child is running forward. At the same
/// time, the secondary animation of the previous child is playing forward to
/// transition that child out. In other words, the primary animation defines
/// how a child enters, and the secondary animation determines how it leaves.
/// This is similar to the transition associated with pushing a new [PageRoute]
/// on top of another.
///
/// When [reverse] is true, then the primary animation of the previous child
/// is playing in reverse to reveal the new child underneath, whose secondary
Comment thread
shihaohong marked this conversation as resolved.
Outdated
/// animation is also playing in reverse. This is similar to popping a
/// [PageRoute] to reveal a new [PageRoute] underneath it.
///
/// If the children are swapped fast enough (i.e. before [duration] elapses),
/// more than one previous child can exist and be transitioning out while the
/// newest one is transitioning in.
///
/// If the *new* child is the same widget type and key as the *previous* child,
/// but with different parameters, then [PageTransitionSwitcher] will *not* do a

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case by "but with different parameters" I assume that you mean that the new child was configured differently. Developers may find this a little confusing. You might provide an example. Like: changing the child from SizedBox(width: 10) to SizedBox(width: 100) would not trigger a transition but changing the child from SizedBox(width: 10) to SizedBox(key: Key('foo'), width: 100) would. Similarly, changing the child to Container(width: 10) would trigger a transition.

The AnimatedSwitcher class API doc makes this same point. Might make sense to sync this paragraph with that one.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

/// transition between them, since as far as the framework is concerned, they
/// are the same widget and the existing widget can be updated with the new
/// parameters. To force the transition to occur, set a [Key] on each child
/// widget that you wish to be considered unique (typically a [ValueKey] on the
/// widget data that distinguishes this child from the others).
///
/// The same key can be used for a new child as was used for an already-outgoing
/// child; the two will not be considered related. (For example, if a progress

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// child; the two will not be considered related. (For example, if a progress
/// child; the two will not be considered related. For example, if a progress

/// indicator with key A is first shown, then an image with key B, then another
/// progress indicator with key A again, all in rapid succession, then the old
/// progress indicator and the image will be fading out while a new progress
/// indicator is fading in.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// indicator is fading in.)
/// indicator is fading in.

class PageTransitionSwitcher extends StatefulWidget {
/// Creates a [PageTransitionSwitcher].
///
/// The [duration], [reverse], and [transitionBuilder] parameters
/// must not be null.
const PageTransitionSwitcher({
Key key,
this.duration = const Duration(milliseconds: 300),
this.reverse = false,
@required this.transitionBuilder,
this.child,
}) : assert(duration != null),
assert(reverse != null),
assert(transitionBuilder != null),
super(key: key);

/// The current child widget to display.
///
/// If there was a previous child, it will be transitioning out using the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

transitioning => transitioned

/// secondary animation of the [transitionBuilder], while the new child
/// transitions in using the primary animation of the [transitionBuilder].
///
/// If there was no previous child, then this child will transition in using
/// the primary animation of the [transitionBuilder].
///
/// The child is considered to be "new" if it has a different type or [Key]
/// (see [Widget.canUpdate]).
final Widget child;

/// The duration of the transition from the old [child] value to the new one.
///
/// This duration is applied to the given [child] when that property is set to
/// a new child. Changing [duration] will not affect the
/// durations of transitions already in progress.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will a duration change affect in-progress transitions if transitionBuilder is also changed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No.

final Duration duration;

/// Indicates the direction of the animation when a new [child] is set.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe "Determines whether or not the new [child] will replace the previous [child] from the top or underneath the previous [child]", or something similar to describe the actual transition visually.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe: when the [child] is changed.

Widgets don't have properties that are set so much as constructor parameters whose values are provided.

///
/// When this is false, the new child will transition in on top of the
/// previously set child while its primary animation and the secondary

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

previously set child => previous child

I'll stop complaining about "child setting" etc.

/// animation of the previous child are running forward. This is similar to
/// the transition associated with pushing a new [PageRoute] on top of
/// another.
///
/// When this is true, the new child will transition in below the
Comment thread
shihaohong marked this conversation as resolved.
/// previously set child while its secondary animation and the primary
/// animation of the previous child are running in reverse. This is similar to
/// the transition associated with popping a [PageRoute] to reveal a new
/// [PageRoute] below it.
final bool reverse;

/// A function that wraps a new [child] with a primary and secondary animation

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new PageTransition child could be null, by the transitionBuilder's child is guaranteed to be non-null, right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. added documentation.

/// to transition between the previously set child and the new child.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a little confusing. The returned widget defines how [child] enters and leaves. It doesn't explicitly define a transition between two children.

///
/// This is only called when a new [child] is set (not for each build), or
/// when a new [transitionBuilder] is set. If a new [transitionBuilder] is
/// set, then the transition is rebuilt for the current child and all previous
/// children using the new [transitionBuilder]. The function must not return
/// null.
final PageTransitionSwitcherTransitionBuilder transitionBuilder;

@override
_PageTransitionSwitcherState createState() => _PageTransitionSwitcherState();
}

class _PageTransitionSwitcherState extends State<PageTransitionSwitcher>
with TickerProviderStateMixin {
final List<_ChildEntry> _activeEntries = <_ChildEntry>[];
_ChildEntry _currentEntry;
int _childNumber = 0;

@override
void initState() {
super.initState();
_addEntryForNewChild(animate: false);
}

@override
void didUpdateWidget(PageTransitionSwitcher oldWidget) {
super.didUpdateWidget(oldWidget);

// If the transition builder changed, then update all of the previous
// transitions.
if (widget.transitionBuilder != oldWidget.transitionBuilder) {
_activeEntries.forEach(_updateTransitionForEntry);
}

final bool hasNewChild = widget.child != null;
final bool hasOldChild = _currentEntry != null;
if (hasNewChild != hasOldChild ||
hasNewChild &&
!Widget.canUpdate(widget.child, _currentEntry.widgetChild)) {
// Child has changed, fade current entry out and add new entry.
_childNumber += 1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the transition duration was 1ms, then this value will overflow in about 292 million years. Probably a safe bet :-).

_addEntryForNewChild(animate: true);
} else if (_currentEntry != null) {
assert(hasOldChild && hasNewChild);
assert(Widget.canUpdate(widget.child, _currentEntry.widgetChild));
// Child has been updated. Make sure we update the child widget and
// transition in _currentEntry even though we're not going to start a new
// animation, but keep the key from the previous transition so that we
// update the transition instead of replacing it.
_currentEntry.widgetChild = widget.child;
_updateTransitionForEntry(_currentEntry); // uses entry.widgetChild
}
}

void _addEntryForNewChild({@required bool animate}) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe update the bool variable name to shouldAnimate? I somehow keep thinking of the Animation.animate method, and the verb helps with reading the code as well.

Suggested change
void _addEntryForNewChild({@required bool animate}) {
void _addEntryForNewChild({@required bool shouldAnimate}) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conventionally ({ @required bool animate })

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wish, but dartfmt is tying my hands in this repo.

assert(animate || _currentEntry == null);
if (_currentEntry != null) {
assert(animate);
if (widget.reverse) {
_currentEntry.primaryController.reverse();
} else {
_currentEntry.secondaryController.forward();
}
_currentEntry = null;
}
if (widget.child == null) {
Comment thread
shihaohong marked this conversation as resolved.
return;
}
final AnimationController primaryController = AnimationController(
duration: widget.duration,
vsync: this,
);
final AnimationController secondaryController = AnimationController(
duration: widget.duration,
vsync: this,
);
if (animate) {
if (widget.reverse) {
primaryController.value = 1.0;
secondaryController.value = 1.0;
secondaryController.reverse();
Comment thread
shihaohong marked this conversation as resolved.
} else {
primaryController.forward();
}
} else {
assert(_activeEntries.isEmpty);
primaryController.value = 1.0;
}
_currentEntry = _newEntry(
child: widget.child,
primaryController: primaryController,
secondaryController: secondaryController,
builder: widget.transitionBuilder,
);
if (widget.reverse && _activeEntries.isNotEmpty) {
// Add below previous child.
_activeEntries.insert(_activeEntries.length - 1, _currentEntry);
} else {
// Add on top of previous child.
_activeEntries.add(_currentEntry);
}
}

_ChildEntry _newEntry({
@required Widget child,
@required PageTransitionSwitcherTransitionBuilder builder,
@required AnimationController primaryController,
@required AnimationController secondaryController,
}) {
final _ChildEntry entry = _ChildEntry(
widgetChild: child,
transition: KeyedSubtree.wrap(

@shihaohong shihaohong Dec 4, 2019

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main purpose of KeyedSubtree here is to avoid any complete replacements of the child when PageTransitionSwitcher's transition is updated right? I'm not too familiar with KeyedSubtree, so I just want to make sure I'm getting its intent right.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

builder(child, primaryController, secondaryController),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe assert that the builder's return value is non-null here and provide a nice error message

_childNumber,
),
primaryController: primaryController,
secondaryController: secondaryController,
);
secondaryController.addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed) {
assert(mounted);
assert(_activeEntries.contains(entry));
setState(() {
_activeEntries.remove(entry);
entry.dispose();
});
}
});
primaryController.addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.dismissed) {
assert(mounted);
assert(_activeEntries.contains(entry));
setState(() {
_activeEntries.remove(entry);
entry.dispose();
});
}
});
return entry;
}

void _updateTransitionForEntry(_ChildEntry entry) {
entry.transition = KeyedSubtree(
key: entry.transition.key,
child: widget.transitionBuilder(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe assert that the child is non-null here and provide a nice error message

entry.widgetChild,
entry.primaryController,
entry.secondaryController,
),
);
}

@override
void dispose() {
for (_ChildEntry entry in _activeEntries) {
entry.dispose();
}
super.dispose();
}

@override
Widget build(BuildContext context) {
return Stack(
children: _activeEntries
.map<Widget>((_ChildEntry entry) => entry.transition)
.toList(),
alignment: Alignment.center,
);
}
}
2 changes: 1 addition & 1 deletion packages/animations/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version: 0.0.1
homepage: https://github.com/flutter/packages/tree/master/packages/animations

environment:
sdk: ">=2.1.0 <3.0.0"
sdk: ">=2.2.2 <3.0.0"

dependencies:
flutter:
Expand Down
Loading