chrome/browser/ui/animation/README.md
This is a browser-window-level service which coordinates multi-View and multi-part animations, such as the Vertical Tab Strip expand/collapse, side panel show/hide, etc.
It provides an interface for declaratively specifying animations, including using (but not limited to) syntax similar to CSS.
It also allows for smooth redirection of animations, even mid-animation.
Consider the toolbar-height side panel. When it shows, several things happen:
These motions don't use the same animation curves, and they don't happen at exactly the same time. This can be confusing to program and it often isn't easy to translate the UX spec (which often uses CSS animation concepts) to a Views animation.
Furthermore, if the panel is in the middle of closing and then decides to open again, you have to write special-case code to blend the two animations together.
Browser Animation Controller is designed to make it simple to define these animations, have them behave correctly, and to allow views to subscribe for update events, without having to write any special-case code.
The primary entry point for this system is BrowserAnimationController. This is
unowned user data attached to a
BrowserWindowInterface, so it can be retrieved using:
auto* const controller =
BrowserAnimationController::From(browser_window_interface);
Animations are broken down into:
Groups, Motions, and Sequences are named using
unique identifiers: BrowserAnimationGroup,
BrowserAnimationMotion, and BrowserAnimationSequence, defined in
browser_animation_types.h. Specify these when
playing an animation or reading an animation value.
A BrowserAnimationProvider defines animations using a straightforward,
declarative syntax. For example, this (partial) example uses keyframes:
Group(kToolbarHeightSidePanelAnimationGroup,
Motion(kSidePanelOpenMotion,
// Animate the side panel for 350ms using an EASE_IN_OUT tween.
Sequence(kSidePanelWidth,
Keyframe(AtMs(0), Value(0)),
Keyframe(AtMs(350), Value(1.0), gfx::Tween::EASE_IN_OUT)),
// Animate the padding around the main contents area from 100 to
// 450ms using an EASE_IN tween.
Sequence(kMainAreaPaddingSize,
Keyframe(AtMs(100), Value(0)),
Keyframe(AtMs(450), Value(1.0), gfx::Tween::EASE_IN)),
// Animate in the shadow opacity from 350ms to the end of the
// animation using a LINEAR tween (i.e. no animation curve).
Sequence(kMainAreaShadowOpacity,
Keyframe(AtMs(350), Value(0)),
Keyframe(AtMs(650), Value(1.0), gfx::Tween::LINEAR))),
Motion(kSidePanelCloseMotion,
// ...
All of the different ways to define an animation will be described later.
In order to create an animation, you will need to subclass either
BrowserAnimationProvider or CachingBrowserAnimationProvider and override a
single method.
CachingBrowserAnimationProvider (recommended): defines all of its
animations once, in a single function.BrowserAnimationProvider: computes animations on demand, whenever an
animation is started.The caching version is simpler, but some animations may need to change in
response to user or system preferences, or the state of the browser, so plain
BrowserAnimationProvider is provided for this case.
You register the animations using
BrowserAnimationController::AddAnimationProvider(). You should register the
provider once, before you need it:
BrowserAnimationController::From(browser)->AddAnimationProvider(
std::make_unique<MyAnimations>());
Animation providers should be placed in chrome/browser/ui/views/animations. There are several examples already there you can use as guides.
Animations should be registered in
BrowserWindowFeatures::InitPostBrowserViewConstruction().
You'll need to define your group(s), motions, and segments - either in your
provider class using DECLARE/DEFINE_CLASS_BROWSER_ANIMATION_*() macros, or in
a separate file using DECLARE/DEFINE_BROWSER_ANIMATION_*() macros. Putting
them in the class allows you to use much shorter identifier names and is
preferred.
CachingBrowserAnimationProviderThis is the easier (and recommended) of the two options.
Derive from CachingBrowserAnimationProvider and override
GenerateAnimations():
// In chrome/browser/ui/views/animations/my_animation_provider.h:
class MyAnimations : public CachingBrowserAnimationProvider {
public:
// Required to support retrieval.
DECLARE_FRAMEWORK_SPECIFIC_IMPLEMENTATION()
DECLARE_CLASS_BROWSER_ANIMATION_GROUP(kPanelGroup);
DECLARE_CLASS_BROWSER_ANIMATION_GROUP(kFlyoverGroup);
DECLARE_CLASS_BROWSER_ANIMATION_MOTION(kExpandMotion);
DECLARE_CLASS_BROWSER_ANIMATION_MOTION(kCollapseMotion);
DECLARE_CLASS_BROWSER_ANIMATION_MOTION(kFadeInMotion);
DECLARE_CLASS_BROWSER_ANIMATION_MOTION(kFadeOutMotion);
DECLARE_CLASS_BROWSER_ANIMATION_SEQUENCE(kPanelWidth);
DECLARE_CLASS_BROWSER_ANIMATION_SEQUENCE(kElementHeight);
DECLARE_CLASS_BROWSER_ANIMATION_SEQUENCE(kOpacity);
GroupInfos GenerateAnimations() override;
};
// In chrome/browser/ui/views/animations/my_animation_provider.cc:
DEFINE_FRAMEWORK_SPECIFIC_IMPLEMENTATION(MyAnimations)
DEFINE_CLASS_BROWSER_ANIMATION_GROUP(MyAnimations, kPanelGroup);
DEFINE_CLASS_BROWSER_ANIMATION_GROUP(MyAnimations, kFlyoverGroup);
DEFINE_CLASS_BROWSER_ANIMATION_MOTION(MyAnimations, kExpandMotion);
DEFINE_CLASS_BROWSER_ANIMATION_MOTION(MyAnimations, kCollapseMotion);
DEFINE_CLASS_BROWSER_ANIMATION_MOTION(MyAnimations, kFadeInMotion);
DEFINE_CLASS_BROWSER_ANIMATION_MOTION(MyAnimations, kFadeOutMotion);
DEFINE_CLASS_BROWSER_ANIMATION_SEQUENCE(MyAnimations, kPanelWidth);
DEFINE_CLASS_BROWSER_ANIMATION_SEQUENCE(MyAnimations, kElementHeight);
DEFINE_CLASS_BROWSER_ANIMATION_SEQUENCE(MyAnimations, kOpacity);
MyAnimations::GroupInfos
MyAnimations::GenerateAnimations() {
return Groups(
// Panel slides in and out while also growing vertically:
Group(kPanelGroup,
Motion(kExpandMotion, /* Insert sequences here. */),
Motion(kCollapseMotion, /* Insert sequences here. */)),
// Flyover expands/contracts and fades in/out:
Group(kFlyoverGroup,
Motion(kExpandMotion, /* Insert sequences here. */),
Motion(kFadeOutMotion, /* Insert sequences here. */)));
}
See below for the syntax for sequences. Note that for
CachingBrowserAnimationProvider, Motion() requires specifying the
BrowserAnimationMotion as its first parameter.
BrowserAnimationProviderThis is the more complex of the two options, but provides the ability to dynamically generate parameters as the state of the browser changes.
Derive from BrowserAnimationProvider and override
GetMotionSpecificationImpl(). You must manually determine which animation
sequences to build based on the group and motion parameters. Return
std::nullopt if you do not provide that motion in that group.
Example:
// .h file:
class MyAnimations : public BrowserAnimationProvider {
public:
// Required to support retrieval.
DECLARE_FRAMEWORK_SPECIFIC_IMPLEMENTATION()
DECLARE_CLASS_BROWSER_ANIMATION_GROUP(kPanelGroup);
DECLARE_CLASS_BROWSER_ANIMATION_GROUP(kFlyoverGroup);
DECLARE_CLASS_BROWSER_ANIMATION_MOTION(kExpandMotion);
DECLARE_CLASS_BROWSER_ANIMATION_MOTION(kCollapseMotion);
DECLARE_CLASS_BROWSER_ANIMATION_MOTION(kFadeInMotion);
DECLARE_CLASS_BROWSER_ANIMATION_MOTION(kFadeOutMotion);
DECLARE_CLASS_BROWSER_ANIMATION_SEQUENCE(kPanelWidth);
DECLARE_CLASS_BROWSER_ANIMATION_SEQUENCE(kElementHeight);
DECLARE_CLASS_BROWSER_ANIMATION_SEQUENCE(kOpacity);
std::optional<internal::MotionSpecification> GetMotionSpecificationImpl(
BrowserAnimationGroup group,
BrowserAnimationMotion motion) const override;
};
// .cc file:
DEFINE_FRAMEWORK_SPECIFIC_IMPLEMENTATION(MyAnimations)
DEFINE_CLASS_BROWSER_ANIMATION_GROUP(MyAnimations, kPanelGroup);
DEFINE_CLASS_BROWSER_ANIMATION_GROUP(MyAnimations, kFlyoverGroup);
DEFINE_CLASS_BROWSER_ANIMATION_MOTION(MyAnimations, kExpandMotion);
DEFINE_CLASS_BROWSER_ANIMATION_MOTION(MyAnimations, kCollapseMotion);
DEFINE_CLASS_BROWSER_ANIMATION_MOTION(MyAnimations, kFadeInMotion);
DEFINE_CLASS_BROWSER_ANIMATION_MOTION(MyAnimations, kFadeOutMotion);
DEFINE_CLASS_BROWSER_ANIMATION_SEQUENCE(MyAnimations, kPanelWidth);
DEFINE_CLASS_BROWSER_ANIMATION_SEQUENCE(MyAnimations, kElementHeight);
DEFINE_CLASS_BROWSER_ANIMATION_SEQUENCE(MyAnimations, kOpacity);
std::optional<internal::MotionSpecification>
MyAnimations::GetMotionSpecification(
BrowserAnimationGroup group,
BrowserAnimationMotion motion) {
// Have to explicitly check for groups and motions I support.
if (group == kPanelGroup) {
if (motion == kExpandMotion) {
return Motion(/* Insert sequences here. */);
} else if (motion == kCollapseMotion) {
return Motion(/* Insert sequences here. */);
}
} else if (group == kFlyoverGroup) {
if (motion == kFadeInMotion) {
return Motion(/* Insert sequences here. */);
} else if (motion == kFadeOutMotion) {
return Motion(/* Insert sequences here. */);
}
}
// I don't handle any other animations or motions, so return null.
return std::nullopt;
}
See below for the syntax for sequences. Note that for
BrowserAnimationProvider, Motion() does not require specifying the
BrowserAnimationMotion as its first parameter.
Providers are checked from most-recently-added to least-recently-added. The first provider that can provide an animation motion does so. This allows motions to be overridden for e.g. testing by simply adding another provider after all the others.
Normally, only one provider would provide a given motion.
There are four different types of ways to specify sequences:
When specifying timestamps for snaps, keyframes, and segments, you can use two different approaches:
You cannot mix and match milliseconds and percentages within the same sequence (exception: zero ms and zero percent are equivalent).
Snap sequences provide a stepwise animation function.
Syntax for snap sequences is simple:
Snap(sequence_name,
FromValue(starting_value), ToValue(ending_value),
AtPercent(progress_percent));
Snap(sequence_name,
FromValue(starting_value), ToValue(ending_value),
AtMs(absolute_time));
For example:
// Snap the top padding from 0% to 100% halfway through the motion.
Snap(kTopPadding, FromValue(0.0), ToValue(1.0), AtPercent(0.5))
You may snap at the very start or end of a motion.
A full-motion sequence uses the Animate() sequence declaration:
Animate(sequence_name,
FromValue(starting_value), ToValue(ending_value)
[, tween])
For example:
// Slide the panel out over the entire animation, using the global tween.
Animate(kPanelWidth, FromValue(0.0), ToValue(1.0))
// Slide the top of the panel down over the entire animation, applying an
// ease in/out.
Animate(kPanelTop, FromValue(0.0), ToValue(1.0), gfx::Tween::EASE_IN_OUT)
These will always play over the entire length of the animation. When using
Animate(), you must specify the motion's total duration (see below).
A keyframe sequence is a series of Keyframe() declarations; similar to CSS:
Sequence(sequence_name,
Keyframe(AtMs(first_keyframe_time),
Value(first_keyframe_value)),
Keyframe(AtMs(second_keyframe_time),
Value(second_keyframe_value)
[, tween]),
[, Keyframe(...)]);
Sequence(sequence_name,
Keyframe(AtPercent(first_keyframe_percent),
Value(first_keyframe_value)),
Keyframe(AtPercent(second_keyframe_percent),
Value(second_keyframe_value)
[, tween]),
[, Keyframe(...)]);
The animation value will start at the value of the first keyframe, and end at the value of the last keyframe. You may specify keyframes at the start (time zero) and end of the animation.
Keyframes must be specified in order of time. If two subsequent keyframes are at the exact same time, the value of the sequence will jump from the first value to the second.
If two subsequent keyframes have the same value, no animation takes place between them.
If keyframes are specified in terms of percentage, you must specify the motion's total duration (see below). Keyframes in the same sequence cannot mix and match percentage and absolute time.
Sequence(kMyAnimationElement,
Keyframe(AtMs(100), Value(0)),
Keyframe(AtMs(350), Value(0.7), gfx::Tween::EASE_IN_OUT),
Keyframe(AtMs(500), Value(0.7)),
Keyframe(AtMs(750), Value(1.0), gfx::Tween::LINEAR))
In this example:
A segment sequence has a defined starting value, then only animates over specific segments, which are subsets of the motion.
Syntax for segment sequences is:
Sequence(sequence_name,
StartingAt(initial_value),
Segment(StartPercent(first_segment_start),
EndPercent(first_segment_end),
AnimateTo(first_segment_end_value)
[, tween]),
Segment(StartPercent(second_segment_start),
EndPercent(second_segment_end),
AnimateTo(second_segment_end_value)
[, tween])
[, Segment(...)])
Segments must be in order and cannot overlap, though the end of one segment can be the beginning of the next.
You may specify a length instead of an end.
If segments are specified using percentages, you must specify the motion's total duration (see below). Segments in the same sequence cannot mix and match percent and absolute time.
The exact same animation as in the keyframe example above can be expressed using segments:
Element(kMyAnimationElement,
StartingValue(0.0),
Segment(StartMs(100), EndMs(350),
AnimateTo(0.7), gfx::Tween::EASE_IN_OUT),
Segment(StartMs(500), LengthMs(250),
AnimateTo(1.0), gfx::Tween::LINEAR))
Keyframes and segments are functionally equivalent so use whichever one makes more sense for your situation.
You may optionally specify two additional values when defining a motion:
a motion length, using TotalDurationMs(motion_length) and a global tween.
These go immediately before the sequences.
If you do not specify a motion length and tween, then:
If you do specify a motion length and tween, then:
LINEAR to avoid layering tweens in this way.Percent and absolute time cannot be mixed in the same keyframe or segment sequence (with the caveat that zero ms and zero percent are equivalent), but you can mix and match sequences that use percentage and absolute time.
Motion(// Note: motion id is only required for caching provider; omit this if
// extending `BrowserAnimationProvider`:
motion_id,
// The motion lasts for exactly 1s.
TotalDurationMs(1000),
// All segments with percentages will follow an ease tween.
gfx::Tween::EASE_IN_OUT,
// This sequence is in percent and will use the global tween, animating
// between 0.0 and 1.0 when the output of the global tween is between
// 0.1 and 0.9.
Sequence(kPanelWidth,
StartingAt(0.0),
Segment(StartPercent(0.1), EndPercent(0.9), AnimateTo(1.0))),
// This sequence is in absolute time and will ignore the global tween.
Sequence(kFloatingElement,
Keyframe(AtMs(0), Value(0.0)),
Keyframe(AtMs(250), Value(1.0), gfx::Tween::EASE_OUT)),
// This sequence will play over the entire 1000 ms and will use the
// global tween.
Animate(kSomeOtherThing, FromValue(1.0), ToValue(0.0)))
By default, once a motion has completed, GetCurrentValue() will return null.
This means you will need to compute all the visual properties of your UI
elements whenever an animation isn't playing.
If you want sequence values to persist between motions and even be factored into
future motions, you can specify sequence properties. You do not need to specify
properties for all sequences in your BrowserAnimationProvider; merely those
that need to not have the default behavior described above.
For example, you might want to have the system remember your side panel width between animations, so that (a) you don't have to query the panel state when it's not animating and (b) if you start a close in the middle of an open or vice-versa, the animation starts from its current position (rather than fully open or closed).
You'll want to create a SequenceParams in your animation provider for each
such sequence. These have three properties:
persist_between_animations - if set to true, the value is not discarded
at the end of a motion. Default is false.default_value - specifies a value that will be returned if there is no
known value, or the current value is explicitly default (see below). Default
is none.auto_return_to_default - whether a motion which does not specify an
animation for this sequence should automatically return it to its default
value. Default is false.For BrowserAnimationProvider, override GetAllSequenceParams() and optionally
GetSequenceParams(). These require directly using SequenceParams.
For CachingBrowserAnimationProvider (recommended), you should instead use
SetSequenceParams(), typically in the constructor. This lets you use the much
simpler Persist() and Default() to define params.
MyAnimations::MyAnimations() {
SetSequenceParams(
// Parameters for the panel group.
kPanelGroup,
// Panel width value persists between motions.
Persist(kPanelWidth),
// Element height persists and returns to a default value of 1 if not
// specified in a motion.
Default(kElementHeight, 1.0, /*auto_return=*/true));
}
In addition to a literal value (e.g. "1.0", "0.0", "0.5") you can specify that the default value should be used for a keyframe or animation endpoint:
Motion(kExpandMotion,
Animate(kOpacity, FromValue(0.0), ToValue(DefaultValue())))
This will animate to whatever the default opacity is (you must have already defined it). If you want to create a more complex animation curve but don't want to exceed the default value, you can do something like:
Motion(kExpandMotion,
Sequence(
kOpacity,
// Start at zero.
StartingValue(0.0),
// Pause at 0.7 or default value, whichever is less.
Segment(StartMs(0), EndMs(200), ToValue(MinOfDefaultAnd(0.7))),
// Animate to the default value.
Segment(StartMs(800), EndMs(1000), ToValue(DefaultValue()))))
Sequence properties including the default value may change, e.g. via
CachingBrowserAnimationProvider::UpdateDefaultValue(). You might use this if,
for example, the user changes a setting that affects the UI. Note that you can
retrieve an animation provider from the animation controller by type:
void OnUsePanelTransparencyChanged(bool use_transparency) {
auto* const controller = BrowserAnimationController::From(browser);
MyAnimations* const animations = controller->GetAnimationProvider<MyAnimations>();
animations->UpdateDefaultValue(
MyAnimations::kPanelGroup,
MyAnimations::kOpacity,
use_transparency ? 0.8 : 1.0);
}
This won't only affect the target opacity for future animations - if the current
value of kOpacity is the default, any calls to
BrowserAnimationController::GetCurrentValue(kPanelGroup, kOpacity) will return
the updated value.
You can specify that a sequence should explicitly return from the current value
to some specific value using the Return() sequence specification. The value
must use Persist() or Default() since otherwise there's no value to return
from:
Motion(kCollapseMotion,
Return(kPanelWidth, ToValue(0.0)),
...)
Basically, if you don't care where the animation starts from, use Return().
Auto-return can happen when you specify something like
Default(kElementHeight, 1.0, /*auto_return=*/true) and you don't specify
what to do with kElementHeight in a particular motion. When that motion is
played, the following specification will be implicitly added:
Return(kElementHeight, ToValue(DefaultValue()))
When there is an existing value that has persisted, a transition plays. The default transition is "start at", but there are three options, which can be specified in most sequences:
Consider a segment which tweens linearly from 0 to 1, but the current value is 0.5. Here's the result with each transition type:
| Transition | before | 0% | 25% | 50% | 75% | 100% |
|---|---|---|---|---|---|---|
| start at | 0.5 | 0.5 | 0.68 | 0.75 | 0.89 | 1 |
| cap at | 0.5 | 0.5 | 0.5 | 0.5 | 0.75 | 1 |
| ignore | 0.5 | 0 | 0.25 | 0.5 | 0.75 | 1 |
If start at or cap at is specified, but the starting value is outside the range of the animation, a crossfade is played between the current value and the animation over the course of the motion:
| Transition | before | 0% | 25% | 50% | 75% | 100% |
|---|---|---|---|---|---|---|
| start at | -1 | -1 | -0.5 | 0 | 0.5 | 1 |
| cap at | -1 | -1 | -0.5 | 0 | 0.5 | 1 |
| ignore | -1 | 0 | 0.25 | 0.5 | 0.75 | 1 |
You may explicitly specify a transition as the second parameter of a Segment()
declaration. You never have to specify kStartAtOldValue as it is the default.
Segment(kOpacity, Transition::kCapAtOldValue, Keyframe(...), ...)
Segment(kOpacity, Transition::kIgnoreOldValue,
StartingValue(0.0), Segment(...), ...)
BrowserAnimationControllerOnce your animation provider is registered, you can start an animation motion
via Start(). Example:
BrowserAnimationController::From(browser)->Start(
MyAnimations::kPanelGroup, MyAnimations::kExpandMotion);
You can then get the value of an animation using GetCurrentValue(). Sometimes,
it's sufficient to just call this during Layout() or OnPaint().
const auto anim_value =
BrowserAnimationController::From(browser)->GetCurrentValue(
MyAnimations::kPanelGroup, MyAnimations::kPanelWidth);
// Note: if we always call `Reset()` during system initialization, then
// `kPanelWidth` - which is persisted - would always have a valid value, and
// we wouldn't need `value_or()` here.
const int side_panel_width =
base::ClampRound(preferred_side_panel_width * anim_value.value_or(1.0));
Other times, you need to know when the animation has changed so you can e.g.
invalidate a layout, or update a size or opacity. In this case, use
Subscribe():
MyClass() {
// This will only trigger the callback for cases where the "panel group" is
// animating; other animations won't trigger callbacks.
animation_subscription_ =
BrowserAnimationController::From(browser)->Subscribe(
MyAnimations::kPanelGroup,
base::BindRepeating(&MyClass::OnAnimationUpdated,
base::Unretained(this)));
}
void OnAnimationUpdated(const BrowserAnimationController* controller,
BrowserAnimationUpdate status) {
// View grows or shrinks based on the current animation value.
PreferredSizeChanged();
// If I need to directly update a view property in this method, I can call
// `controller->GetCurrentValue()` to see where the animation is.
}
The status will be "started", "progressed", "ended", or "canceled". Calling
GetCurrentValue() when the animation ends will always yield the final value
for the sequence.
The Clear() method clears all values and stops all motions, subsequently,
GetCurrentValue() will return null (at least until the next call to
Start()), or a default value if one has been set.
The Reset() method immediately stops all motions and snaps the state of all
sequences to the end of a specified motion. Only sequences marked as persist
will be saved; all others will be null/default as with Clear().
Reset() is useful when you want to snap directly to a state. If you do not
specify a motion, then the values will snap to the end of the current motion
(if there is no current motion this does nothing).
// Immediately collapse the panel.
auto* const controller = BrowserAnimationController::From(bwi);
controller->Reset(kPanelGroup, kCollapseMotion);
Clear() generates a cancel event if a motion is playing. Reset() may
generate a cancel event if a different motion is playing, and will generate
an ended event if the target motion is valid. During the ended event, all
sequence values will be the final values of the motion.
Persist all values that make sense to. This will make animations smoother. All things equal, also prefer to enable auto-return to make your motions simpler.
If your UI can be configured to start in one of several initial states (such as
vertical tab strip expanded or collapsed mode), call Reset() to set up the
current state on UI initialization. That way, each sequence will always have a
valid value that reflects the current state.
When choosing which state to pick "default" values from, think of the state that the most things can transition to/from. For example, all animations in vertical tab strip start or end at the collapsed state.