blob: 02c89c5ab867c64f9207e338c56d7de3bf16fd7e [file]
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cc/input/browser_controls_offset_manager.h"
#include <stdint.h>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <numbers>
#include <utility>
#include <vector>
#include "base/check_op.h"
#include "base/feature_list.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "base/types/optional_ref.h"
#include "build/build_config.h"
#include "cc/base/features.h"
#include "cc/input/browser_controls_offset_manager_client.h"
#include "cc/input/browser_controls_offset_tag_modifications.h"
#include "cc/input/browser_controls_state.h"
#include "cc/trees/layer_tree_impl.h"
#include "components/viz/common/frame_sinks/begin_frame_args.h"
#include "components/viz/common/quads/offset_tag.h"
#include "ui/gfx/animation/tween.h"
#include "ui/gfx/geometry/transform.h"
#include "ui/gfx/geometry/vector2d_f.h"
namespace cc {
namespace {
// These constants were chosen empirically for their visually pleasant behavior.
// Contact tedchoc@chromium.org for questions about changing these values.
const int64_t kShowHideMaxDurationMs = 200;
// TODO(sinansahin): Temporary value, pending UX guidance probably.
const int64_t kHeightChangeDurationMs = 200;
// These constants were chosen based on local testing to preserve responsiveness
// when using snap animation.
const int64_t kShowHideMinDurationMs = 75;
constexpr float kAlwaysShownRegionMultiplier = 0.5f;
constexpr float kCanHideRegionMinMultiplier = 1.5f;
constexpr float kCanHideRegionMaxMultiplier = 3.0f;
static_assert(
kCanHideRegionMinMultiplier >= kAlwaysShownRegionMultiplier + 1.0f,
"Can hide region must start after always shown region and account for "
"counter-scrolling due to the controls hiding.");
static_assert(kAlwaysShownRegionMultiplier > 0.0f,
"Always shown region must be non-zero to minimize the chance of "
"shifting the web contents when controls are shown.");
constexpr float kSnapAnimationThresholdMinMultiplier = 0.2f;
constexpr float kSnapAnimationReferenceThresholdDp = 30.0f;
float NormalizeShownRatio(float value, float min_shown_ratio) {
if (min_shown_ratio == 1.0f) {
return 1.0f;
}
return (std::clamp(value, min_shown_ratio, 1.0f) - min_shown_ratio) /
(1.0f - min_shown_ratio);
}
} // namespace
// static
std::unique_ptr<BrowserControlsOffsetManager>
BrowserControlsOffsetManager::Create(BrowserControlsOffsetManagerClient* client,
float controls_show_threshold,
float controls_hide_threshold,
bool in_viz_process) {
return base::WrapUnique(new BrowserControlsOffsetManager(
client, controls_show_threshold, controls_hide_threshold,
in_viz_process));
}
BrowserControlsOffsetManager::BrowserControlsOffsetManager(
BrowserControlsOffsetManagerClient* client,
float controls_show_threshold,
float controls_hide_threshold,
bool in_viz_process)
: client_(client),
permitted_state_(BrowserControlsState::kBoth),
accumulated_scroll_delta_(0.0f),
unapplied_scroll_delta_(0.0f),
baseline_top_content_offset_(0.0f),
baseline_bottom_content_offset_(0.0f),
controls_show_threshold_(controls_hide_threshold),
controls_hide_threshold_(controls_show_threshold),
pinch_gesture_active_(false),
constraint_changed_since_commit_(false),
top_min_height_change_in_progress_(false),
bottom_min_height_change_in_progress_(false),
top_controls_min_height_offset_(0.0f),
bottom_controls_min_height_offset_(0.0f),
use_snap_animation_(base::FeatureList::IsEnabled(
features::kBrowserControlsScrollSnapAnimation)),
in_viz_process_(in_viz_process),
scroll_velocity_tracker_(base::Milliseconds(kShowHideMaxDurationMs)) {
CHECK(client_);
UpdateOldBrowserControlsParams();
}
BrowserControlsOffsetManager::~BrowserControlsOffsetManager() = default;
float BrowserControlsOffsetManager::ControlsTopOffset() const {
return ContentTopOffset() - TopControlsHeight();
}
float BrowserControlsOffsetManager::ContentTopOffset() const {
return TopControlsHeight() > 0
? TopControlsShownRatio() * TopControlsHeight() : 0.0f;
}
float BrowserControlsOffsetManager::TopControlsShownRatio() const {
return client_->CurrentTopControlsShownRatio();
}
float BrowserControlsOffsetManager::TopControlsHeight() const {
return client_->TopControlsHeight();
}
float BrowserControlsOffsetManager::TopControlsMinHeight() const {
return client_->TopControlsMinHeight();
}
float BrowserControlsOffsetManager::TopControlsMinShownRatio() const {
return TopControlsHeight() ? TopControlsMinHeight() / TopControlsHeight()
: 0.0f;
}
float BrowserControlsOffsetManager::BottomControlsHeight() const {
return client_->BottomControlsHeight();
}
float BrowserControlsOffsetManager::BottomControlsMinHeight() const {
return client_->BottomControlsMinHeight();
}
float BrowserControlsOffsetManager::BottomControlsMinShownRatio() const {
return BottomControlsHeight()
? BottomControlsMinHeight() / BottomControlsHeight()
: 0.0f;
}
float BrowserControlsOffsetManager::ContentBottomOffset() const {
return BottomControlsHeight() > 0
? BottomControlsShownRatio() * BottomControlsHeight() : 0.0f;
}
float BrowserControlsOffsetManager::BottomControlsShownRatio() const {
return client_->CurrentBottomControlsShownRatio();
}
float BrowserControlsOffsetManager::TopControlsMinHeightOffset() const {
return top_controls_min_height_offset_;
}
float BrowserControlsOffsetManager::BottomControlsMinHeightOffset() const {
return bottom_controls_min_height_offset_;
}
std::vector<viz::OffsetTagValue>
BrowserControlsOffsetManager::GetOffsetTagValues() const {
std::vector<viz::OffsetTagValue> offset_tag_values;
if (TopControlsHeight() > 0) {
const viz::OffsetTag& top_controls_offset_tag =
offset_tag_modifications_.tags.top_controls_offset_tag;
const viz::OffsetTag& content_offset_tag =
offset_tag_modifications_.tags.content_offset_tag;
float visible_height = ContentTopOffset();
if (top_controls_offset_tag) {
CHECK(!content_offset_tag.IsEmpty());
float offset = TopControlsHeight() - visible_height;
if (visible_height == 0) {
// The toolbar hairline is still shown after the top controls are
// completely scrolled off screen. Shift the top controls a bit more
// so that the hairline disappears.
offset += offset_tag_modifications_.top_controls_additional_height;
}
// ViewAndroid::OnTopControlsChanged() also rounds the offset before
// handing it off to Android.
gfx::Vector2dF offset2d(0.0f, -std::round(offset));
offset_tag_values.emplace_back(top_controls_offset_tag, offset2d);
}
if (content_offset_tag) {
float offset = TopControlsHeight() - visible_height;
// ViewAndroid::OnTopControlsChanged() also rounds the offset before
// handing it off to Android.
gfx::Vector2dF offset2d(0.0f, -std::round(offset));
offset_tag_values.emplace_back(content_offset_tag, offset2d);
}
}
if (BottomControlsHeight() > 0) {
const viz::OffsetTag& bottom_controls_offset_tag =
offset_tag_modifications_.tags.bottom_controls_offset_tag;
if (bottom_controls_offset_tag) {
float bottom_controls_visible_height = ContentBottomOffset();
float offset = BottomControlsHeight() - bottom_controls_visible_height;
if (bottom_controls_visible_height == 0) {
// Similar to the top toolbar hairline, there are visual effects
// on the top most bottom controls that are still shown after being
// completely scrolled off screen. Shift the bottom controls a bit
// more so that these visual effects disappear.
offset += offset_tag_modifications_.bottom_controls_additional_height;
}
// ViewAndroid::OnTopControlsChanged() also rounds the offset before
// handing it off to Android.
gfx::Vector2dF offset2d(0.0f, std::round(offset));
offset_tag_values.emplace_back(bottom_controls_offset_tag, offset2d);
}
}
return offset_tag_values;
}
BrowserControlsMetadata BrowserControlsOffsetManager::GetMetadata() const {
BrowserControlsMetadata metadata;
metadata.top_controls_height = TopControlsHeight();
metadata.top_controls_shown_ratio =
std::clamp(TopControlsShownRatio(), 0.f, 1.f);
#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS)
metadata.bottom_controls_height = BottomControlsHeight();
metadata.bottom_controls_shown_ratio =
std::clamp(BottomControlsShownRatio(), 0.f, 1.f);
metadata.top_controls_min_height_offset = TopControlsMinHeightOffset();
metadata.bottom_controls_min_height_offset = BottomControlsMinHeightOffset();
metadata.has_offset_tag = !offset_tag_modifications_.tags.IsEmpty();
#endif // BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS)
return metadata;
}
std::pair<float, float>
BrowserControlsOffsetManager::TopControlsShownRatioRange() {
if (top_controls_animation_.IsInitialized())
return std::make_pair(top_controls_animation_.min_value(),
top_controls_animation_.max_value());
return std::make_pair(0.0f, 1.0f);
}
std::pair<float, float>
BrowserControlsOffsetManager::BottomControlsShownRatioRange() {
if (bottom_controls_animation_.IsInitialized())
return std::make_pair(bottom_controls_animation_.min_value(),
bottom_controls_animation_.max_value());
return std::make_pair(0.0f, 1.0f);
}
float BrowserControlsOffsetManager::ControlsAnimatedHeight() const {
float animated_height = TopControlsHeight() > 0
? TopControlsAnimatedHeight()
: BottomControlsAnimatedHeight();
DCHECK_GE(animated_height, 0.0f);
return animated_height;
}
float BrowserControlsOffsetManager::TopControlsAnimatedHeight() const {
return TopControlsHeight() - TopControlsMinHeight();
}
float BrowserControlsOffsetManager::BottomControlsAnimatedHeight() const {
return BottomControlsHeight() - BottomControlsMinHeight();
}
float BrowserControlsOffsetManager::SnapAnimationAlwaysShownRegionHeight()
const {
// When controls are at the top, trigger the show animation early to prevent
// the browser controls from being shown when the page has been scrolled all
// the way to the top and the controls are not obscuring any web content. This
// is not a concern when the controls are only on the bottom.
if (TopControlsHeight() == 0) {
return 0.0f;
}
return TopControlsAnimatedHeight() * kAlwaysShownRegionMultiplier;
}
std::pair<float, float>
BrowserControlsOffsetManager::SnapAnimationCanHideRegionHeights(
float slowness) const {
DCHECK_GE(slowness, 0.0f);
DCHECK_LE(slowness, 1.0f);
// Hide the browser controls well-past the always shown region (if it exists).
// The purpose of the can-hide region is to prevent the browser controls from
// animating to show when the page has been scrolled all the way to the top.
// It works by delaying the hide animation until the user has scrolled a
// sufficient distance from the top of the page so that a subsequent show
// animation does not start when the page is at the top, resulting in the web
// content being shifted down as part of the animation. The faster the user
// scrolls, the shorter the duration of the hide animation, and therefore the
// smaller this region needs to be.
const float multiplier = gfx::Tween::FloatValueBetween(
slowness, kCanHideRegionMinMultiplier, kCanHideRegionMaxMultiplier);
return std::make_pair(TopControlsAnimatedHeight() * multiplier,
BottomControlsAnimatedHeight() * multiplier);
}
float BrowserControlsOffsetManager::SnapAnimationThreshold(
float slowness) const {
DCHECK_GE(slowness, 0.0f);
DCHECK_LE(slowness, 1.0f);
// Using a fixed reference threshold makes the experience consistent under
// browser states that can greatly increase the height of the controls (e.g.
// tab groups)
const float reference_threshold =
client_->DeviceScaleFactor() * kSnapAnimationReferenceThresholdDp;
// Scale the threshold based on the animation duration. Start the animation
// early when the animation duration is short, and late when the animation
// duration is long. This gives the appearance of a consistent, responsive
// threshold.
return reference_threshold *
gfx::Tween::FloatValueBetween(
slowness, kSnapAnimationThresholdMinMultiplier, 1.0f);
}
void BrowserControlsOffsetManager::UpdateBrowserControlsState(
BrowserControlsState constraints,
BrowserControlsState current,
bool animate,
base::optional_ref<const BrowserControlsOffsetTagModifications>
offset_tag_modifications) {
DCHECK(!(constraints == BrowserControlsState::kShown &&
current == BrowserControlsState::kHidden));
DCHECK(!(constraints == BrowserControlsState::kHidden &&
current == BrowserControlsState::kShown));
TRACE_EVENT2("cc", "BrowserControlsOffsetManager::UpdateBrowserControlsState",
"constraints", static_cast<int>(constraints), "current",
static_cast<int>(current));
// If the constraints have changed we need to inform Blink about it since
// that'll affect main thread scrolling as well as layout.
if (permitted_state_ != constraints) {
constraint_changed_since_commit_ = true;
client_->SetNeedsCommit();
}
permitted_state_ = constraints;
if (offset_tag_modifications.has_value()) {
offset_tag_modifications_ = offset_tag_modifications.value();
}
// Don't do anything if it doesn't matter which state the controls are in.
if (constraints == BrowserControlsState::kBoth &&
current == BrowserControlsState::kBoth)
return;
// Don't do anything if there is no change in offset.
float final_top_shown_ratio = 1.0f;
float final_bottom_shown_ratio = 1.0f;
AnimationDirection direction = AnimationDirection::kShowingControls;
if (constraints == BrowserControlsState::kHidden ||
current == BrowserControlsState::kHidden) {
final_top_shown_ratio = TopControlsMinShownRatio();
final_bottom_shown_ratio = BottomControlsMinShownRatio();
direction = AnimationDirection::kHidingControls;
}
if (final_top_shown_ratio == TopControlsShownRatio() &&
final_bottom_shown_ratio == BottomControlsShownRatio()) {
TRACE_EVENT_INSTANT("cc", "Ratios Unchanged");
ResetAnimations();
return;
}
// Don't do anything if the currently running animations end in our desired
// state.
float animated_top_shown_ratio = top_controls_animation_.IsInitialized()
? top_controls_animation_.FinalValue()
: TopControlsShownRatio();
float animated_bottom_shown_ratio =
bottom_controls_animation_.IsInitialized()
? bottom_controls_animation_.FinalValue()
: BottomControlsShownRatio();
if (animated_top_shown_ratio == final_top_shown_ratio &&
animated_bottom_shown_ratio == final_bottom_shown_ratio) {
return;
}
ResetAnimations();
// If we're about to animate the controls in, then restart the animation after
// the scroll completes. We don't know if a scroll is in progress, but that's
// okay; the flag will be reset when a scroll starts next in that case.
if (animate && direction == AnimationDirection::kShowingControls) {
show_controls_when_scroll_completes_ = true;
}
if (animate)
SetupAnimation(direction, kShowHideMaxDurationMs);
else
client_->SetCurrentBrowserControlsShownRatio(final_top_shown_ratio,
final_bottom_shown_ratio);
}
BrowserControlsState BrowserControlsOffsetManager::PullConstraintForMainThread(
bool* out_changed_since_commit) {
DCHECK(out_changed_since_commit);
*out_changed_since_commit = constraint_changed_since_commit_;
return permitted_state_;
}
void BrowserControlsOffsetManager::NotifyConstraintSyncedToMainThread() {
constraint_changed_since_commit_ = false;
}
void BrowserControlsOffsetManager::OnBrowserControlsParamsChanged(
bool animate_changes) {
if (old_browser_controls_params_.top_controls_height == TopControlsHeight() &&
old_browser_controls_params_.top_controls_min_height ==
TopControlsMinHeight() &&
old_browser_controls_params_.bottom_controls_height ==
BottomControlsHeight() &&
old_browser_controls_params_.bottom_controls_min_height ==
BottomControlsMinHeight()) {
return;
}
// We continue to update both top and bottom controls even if one has a height
// of 0 so that animations work properly. So here, we should preserve the
// ratios even if the controls height is 0.
float old_top_height = old_browser_controls_params_.top_controls_height;
float new_top_ratio =
TopControlsHeight()
? TopControlsShownRatio() * old_top_height / TopControlsHeight()
: TopControlsShownRatio();
float old_bottom_height = old_browser_controls_params_.bottom_controls_height;
float new_bottom_ratio = BottomControlsHeight()
? BottomControlsShownRatio() *
old_bottom_height / BottomControlsHeight()
: BottomControlsShownRatio();
if (!animate_changes) {
// 1. If browser controls params were updated when permitted_state_ is
// hidden, we are always snapping the controls to the min-height.
// 2. If the controls were already fully shown, we want to keep them fully
// shown even after the heights changed.
// 3. If either heights changed when the controls were at the min-height,
// the shown ratios need to be snapped to the new min-shown-ratio to keep
// the controls at the min height.
// 4. For any other cases, we should update the shown ratio so the visible
// height remains the same.
bool hide_controls = (permitted_state_ == BrowserControlsState::kHidden);
if (hide_controls) {
new_top_ratio = TopControlsMinShownRatio();
} else if (TopControlsShownRatio() == 1.0f) {
new_top_ratio = 1.0f;
} else if (TopControlsShownRatio() == OldTopControlsMinShownRatio()) {
new_top_ratio = TopControlsMinShownRatio();
}
if (hide_controls) {
new_bottom_ratio = BottomControlsMinShownRatio();
} else if (BottomControlsShownRatio() == 1.0f) {
new_bottom_ratio = 1.0f;
} else if (BottomControlsShownRatio() == OldBottomControlsMinShownRatio()) {
new_bottom_ratio = BottomControlsMinShownRatio();
}
if (base::FeatureList::IsEnabled(
features::kBrowserControlsHeightChangeCancelAnimations) &&
IsAnimatingHeightChange()) {
// Reset the animation if one of the animation has marked with
// `jump_to_end_on_reset`. This can be used as a signal that the current
// animation is going through a height change.
ResetAnimations();
}
}
// Browser controls height change animations
// If the browser controls heights (and/or min-heights) changed and need to be
// animated, the setup is done here. All the animations done in this class
// involve changing the shown ratios smoothly.
//
// There are several cases to handle:
// 1- The controls shown ratio was at the minimum ratio
// - If the min-height changed, we will run an animation from
// old-min-height / new-total-height to new-min-height / new-total-height
// - If the min-height didn't change, we should update the shown ratio to
// min-height / new-total-height so that the controls keep the same
// visible height and don't jump. No animation needed in this case.
// 2- The controls shown ratio was at the highest ratio (should be 1 here)
// - If the total height changed, we will run an animation from
// old-total-height / new-total-height to 1.
// - If the total height didn't change, we don't need to do anything.
// 3- The controls shown ratio is between the minimum and the maximum.
// - If an animation is running to the old min-height, start a new
// animation to min-height / total-height.
// - Otherwise don't start an animation. We're either animating the
// controls to their expanded state, in which case we can let that
// animation continue, or we're scrolling and no animation is needed.
// Update the shown ratio so the visible height remains the same (see
// new_{top,bottom}_ratio above).
//
// When this method is called as a result of a height change,
// TopControlsHeight(), TopControlsMinHeight(), BottomControlsHeight(), and
// BottomControlsMinHeight() will already be returning the new values.
// However, the shown ratios aren't updated.
bool top_controls_need_animation = animate_changes;
bool bottom_controls_need_animation = animate_changes;
// To handle the case where the min-height changes while we're animating to
// the previous min-height, base our "are we at the minimum shown ratio"
// check on the post-animation ratio if an animation is running, rather than
// its current value.
float effective_top_shown_ratio = TopControlsShownRatio();
if (top_controls_animation_.IsInitialized()) {
effective_top_shown_ratio = top_controls_animation_.FinalValue();
}
float effective_bottom_shown_ratio = BottomControlsShownRatio();
if (bottom_controls_animation_.IsInitialized()) {
effective_bottom_shown_ratio = bottom_controls_animation_.FinalValue();
}
float top_target_ratio;
// We can't animate if we don't have top controls.
if (!TopControlsHeight()) {
top_controls_need_animation = false;
// If the top controls height changed when they were fully shown.
} else if (TopControlsShownRatio() == 1.0f &&
TopControlsHeight() != old_top_height) {
top_target_ratio = 1.0f; // i.e. new_height / new_height
// If the top controls min-height changed when they were at the minimum
// shown ratio. For example, the min height changed from 0 to a positive
// value while the top controls were completely hidden.
} else if (effective_top_shown_ratio == OldTopControlsMinShownRatio() &&
TopControlsMinHeight() !=
old_browser_controls_params_.top_controls_min_height) {
top_target_ratio = TopControlsMinShownRatio();
} else {
top_controls_need_animation = false;
}
float bottom_target_ratio;
// We can't animate if we don't have bottom controls.
if (!BottomControlsHeight()) {
bottom_controls_need_animation = false;
// If the bottom controls height changed when they were fully shown.
} else if (BottomControlsShownRatio() == 1.0f &&
BottomControlsHeight() != old_bottom_height) {
bottom_target_ratio = 1.0f; // i.e. new_height / new_height
// If the bottom controls min-height changed when they were at the minimum
// shown ratio.
} else if (effective_bottom_shown_ratio == OldBottomControlsMinShownRatio() &&
BottomControlsMinHeight() !=
old_browser_controls_params_.bottom_controls_min_height) {
bottom_target_ratio = BottomControlsMinShownRatio();
} else {
bottom_controls_need_animation = false;
}
if (top_controls_need_animation || bottom_controls_need_animation) {
MaybeRecordHasExistingAnimationHistogram();
}
if (top_controls_need_animation) {
InitAnimationForHeightChange(&top_controls_animation_, new_top_ratio,
top_target_ratio);
if (old_browser_controls_params_.top_controls_min_height !=
TopControlsMinHeight()) {
top_controls_min_height_offset_ =
old_browser_controls_params_.top_controls_min_height;
top_min_height_change_in_progress_ = true;
SetTopMinHeightOffsetAnimationRange(top_controls_min_height_offset_,
TopControlsMinHeight());
}
} else {
top_controls_min_height_offset_ = TopControlsMinHeight();
}
if (bottom_controls_need_animation) {
InitAnimationForHeightChange(&bottom_controls_animation_, new_bottom_ratio,
bottom_target_ratio);
if (old_browser_controls_params_.bottom_controls_min_height !=
BottomControlsMinHeight()) {
bottom_controls_min_height_offset_ =
old_browser_controls_params_.bottom_controls_min_height;
int height_delta = BottomControlsHeight() - old_bottom_height;
int min_height_delta =
BottomControlsMinHeight() -
old_browser_controls_params_.bottom_controls_min_height;
// Currently, browser controls animate purely based on the change in the
// height, not on the change in minHeight. This works fine when that
// change is the same, but causes issues if the minHeight has been changed
// by a different value than the height has. This is mitigated by
// "stepping up" or "down" the starting min height offset such that the
// effective change is the same for both the height and minHeight.
if (min_height_delta > height_delta) {
bottom_controls_min_height_offset_ += min_height_delta - height_delta;
}
bottom_min_height_change_in_progress_ = true;
SetBottomMinHeightOffsetAnimationRange(bottom_controls_min_height_offset_,
BottomControlsMinHeight());
}
} else {
bottom_controls_min_height_offset_ = BottomControlsMinHeight();
}
// We won't run any animations if the controls are in an in-between state.
// Examples: a show/hide animation is running, shown ratio is some value
// between min-shown-ratio and 1 because of a scroll event.
UpdateOldBrowserControlsParams();
client_->SetCurrentBrowserControlsShownRatio(new_top_ratio, new_bottom_ratio);
}
void BrowserControlsOffsetManager::ScrollBegin() {
if (pinch_gesture_active_)
return;
if (use_snap_animation_) {
// If an animation is running when the scroll starts, count the current
// scroll as having animated and let the animation complete its course.
if (HasAnimation()) {
did_hide_this_scroll_ = (top_controls_animation_.IsInitialized() &&
top_controls_animation_.Direction() ==
AnimationDirection::kHidingControls) ||
(bottom_controls_animation_.IsInitialized() &&
bottom_controls_animation_.Direction() ==
AnimationDirection::kHidingControls);
return;
}
} else {
// If an animation to show the controls is in progress, re-order the
// animation to start after the scroll completes. This ensures that the
// user doesn't accidentally hide the controls with a gesture that would not
// normally be enough to hide them.
show_controls_when_scroll_completes_ = IsAnimatingToShowControls();
}
ResetAnimations();
ResetBaseline();
}
float BrowserControlsOffsetManager::MaximumShownRatioDeltaPerFrame(
float min_ratio) const {
if (!base::FeatureList::IsEnabled(features::kBrowserControlsSmoothScroll)) {
return 1.0f;
}
// When smooth scrolling is enabled, limit the rate at which their shown ratio
// can change to prevent browser controls from jumping due to large scroll
// deltas to match the speed of the show-hide animation i.e. from the
// show/hide threshold to either 0 or 1 in `kAnimationDuration`, whichever is
// faster.
float animation_duration_ms =
kShowHideMaxDurationMs * (std::min(1.0f - controls_show_threshold_,
controls_hide_threshold_ - min_ratio));
float frame_duration_ms = client_->CurrentFrameInterval().InMillisecondsF();
animation_duration_ms =
std::clamp(animation_duration_ms, frame_duration_ms,
static_cast<float>(kShowHideMaxDurationMs));
return frame_duration_ms / animation_duration_ms;
}
gfx::Vector2dF BrowserControlsOffsetManager::ScrollBy(
const gfx::Vector2dF& pending_delta,
bool is_inertial) {
// If one or both of the top/bottom controls are showing, the shown ratio
// needs to be computed.
if (!TopControlsHeight() && !BottomControlsHeight())
return pending_delta;
if (pinch_gesture_active_)
return pending_delta;
if ((permitted_state_ == BrowserControlsState::kShown &&
pending_delta.y() > 0) ||
(permitted_state_ == BrowserControlsState::kHidden &&
pending_delta.y() < 0))
return pending_delta;
if (use_snap_animation_) {
ScrollBySnap(pending_delta, is_inertial);
return pending_delta;
}
return ScrollByPrecise(pending_delta);
}
gfx::Vector2dF BrowserControlsOffsetManager::ScrollByPrecise(
const gfx::Vector2dF& pending_delta) {
// Scroll the page up before expanding the browser controls if
// OnlyExpandTopControlsAtPageTop() returns true.
float viewport_offset_y = client_->ViewportScrollOffset().y();
if (client_->OnlyExpandTopControlsAtPageTop() && pending_delta.y() < 0 &&
viewport_offset_y > 0) {
// Reset the baseline so the controls will immediately begin to scroll
// once we're at the top.
ResetBaseline();
// Only scroll the controls by the amount remaining after the page contents
// have been scrolled to the top.
accumulated_scroll_delta_ =
std::min(0.0f, pending_delta.y() + viewport_offset_y);
} else {
accumulated_scroll_delta_ += pending_delta.y();
}
// We want to base our calculations on top or bottom controls. After consuming
// the scroll delta, we will calculate a shown ratio for the controls. The
// top controls have the priority because they need to visually be in sync
// with the web contents.
bool base_on_top_controls = TopControlsHeight();
float controls_height =
base_on_top_controls ? TopControlsHeight() : BottomControlsHeight();
float old_top_offset = ContentTopOffset();
float baseline_content_offset = base_on_top_controls
? baseline_top_content_offset_
: baseline_bottom_content_offset_;
// The top and bottom controls ratios can be calculated independently.
// However, we want the (normalized) ratios to be equal when scrolling.
// Having normalized ratios in this context means the top and bottom controls
// reach the min and max ratios at the same time when scrolling or during
// the usual show/hide animations, but they can have different shown ratios at
// any time.
float shown_ratio = (baseline_content_offset - accumulated_scroll_delta_ +
unapplied_scroll_delta_) /
controls_height;
float min_ratio = base_on_top_controls ? TopControlsMinShownRatio()
: BottomControlsMinShownRatio();
float normalized_shown_ratio = NormalizeShownRatio(shown_ratio, min_ratio);
float max_shown_ratio_delta = MaximumShownRatioDeltaPerFrame(min_ratio);
float current_normalized_shown_ratio =
NormalizeShownRatio(base_on_top_controls ? TopControlsShownRatio()
: BottomControlsShownRatio(),
min_ratio);
float clamped_normalized_shown_ratio =
std::clamp(normalized_shown_ratio,
current_normalized_shown_ratio - max_shown_ratio_delta,
current_normalized_shown_ratio + max_shown_ratio_delta);
if (clamped_normalized_shown_ratio != normalized_shown_ratio) {
// If the scroll delta wasn't fully applied because it was capped, update
// the unapplied_scroll_delta_ to account for the capped amount. This
// will be smeared across future calls to ScrollBy.
float unapplied_scroll_delta =
(normalized_shown_ratio - clamped_normalized_shown_ratio) *
(1.0f - min_ratio) * controls_height;
if (std::signbit(unapplied_scroll_delta) !=
std::signbit(unapplied_scroll_delta_)) {
unapplied_scroll_delta_ = 0.0f;
}
unapplied_scroll_delta_ += unapplied_scroll_delta;
}
normalized_shown_ratio = clamped_normalized_shown_ratio;
// Even though the real shown ratios (shown height / total height) of the top
// and bottom controls can be different, they share the same
// relative/normalized ratio to keep them in sync.
client_->SetCurrentBrowserControlsShownRatio(
TopControlsMinShownRatio() +
normalized_shown_ratio * (1.0f - TopControlsMinShownRatio()),
BottomControlsMinShownRatio() +
normalized_shown_ratio * (1.0f - BottomControlsMinShownRatio()));
// If the controls are fully visible, treat the current position as the
// new baseline even if the gesture didn't end.
if (TopControlsShownRatio() == 1.0f && BottomControlsShownRatio() == 1.0f) {
ResetBaseline();
// Once the controls are fully visible, then any cancelled animation to show
// them isn't relevant; the user definitely sees the controls and can decide
// if they'd like to keep them.
show_controls_when_scroll_completes_ = false;
}
ResetAnimations();
// If the controls are not yet fully visible and the user is scrolling up
// from the top of the page, prevent any overscroll since that can trigger
// page refresh before the controls are fully visible.
if (normalized_shown_ratio < 1.0f && pending_delta.y() < 0.0f &&
viewport_offset_y == 0.0f) {
return gfx::Vector2dF();
}
// applied_delta will negate any scroll on the content if the top browser
// controls are showing in favor of hiding the controls and resizing the
// content. If the top controls have no height, the content should scroll
// immediately.
gfx::Vector2dF applied_delta(0.0f, old_top_offset - ContentTopOffset());
return pending_delta - applied_delta;
}
void BrowserControlsOffsetManager::ScrollBySnap(
const gfx::Vector2dF& pending_delta,
bool is_inertial) {
accumulated_scroll_delta_ += pending_delta.y();
float velocity_y = scroll_velocity_tracker_.CurrentVelocity().y();
float current_y = pending_delta.y();
// Clear all scroll deltas if current scroll delta is not in the same
// direction as the current velocity.
if (!(velocity_y >= 0 && current_y >= 0) &&
!(velocity_y <= 0 && current_y <= 0)) {
scroll_velocity_tracker_.Reset();
}
scroll_velocity_tracker_.AddSample(base::TimeTicks::Now(), pending_delta);
AnimationDirection direction = pending_delta.y() >= 0
? AnimationDirection::kHidingControls
: AnimationDirection::kShowingControls;
// Prevent the hide animation from running more than once per scroll until the
// user lifts their finger.
if (direction == AnimationDirection::kHidingControls &&
did_hide_this_scroll_ && !is_inertial) {
return;
}
// Wait for at least two samples to setup the snap animation as the velocity
// is not as accurate with a single sample.
if (scroll_velocity_tracker_.num_samples() < 2) {
return;
}
// Use inertial scrolling as a proxy for if the user has lifted their finger
// off the screen and to allow the minimum can-hide region to be used.
SetupSnapAnimation(direction, pending_delta,
/*use_minimum_can_hide_region=*/is_inertial);
}
void BrowserControlsOffsetManager::SetupSnapAnimation(
AnimationDirection direction,
const gfx::Vector2dF& scroll_delta,
bool use_minimum_can_hide_region) {
CHECK(direction == AnimationDirection::kHidingControls ||
direction == AnimationDirection::kShowingControls);
DCHECK((direction == AnimationDirection::kShowingControls &&
scroll_delta.y() <= 0) ||
(direction == AnimationDirection::kHidingControls &&
scroll_delta.y() >= 0));
if (HasAnimation()) {
return;
}
// The top controls have priority because they need to visually be in sync
// with the web contents.
const bool base_on_top_controls = TopControlsHeight();
const float controls_animated_height = ControlsAnimatedHeight();
// Return early if the controls have no height to animate.
if (controls_animated_height == 0.0f) {
return;
}
DCHECK_GT(controls_animated_height, 0.0f);
const float shown_ratio = base_on_top_controls ? TopControlsShownRatio()
: BottomControlsShownRatio();
const float min_shown_ratio = base_on_top_controls
? TopControlsMinShownRatio()
: BottomControlsMinShownRatio();
const float viewport_offset_y = client_->ViewportScrollOffset().y();
// Calculate the slowness factor based on how long it would take to animate
// the controls to their final position given the current velocity compared to
// the maximum animation duration.
float slowness =
std::clamp((controls_animated_height /
std::abs(scroll_velocity_tracker_.CurrentVelocity().y())) /
kShowHideMaxDurationMs,
0.0f, 1.0f);
const float trigger_threshold = SnapAnimationThreshold(slowness);
gfx::Tween::Type curve;
// The snap animation logic divides the page into four vertical regions:
//
// +-----------------------------+ Page Top
// | |
// | Always-Shown Region | Show controls immediately
// | (from page top) | when scrolling up.
// | |
// +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^+
// | |
// | (top gap) | Prevents show/hide loops.
// | |
// +vvvvvvvvvvvvvvvvvvvvvvvvvvvvv+
// | |
// | Can-Hide Region | Allows hiding controls
// | | when scrolling down.
// | |
// +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^+
// | |
// | (bottom gap) | Prevents show/hide loops.
// | |
// +-----------------------------+ Page Bottom
//
// The core goal is to prevent web contents from shifting. Controls should
// neither animate in at the page top nor hide unless there is enough web
// content below to absorb the counter-scroll.
//
// 1. Always-Shown Region: At the very top, the page does not counter-scroll,
// so an animation would shift the web contents down. By triggering the
// show animation even if the user has not scrolled up enough in the
// current scroll sequence, the chance of the controls animating in when
// scrolled to the top is minimized.
// 2. Top Gap: When top controls hide, the page counter-scrolls upward by the
// top controls' height. The gap ensures this counter-scroll doesn't push
// the viewport top back into the Always-Shown region, which can
// instantly trigger a show at the end of the hide animation.
// 3. Can-Hide Region: Starts dynamically below the gap. Slower scrolls
// push the start further down to prevent the user from scrolling up
// while the hide animation is running and reaching the page top before a
// show animation can start.
// 4. Bottom Gap: When bottom controls hide, the page
// counter-scrolls upward by the bottom controls' height. The gap ensures
// this counter-scroll doesn't push the viewport top back into the
// Always-Shown region, which can instantly trigger a show at the end of
// the hide animation.
//
// Note that if the page is short enough, the top and bottom gaps will
// overlap, and the Can-Hide Region will not exist.
if (direction == AnimationDirection::kHidingControls) {
auto [can_hide_region_height_top, can_hide_region_height_bottom] =
SnapAnimationCanHideRegionHeights(
use_minimum_can_hide_region ? 0.0f : slowness);
// Animate to hide the controls when the user scrolls down:
// - If the viewport offset is in the can-hide region
// - If the accumulated delta for this scroll is greater than the trigger
// threshold in the direction of hiding the controls
// - At most once per scroll to prevent the controls from thrashing between
// the shown and hidden states
if (viewport_offset_y <= can_hide_region_height_top ||
viewport_offset_y >= client_->MaxViewportScrollOffsetY() -
can_hide_region_height_bottom ||
accumulated_scroll_delta_ < trigger_threshold) {
return;
}
// No need to animate if the controls are at the final position.
if (shown_ratio == min_shown_ratio) {
return;
}
curve = gfx::Tween::ACCEL_30_DECEL_LIN;
did_hide_this_scroll_ = true;
} else {
// Animate to show the controls when the user scrolls up:
// - If the viewport offset is or is expected to be in the always-shown
// region
// - If the accumulated delta for this scroll is greater than the trigger
// threshold in the direction of showing the controls
//
// There is no restriction on how many times the show animation can run per
// scroll. This combined with the restriction on the hide animation is
// intended to leave the controls always showing if the user rapidly scrolls
// up and down.
if (viewport_offset_y + scroll_delta.y() >
SnapAnimationAlwaysShownRegionHeight() &&
accumulated_scroll_delta_ > -trigger_threshold) {
return;
}
// No need to animate if the controls are at the final position.
if (shown_ratio == 1.0f) {
return;
}
// The web contents might shift when the controls are animated to show in
// the always-shown region, in which case a linear curve is more visually
// pleasant.
curve = viewport_offset_y > controls_animated_height
? gfx::Tween::ACCEL_LIN_DECEL_60
: gfx::Tween::LINEAR;
}
SetupAnimation(direction,
gfx::Tween::IntValueBetween(slowness, kShowHideMinDurationMs,
kShowHideMaxDurationMs),
curve);
}
void BrowserControlsOffsetManager::ScrollEnd(
const gfx::Vector2dF& compensated_scroll_delta) {
if (pinch_gesture_active_)
return;
if (use_snap_animation_) {
// When browser controls offset manager receives a scroll end event, it
// means that the user has lifted their finger off the screen. At this
// point, the browser controls can be animated with very little risk of the
// user scrolling in the opposite direction before the animation completes.
// Thus, use minimum can-hide region and trigger the animation based on the
// current velocity (animation will run only if the controls are not at the
// final position). This allows the user to hide the controls a second time
// in the same scroll sequence, reveal the controls in the always-shown
// region, and reliably hide the controls near the top of the page even when
// scrolling slowly.
SetupSnapAnimation(scroll_velocity_tracker_.CurrentVelocity().y() >= 0.f
? AnimationDirection::kHidingControls
: AnimationDirection::kShowingControls,
gfx::Vector2dF(), /*use_minimum_can_hide_region=*/true);
scroll_velocity_tracker_.Reset();
did_hide_this_scroll_ = false;
return;
}
// If the user scrolled up but scroll delta was compensated for latency, the
// controls should still be shown when the scroll completes if it would have
// been shown otherwise. Both top and bottom controls' animation is triggered
// when scroll delta is negative (i.e. scrolling up), with the only difference
// being the specific height and ratio values.
float compensated_scroll_delta_y = compensated_scroll_delta.y();
if (!show_controls_when_scroll_completes_ &&
compensated_scroll_delta_y < 0.0f) {
float controls_height =
TopControlsHeight() ? TopControlsHeight() : BottomControlsHeight();
float controls_shown_ratio = TopControlsHeight()
? TopControlsShownRatio()
: BottomControlsShownRatio();
float controls_min_shown_ratio = TopControlsHeight()
? TopControlsMinShownRatio()
: BottomControlsMinShownRatio();
float normalized_shown_ratio_after_compensation = NormalizeShownRatio(
controls_shown_ratio +
std::abs(compensated_scroll_delta_y) / controls_height,
controls_min_shown_ratio);
if (normalized_shown_ratio_after_compensation >= controls_show_threshold_) {
show_controls_when_scroll_completes_ = true;
}
}
// See if we should animate the top bar in, in case there was a race between
// chrome showing the controls and the user performing a scroll. We only need
// to animate the top control if it's not fully shown.
if (show_controls_when_scroll_completes_ && TopControlsShownRatio() != 1.0f) {
SetupAnimation(AnimationDirection::kShowingControls,
kShowHideMaxDurationMs);
return;
}
StartAnimationIfNecessary();
}
void BrowserControlsOffsetManager::PinchBegin() {
DCHECK(!pinch_gesture_active_);
pinch_gesture_active_ = true;
StartAnimationIfNecessary();
}
void BrowserControlsOffsetManager::PinchEnd() {
DCHECK(pinch_gesture_active_);
// Pinch{Begin,End} will always occur within the scope of Scroll{Begin,End},
// so return to a state expected by the remaining scroll sequence.
pinch_gesture_active_ = false;
ScrollBegin();
}
gfx::Vector2dF BrowserControlsOffsetManager::Animate(
base::TimeTicks monotonic_time) {
if (!HasAnimation() || !client_->HaveRootScrollNode())
return gfx::Vector2dF();
if (unapplied_scroll_delta_ != 0.0f) {
float unapplied_scroll_delta = unapplied_scroll_delta_;
gfx::Vector2dF applied_delta =
ScrollBy(gfx::Vector2dF(0.0f, unapplied_scroll_delta));
unapplied_scroll_delta_ = unapplied_scroll_delta - applied_delta.y();
// Return zero to prevent the page from counter scrolling to give the effect
// of the controls sliding over the page contents.
return gfx::Vector2dF();
}
float old_top_offset = ContentTopOffset();
float old_bottom_offset = ContentBottomOffset();
std::optional<float> new_top_ratio =
top_controls_animation_.Tick(monotonic_time);
if (!new_top_ratio.has_value())
new_top_ratio = TopControlsShownRatio();
std::optional<float> new_bottom_ratio =
bottom_controls_animation_.Tick(monotonic_time);
if (!new_bottom_ratio.has_value())
new_bottom_ratio = BottomControlsShownRatio();
// Upon completion, the animation object is reset and is no longer
// initialized.
bool animation_completed = !top_controls_animation_.IsInitialized() &&
!bottom_controls_animation_.IsInitialized();
client_->SetCurrentBrowserControlsShownRatio(new_top_ratio.value(),
new_bottom_ratio.value());
float top_offset_delta = ContentTopOffset() - old_top_offset;
float bottom_offset_delta = ContentBottomOffset() - old_bottom_offset;
// If the user is in the always-shown region, snap to the shown state after
// the animation is completed. This prevents the controls from being hidden
// when the user scrolls to the top of the page while a hide animation is
// running.
if (use_snap_animation_ && animation_completed &&
client_->ViewportScrollOffset().y() <=
SnapAnimationAlwaysShownRegionHeight()) {
SetupSnapAnimation(AnimationDirection::kShowingControls, gfx::Vector2dF(),
/*use_minimum_can_hide_region=*/false);
}
if (top_min_height_change_in_progress_) {
// The change in top offset may be larger than the min-height, resulting in
// too low or too high |top_controls_min_height_offset_| values. So, we
// should clamp it to a valid range.
top_controls_min_height_offset_ =
std::clamp(top_controls_min_height_offset_ + top_offset_delta,
top_min_height_offset_animation_range_->first,
top_min_height_offset_animation_range_->second);
// Ticking the animation might reset it if it's at the final value.
top_min_height_change_in_progress_ =
top_controls_animation_.IsInitialized();
}
if (bottom_min_height_change_in_progress_) {
// The change in bottom offset may be larger than the min-height, resulting
// in too low or too high |bottom_controls_min_height_offset_| values. So,
// we should clamp it to a valid range.
bottom_controls_min_height_offset_ =
std::clamp(bottom_controls_min_height_offset_ + bottom_offset_delta,
bottom_min_height_offset_animation_range_->first,
bottom_min_height_offset_animation_range_->second);
// Ticking the animation might reset it if it's at the final value.
bottom_min_height_change_in_progress_ =
bottom_controls_animation_.IsInitialized();
// When shrinking the bottom controls, there may be a remaining offset
// mistake if the min height was decreased by more than the height was. This
// can be fixed by simply "resetting" the offset to the final minHeight
// value at the end of the animation. This only applies to shrinking
// animations, since this adjustment happens at the beginning for growing
// animations. This is done to avoid the bottom controls "lagging behind"
// the changes to the web content and exposing a blank space right above the
// bottom controls.
if (!bottom_min_height_change_in_progress_) {
bottom_controls_min_height_offset_ = BottomControlsMinHeight();
}
}
gfx::Vector2dF scroll_delta(0.0f, top_offset_delta);
return scroll_delta;
}
bool BrowserControlsOffsetManager::HasAnimation() {
return top_controls_animation_.IsInitialized() ||
bottom_controls_animation_.IsInitialized() ||
unapplied_scroll_delta_ != 0.0f;
}
void BrowserControlsOffsetManager::MaybeRecordHasExistingAnimationHistogram() {
if (!in_viz_process_ && use_snap_animation_) {
base::UmaHistogramBoolean(
"Android.BrowserControlsAnimationUpdate.HasExistingAnimation",
HasAnimation());
}
}
void BrowserControlsOffsetManager::ResetAnimations() {
MaybeRecordHasExistingAnimationHistogram();
// If the animation doesn't need to jump to the end, Animation::Reset() will
// return |std::nullopt|.
std::optional<float> top_ratio = top_controls_animation_.Reset();
std::optional<float> bottom_ratio = bottom_controls_animation_.Reset();
if (top_ratio.has_value() || bottom_ratio.has_value()) {
client_->SetCurrentBrowserControlsShownRatio(
top_ratio.has_value() ? top_ratio.value() : TopControlsShownRatio(),
bottom_ratio.has_value() ? bottom_ratio.value()
: BottomControlsShownRatio());
if (top_min_height_change_in_progress_) {
DCHECK(top_ratio.has_value());
top_controls_min_height_offset_ = TopControlsMinHeight();
}
if (bottom_min_height_change_in_progress_) {
DCHECK(bottom_ratio.has_value());
bottom_controls_min_height_offset_ = BottomControlsMinHeight();
}
}
top_min_height_change_in_progress_ = false;
bottom_min_height_change_in_progress_ = false;
top_min_height_offset_animation_range_.reset();
bottom_min_height_offset_animation_range_.reset();
}
void BrowserControlsOffsetManager::SetupAnimation(AnimationDirection direction,
int64_t duration_ms,
gfx::Tween::Type tween_type) {
DCHECK_NE(AnimationDirection::kNoAnimation, direction);
DCHECK(direction != AnimationDirection::kHidingControls ||
TopControlsShownRatio() > 0.0f || BottomControlsShownRatio() > 0.0f);
DCHECK(direction != AnimationDirection::kShowingControls ||
TopControlsShownRatio() < 1.0f || BottomControlsShownRatio() < 1.0f);
if (top_controls_animation_.IsInitialized() &&
top_controls_animation_.Direction() == direction &&
bottom_controls_animation_.IsInitialized() &&
bottom_controls_animation_.Direction() == direction) {
return;
}
MaybeRecordHasExistingAnimationHistogram();
if (!TopControlsHeight() && !BottomControlsHeight()) {
float ratio =
direction == AnimationDirection::kHidingControls ? 0.0f : 1.0f;
client_->SetCurrentBrowserControlsShownRatio(ratio, ratio);
return;
}
// Providing artificially larger/smaller stop ratios to make the animation
// faster if the start ratio is closer to stop ratio.
const float max_stop_ratio =
direction == AnimationDirection::kShowingControls ? 1 : -1;
float top_start_ratio = TopControlsShownRatio();
float top_stop_ratio = top_start_ratio + max_stop_ratio;
top_controls_animation_.Initialize(
direction, top_start_ratio, top_stop_ratio, duration_ms,
/*jump_to_end_on_reset=*/false, tween_type);
top_controls_animation_.SetBounds(TopControlsMinShownRatio(), 1.0f);
float bottom_start_ratio = BottomControlsShownRatio();
float bottom_stop_ratio = bottom_start_ratio + max_stop_ratio;
bottom_controls_animation_.Initialize(
direction, bottom_start_ratio, bottom_stop_ratio, duration_ms,
/*jump_to_end_on_reset=*/false, tween_type);
bottom_controls_animation_.SetBounds(BottomControlsMinShownRatio(), 1.0f);
unapplied_scroll_delta_ = 0.0f;
client_->DidChangeBrowserControlsPosition();
}
void BrowserControlsOffsetManager::StartAnimationIfNecessary() {
if ((TopControlsShownRatio() == TopControlsMinShownRatio() ||
TopControlsShownRatio() == 1.0f) &&
(BottomControlsShownRatio() == BottomControlsMinShownRatio() ||
BottomControlsShownRatio() == 1.0f)) {
return;
}
float normalized_top_ratio =
(TopControlsShownRatio() - TopControlsMinShownRatio()) /
(1.0f - TopControlsMinShownRatio());
if (normalized_top_ratio >= 1.0f - controls_hide_threshold_) {
// If we're showing so much that the hide threshold won't trigger, show.
SetupAnimation(AnimationDirection::kShowingControls,
kShowHideMaxDurationMs);
} else if (normalized_top_ratio <= controls_show_threshold_) {
// If we're showing so little that the show threshold won't trigger, hide.
SetupAnimation(AnimationDirection::kHidingControls, kShowHideMaxDurationMs);
} else {
// If we could be either showing or hiding, we determine which one to
// do based on whether or not the total scroll delta was moving up or
// down.
SetupAnimation(accumulated_scroll_delta_ <= 0.0f
? AnimationDirection::kShowingControls
: AnimationDirection::kHidingControls,
kShowHideMaxDurationMs);
}
}
void BrowserControlsOffsetManager::ResetBaseline() {
accumulated_scroll_delta_ = 0.0f;
unapplied_scroll_delta_ = 0.0f;
baseline_top_content_offset_ = ContentTopOffset();
baseline_bottom_content_offset_ = ContentBottomOffset();
}
void BrowserControlsOffsetManager::InitAnimationForHeightChange(
Animation* animation,
float start_ratio,
float stop_ratio) {
AnimationDirection direction = start_ratio < stop_ratio
? AnimationDirection::kShowingControls
: AnimationDirection::kHidingControls;
animation->Initialize(direction, start_ratio, stop_ratio,
kHeightChangeDurationMs, /*jump_to_end_on_reset=*/true,
gfx::Tween::LINEAR);
}
float BrowserControlsOffsetManager::OldTopControlsMinShownRatio() {
return old_browser_controls_params_.top_controls_height
? old_browser_controls_params_.top_controls_min_height /
old_browser_controls_params_.top_controls_height
: 0.0f;
}
float BrowserControlsOffsetManager::OldBottomControlsMinShownRatio() {
return old_browser_controls_params_.bottom_controls_height
? old_browser_controls_params_.bottom_controls_min_height /
old_browser_controls_params_.bottom_controls_height
: 0.0f;
}
void BrowserControlsOffsetManager::UpdateOldBrowserControlsParams() {
// No need to update the other two bool members as they aren't useful for this
// class.
old_browser_controls_params_.top_controls_height = TopControlsHeight();
old_browser_controls_params_.top_controls_min_height = TopControlsMinHeight();
old_browser_controls_params_.bottom_controls_height = BottomControlsHeight();
old_browser_controls_params_.bottom_controls_min_height =
BottomControlsMinHeight();
}
void BrowserControlsOffsetManager::SetTopMinHeightOffsetAnimationRange(
float from,
float to) {
top_min_height_offset_animation_range_ =
std::make_pair(std::min(from, to), std::max(from, to));
}
void BrowserControlsOffsetManager::SetBottomMinHeightOffsetAnimationRange(
float from,
float to) {
bottom_min_height_offset_animation_range_ =
std::make_pair(std::min(from, to), std::max(from, to));
}
bool BrowserControlsOffsetManager::IsAnimatingHeightChange() {
return (top_controls_animation_.IsInitialized() &&
top_controls_animation_.jump_to_end_on_reset()) ||
(bottom_controls_animation_.IsInitialized() &&
bottom_controls_animation_.jump_to_end_on_reset());
}
double BrowserControlsOffsetManager::PredictViewportBoundsDelta(
double current_bounds_delta,
gfx::Vector2dF scroll_distance) {
double adjustment = current_bounds_delta;
if (scroll_distance.y() > 0 && adjustment > 0) {
// We're scrolling down and started to hide controls. Let's assume they're
// going to be fully hidden by the end of the fling.
if (TopControlsShownRatio() < 1) {
adjustment += ContentTopOffset();
}
if (BottomControlsShownRatio() < 1) {
adjustment += ContentBottomOffset();
}
}
if (scroll_distance.y() < 0 && adjustment < 0) {
// We're scrolling up and started to show controls. Let's assume they're
// going to be fully shown by the end of the fling.
if (TopControlsShownRatio() > 0) {
adjustment -= TopControlsHeight() - ContentTopOffset();
}
if (BottomControlsShownRatio() > 0) {
adjustment -= BottomControlsHeight() - ContentBottomOffset();
}
}
return adjustment;
}
// class Animation
BrowserControlsOffsetManager::Animation::Animation() {}
void BrowserControlsOffsetManager::Animation::Initialize(
AnimationDirection direction,
float start_value,
float stop_value,
int64_t duration,
bool jump_to_end_on_reset,
gfx::Tween::Type tween_type) {
direction_ = direction;
start_value_ = start_value;
stop_value_ = stop_value;
duration_ = base::Milliseconds(duration);
initialized_ = true;
jump_to_end_on_reset_ = jump_to_end_on_reset;
tween_type_ = tween_type;
SetBounds(std::min(start_value_, stop_value_),
std::max(start_value_, stop_value_));
}
std::optional<float> BrowserControlsOffsetManager::Animation::Tick(
base::TimeTicks monotonic_time) {
if (!IsInitialized())
return std::nullopt;
if (!started_) {
start_time_ = monotonic_time;
stop_time_ = start_time_ + duration_;
started_ = true;
}
float value = gfx::Tween::ClampedFloatValueBetween(
monotonic_time, start_time_, start_value_, stop_time_, stop_value_,
tween_type_);
if (IsComplete(value)) {
value = FinalValue();
Reset();
}
return value;
}
void BrowserControlsOffsetManager::Animation::SetBounds(float min, float max) {
min_value_ = min;
max_value_ = max;
}
std::optional<float> BrowserControlsOffsetManager::Animation::Reset() {
auto ret =
jump_to_end_on_reset_
? std::make_optional(std::clamp(stop_value_, min_value_, max_value_))
: std::nullopt;
started_ = false;
initialized_ = false;
start_time_ = base::TimeTicks();
start_value_ = 0.0f;
stop_time_ = base::TimeTicks();
stop_value_ = 0.0f;
direction_ = AnimationDirection::kNoAnimation;
duration_ = base::TimeDelta();
min_value_ = 0.0f;
max_value_ = 1.0f;
jump_to_end_on_reset_ = false;
return ret;
}
bool BrowserControlsOffsetManager::Animation::IsComplete(float value) {
return (direction_ == AnimationDirection::kShowingControls &&
(value >= stop_value_ || value >= max_value_)) ||
(direction_ == AnimationDirection::kHidingControls &&
(value <= stop_value_ || value <= min_value_));
}
float BrowserControlsOffsetManager::Animation::FinalValue() {
return std::clamp(stop_value_, min_value_, max_value_);
}
} // namespace cc