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.
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.
Browser Animation Controller is designed to make it simple to define these animations, and to allow views to subscribe for update events.
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);
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: 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.
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 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:
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_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 GetMotionSpecification().
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:
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> GetMotionSpecification(
BrowserAnimationGroup group,
BrowserAnimationMotion motion) const override;
};
// .cc file:
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))
Snapping at the very start or end of an animation is supported, but often unnecessary if the calling code is responsible for rendering when the animation isn't playing.
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 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(...)])
Sequences must be in order and cannot overlap, though the end of one segment can be the beginning of the next.
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)))
BrowserAnimationControllerOnce the 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 value = BrowserAnimationController::From(browser)->GetCurrentValue(
MyAnimations::kPanelGroup, MyAnimations::kPanelWidth);
if (auto.has_value()) {
// The side panel is animating; the value is valid.
} else {
// The side panel isn't animating; use the default width for the panel's
// current state.
}
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 either be "progressed", "ended", or "canceled". You can use
this if you need it; note that if the animation is canceled, calling
GetCurrentValue() will return null. Calling GetCurrentValue() when the
animation ends will always give the final value for the sequence.