blob: 15ab74967bd515612b192d451bc8d31f0e120dd0 [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/fullscreen/ui_bundled/fullscreen_model.h"
#import <algorithm>
#import "base/check_op.h"
#import "base/memory/raw_ptr.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "ios/chrome/browser/fullscreen/ui_bundled/fullscreen_constants.h"
#import "ios/chrome/browser/fullscreen/ui_bundled/fullscreen_metrics.h"
#import "ios/chrome/browser/fullscreen/ui_bundled/fullscreen_model_observer.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/toolbar/ui_bundled/fullscreen/toolbars_size.h"
#import "ios/chrome/common/ui/util/ui_util.h"
#import "ios/web/common/features.h"
namespace {
// Default value of the mount the scroll must exceed to begin entering and
// exiting fullscreen when the `kFullscreenScrollThreshold` feature is enabled.
constexpr CGFloat kScrollThresholdDefault = 10;
// Object that increments `counter` by 1 for its lifetime.
class ScopedIncrementer {
public:
explicit ScopedIncrementer(size_t* counter) : counter_(counter) {
++(*counter_);
}
~ScopedIncrementer() { --(*counter_); }
private:
raw_ptr<size_t> counter_;
};
} // namespace
FullscreenModel::FullscreenModel() {
UpdateSpeed();
if (web::features::IsFullscreenScrollThresholdEnabled()) {
scroll_threshold_ = GetFieldTrialParamByFeatureAsDouble(
web::features::kFullscreenScrollThreshold,
web::features::kFullscreenScrollThresholdAmount,
kScrollThresholdDefault);
}
}
FullscreenModel::~FullscreenModel() {
[toolbars_size_ removeObserver:this];
}
void FullscreenModel::AddObserver(FullscreenModelObserver* observer) {
observers_.AddObserver(observer);
}
void FullscreenModel::RemoveObserver(FullscreenModelObserver* observer) {
observers_.RemoveObserver(observer);
}
void FullscreenModel::IncrementDisabledCounter() {
if (++disabled_counter_ == 1U) {
ScopedIncrementer disabled_incrementer(&observer_callback_count_);
for (auto& observer : observers_) {
observer.FullscreenModelEnabledStateChanged(this);
}
// Fullscreen observers are expected to show the toolbar when fullscreen is
// disabled. Update the internal state to match this.
SetProgress(1.0);
UpdateBaseOffset();
}
}
void FullscreenModel::DecrementDisabledCounter() {
DCHECK_GT(disabled_counter_, 0U);
if (!--disabled_counter_) {
ScopedIncrementer enabled_incrementer(&observer_callback_count_);
for (auto& observer : observers_) {
observer.FullscreenModelEnabledStateChanged(this);
}
}
}
void FullscreenModel::ForceEnterFullscreen() {
SetProgress(0.0);
}
void FullscreenModel::ResetForNavigation() {
if (IsForceFullscreenMode()) {
return;
}
base::UmaHistogramEnumeration(kExitFullscreenModeTransitionReasonHistogram,
FullscreenModeTransitionReason::kForcedByCode);
progress_ = 1.0;
scrolling_ = false;
start_scrolling_time_ = std::nullopt;
is_scrolling_time_recorded_ = false;
if (base::FeatureList::IsEnabled(web::features::kSmoothScrollingDefault)) {
base_offset_ = NAN;
}
ScopedIncrementer reset_incrementer(&observer_callback_count_);
for (auto& observer : observers_) {
observer.FullscreenModelWasReset(this);
}
}
void FullscreenModel::IgnoreRemainderOfCurrentScroll() {
if (!scrolling_) {
return;
}
ignoring_current_scroll_ = true;
}
void FullscreenModel::AnimationEndedWithProgress(CGFloat progress) {
DCHECK_GE(progress, 0.0);
DCHECK_LE(progress, 1.0);
// Since this is being set by the animator instead of by scroll events, do not
// broadcast the new progress value.
progress_ = progress;
}
void FullscreenModel::ToolbarsHeightDidChange() {
if (base::FeatureList::IsEnabled(web::features::kSmoothScrollingDefault)) {
base_offset_ = NAN;
}
ScopedIncrementer toolbar_height_incrementer(&observer_callback_count_);
for (auto& observer : observers_) {
observer.FullscreenModelToolbarHeightsUpdated(this);
}
}
CGFloat FullscreenModel::GetCollapsedTopToolbarHeight() const {
return toolbars_size_ ? toolbars_size_.collapsedTopToolbarHeight : 0.0;
}
CGFloat FullscreenModel::GetExpandedTopToolbarHeight() const {
return toolbars_size_ ? toolbars_size_.expandedTopToolbarHeight : 0.0;
}
CGFloat FullscreenModel::GetExpandedBottomToolbarHeight() const {
return toolbars_size_ ? toolbars_size_.expandedBottomToolbarHeight : 0.0;
}
CGFloat FullscreenModel::GetCollapsedBottomToolbarHeight() const {
return toolbars_size_ ? toolbars_size_.collapsedBottomToolbarHeight : 0.0;
}
void FullscreenModel::SetScrollViewHeight(CGFloat scroll_view_height) {
scroll_view_height_ = scroll_view_height;
UpdateDisabledCounterForContentHeight();
}
CGFloat FullscreenModel::GetScrollViewHeight() const {
return scroll_view_height_;
}
void FullscreenModel::SetContentHeight(CGFloat content_height) {
content_height_ = content_height;
UpdateDisabledCounterForContentHeight();
}
CGFloat FullscreenModel::GetContentHeight() const {
return content_height_;
}
void FullscreenModel::SetTopContentInset(CGFloat top_inset) {
top_inset_ = top_inset;
}
CGFloat FullscreenModel::GetTopContentInset() const {
return top_inset_;
}
void FullscreenModel::SetYContentOffset(CGFloat y_content_offset) {
CGFloat from_offset = y_content_offset_;
y_content_offset_ = y_content_offset;
if (y_content_offset_ == from_offset) {
// The scroll did not change `y_content_offset_` (e.g., reached the bottom
// of the page).
SetLastScrollDirection(FullscreenModelScrollDirection::kNone);
}
switch (ActionForScrollFromOffset(from_offset)) {
case ScrollAction::kUpdateBaseOffset:
UpdateBaseOffset();
break;
case ScrollAction::kUpdateProgress:
// Updates `scroll_direction_` according to the offset.
if (y_content_offset_ > from_offset) {
SetLastScrollDirection(FullscreenModelScrollDirection::kDown);
} else if (y_content_offset_ < from_offset) {
SetLastScrollDirection(FullscreenModelScrollDirection::kUp);
}
UpdateProgress();
break;
case ScrollAction::kUpdateBaseOffsetAndProgress:
CHECK(
base::FeatureList::IsEnabled(web::features::kSmoothScrollingDefault));
UpdateBaseOffset();
UpdateProgress();
break;
case ScrollAction::kIgnore:
// no op.
break;
}
}
CGFloat FullscreenModel::GetYContentOffset() const {
return y_content_offset_;
}
void FullscreenModel::SetScrollViewIsScrolling(bool scrolling) {
if (scrolling_ == scrolling) {
return;
}
scrolling_ = scrolling;
if (scrolling_) {
// Record the start time when the first scroll happened in the current
// navigation.
if (!start_scrolling_time_.has_value() && can_collapse_toolbar()) {
is_scrolling_time_recorded_ = false;
start_scrolling_time_ = base::TimeTicks::Now();
}
// Notify observers that the scroll event has begun.
ScopedIncrementer scroll_started_incrementer(&observer_callback_count_);
for (auto& observer : observers_) {
observer.FullscreenModelScrollEventStarted(this);
}
} else {
if (is_scrolled_to_bottom() && !is_scrolling_time_recorded_ &&
start_scrolling_time_.has_value()) {
// Record the time spent scrolling to the bottom of the page.
base::UmaHistogramLongTimes(
kFullscreenScrollToTheBottomTime,
base::TimeTicks::Now() - start_scrolling_time_.value());
// Avoid recording multiple time in the same page.
is_scrolling_time_recorded_ = true;
}
// Stop ignoring the current scroll.
ignoring_current_scroll_ = false;
// Notify observers that the scroll event has ended.
ScopedIncrementer scroll_ended_incrementer(&observer_callback_count_);
for (auto& observer : observers_) {
observer.FullscreenModelScrollEventEnded(this);
}
}
}
bool FullscreenModel::IsScrollViewScrolling() const {
return scrolling_;
}
void FullscreenModel::SetScrollViewIsZooming(bool zooming) {
zooming_ = zooming;
}
bool FullscreenModel::IsScrollViewZooming() const {
return zooming_;
}
void FullscreenModel::SetScrollViewIsDragging(bool dragging) {
if (dragging_ == dragging) {
return;
}
dragging_ = dragging;
if (dragging_) {
SetLastScrollDirection(FullscreenModelScrollDirection::kNone);
// Update the base offset for each new scroll event.
UpdateBaseOffset();
offset_at_start_of_drag_ = y_content_offset_;
// Re-rendering events are ignored during scrolls since disabling the model
// mid-scroll leads to choppy animations. If the content was re-rendered
// to be too short to collapse the toolbars, the model should be disabled
// to prevent the subsequent scroll.
UpdateDisabledCounterForContentHeight();
}
}
bool FullscreenModel::IsScrollViewDragging() const {
return dragging_;
}
void FullscreenModel::SetResizesScrollView(bool resizes_scroll_view) {
resizes_scroll_view_ = resizes_scroll_view;
}
bool FullscreenModel::ResizesScrollView() const {
return resizes_scroll_view_;
}
void FullscreenModel::SetWebViewSafeAreaInsets(UIEdgeInsets safe_area_insets) {
if (UIEdgeInsetsEqualToEdgeInsets(safe_area_insets_, safe_area_insets)) {
return;
}
safe_area_insets_ = safe_area_insets;
UpdateDisabledCounterForContentHeight();
}
UIEdgeInsets FullscreenModel::GetWebViewSafeAreaInsets() const {
return safe_area_insets_;
}
void FullscreenModel::SetForceFullscreenMode(bool force_fullscreen_mode) {
if (force_fullscreen_mode) {
force_fullscreen_mode_counter_++;
} else {
force_fullscreen_mode_counter_--;
}
DCHECK_GE(force_fullscreen_mode_counter_, 0U);
}
bool FullscreenModel::IsForceFullscreenMode() const {
return force_fullscreen_mode_counter_ > 0;
}
void FullscreenModel::SetInsetsUpdateEnabled(bool enabled) {
insets_update_enabled_ = enabled;
}
bool FullscreenModel::IsInsetsUpdateEnabled() const {
return insets_update_enabled_;
}
FullscreenModel::ScrollAction FullscreenModel::ActionForScrollFromOffset(
CGFloat from_offset) const {
// Update the base offset but don't recalculate progress if:
// - the model is disabled,
// - the scroll is not triggered by a user action,
// - the sroll view is zooming,
// - the scroll is triggered from a FullscreenModelObserver callback,
// - there is no toolbar,
// - the scroll offset doesn't change,
// - the scroll has not exceeded the required threshold.
if (!enabled() || !scrolling_ || zooming_ || observer_callback_count_ ||
AreCGFloatsEqual(get_toolbar_height_delta(), 0.0) ||
AreCGFloatsEqual(y_content_offset_, from_offset) ||
!ScrollThresholdExceeded()) {
return ScrollAction::kUpdateBaseOffset;
}
// Ignore if:
// - explicitly requested via IgnoreRemainderOfCurrentScroll(),
// - the scroll is a bounce-up animation at the top,
// - the scroll is attempting to scroll content up when it already fits,
// - the scroll is attempting to scroll past the bottom of the content when
// the scroll view is being resized (the rebound scroll animation
// interferes with the frame resizing).
bool scrolling_content_down = y_content_offset_ - from_offset < 0.0;
bool scrolling_past_top = y_content_offset_ <= -top_inset_;
bool content_fits = content_height_ <= scroll_view_height_ - top_inset_;
bool scrolling_past_bottom =
y_content_offset_ + scroll_view_height_ + top_inset_ +
toolbars_size_.expandedTopToolbarHeight +
toolbars_size_.expandedBottomToolbarHeight -
(toolbars_size_.collapsedTopToolbarHeight -
toolbars_size_.collapsedBottomToolbarHeight) >=
content_height_;
if (ignoring_current_scroll_ ||
(scrolling_past_top && !scrolling_content_down) ||
(content_fits && !scrolling_content_down) ||
(resizes_scroll_view_ && scrolling_past_bottom)) {
return ScrollAction::kIgnore;
}
// All other scrolls should result in an updated progress value. If the model
// doesn't have a base offset, it should also be updated.
if (base::FeatureList::IsEnabled(web::features::kSmoothScrollingDefault)) {
return has_base_offset() ? ScrollAction::kUpdateProgress
: ScrollAction::kUpdateBaseOffsetAndProgress;
} else {
return ScrollAction::kUpdateProgress;
}
}
void FullscreenModel::SetLastScrollDirection(
FullscreenModelScrollDirection direction) {
if (direction == fullscreen_scroll_direction_) {
return;
}
if (fullscreen_scroll_direction_ != FullscreenModelScrollDirection::kNone) {
UpdateBaseOffset();
}
fullscreen_scroll_direction_ = direction;
}
void FullscreenModel::UpdateBaseOffset() {
base_offset_ = y_content_offset_ -
(1.0 - progress_) * get_toolbar_height_delta() / speed_;
}
void FullscreenModel::UpdateSpeed() {
if (base::FeatureList::IsEnabled(web::features::kSmoothScrollingDefault) ||
!base::FeatureList::IsEnabled(kFullscreenTransitionSpeed)) {
return;
}
if (FullscreenTransitionSpeedParam() == FullscreenTransitionSpeed::kSlower) {
speed_ = 0.50;
}
if (FullscreenTransitionSpeedParam() == FullscreenTransitionSpeed::kFaster) {
speed_ = 1.50;
}
}
void FullscreenModel::UpdateProgress() {
const CGFloat delta = base_offset_ - y_content_offset_;
const CGFloat toolbar_height_delta = get_toolbar_height_delta();
if (base::FeatureList::IsEnabled(web::features::kSmoothScrollingDefault)) {
SetProgress(1.0 + delta / toolbar_height_delta);
return;
} else {
if (IsFullscreenTransitionSpeedSet()) {
SetProgress(1.0 + (delta * speed_) / toolbar_height_delta);
return;
}
SetProgress(1.0 + delta / toolbar_height_delta);
}
}
void FullscreenModel::UpdateDisabledCounterForContentHeight() {
// Sometimes the content size and scroll view sizes are updated mid-scroll
// such that the scroll view height is updated before the content is re-
// rendered, causing the model to be disabled. These changes should be
// ignored while the content is scrolling.
if (scrolling_) {
return;
}
// The model should be disabled when the content fits.
CGFloat disabling_threshold = scroll_view_height_;
if (resizes_scroll_view_) {
// When Smooth Scrolling is disabled, the scroll view can sometimes be
// resized to account for the viewport insets after the page has been
// rendered, so account for the maximum toolbar insets in the threshold.
disabling_threshold +=
GetExpandedTopToolbarHeight() + GetExpandedBottomToolbarHeight();
} else {
// After reloads, pages whose viewports fit the screen are sometimes
// resized to account for the safe area insets. Adding these to the
// threshold helps prevent fullscreen from beeing re-enabled in this
// case.
// TODO(crbug.com/41437113): This logic can potentially disable
// fullscreen for short pages in which this bug does not occur. It
// should be removed once the page can be reloaded without resizing.
disabling_threshold += safe_area_insets_.top + safe_area_insets_.bottom;
}
// Don't disable fullscreen if both heights have not been received.
bool areBothHeightsSet = !AreCGFloatsEqual(content_height_, 0.0) &&
!AreCGFloatsEqual(scroll_view_height_, 0.0);
bool disable = areBothHeightsSet && content_height_ <= disabling_threshold;
if (disabled_for_short_content_ == disable) {
return;
}
disabled_for_short_content_ = disable;
if (disable) {
IncrementDisabledCounter();
} else {
DecrementDisabledCounter();
}
}
void FullscreenModel::SetProgress(CGFloat progress) {
progress = std::min(static_cast<CGFloat>(1.0), progress);
progress = std::max(static_cast<CGFloat>(0.0), progress);
if (AreCGFloatsEqual(progress_, progress)) {
return;
}
if (progress == 0.0 && progress_ > 0.0) {
base::UmaHistogramEnumeration(
kEnterFullscreenModeTransitionReasonHistogram,
FullscreenModeTransitionReason::kUserControlled);
} else if (progress == 1.0 && progress_ < 1.0) {
base::UmaHistogramEnumeration(
kExitFullscreenModeTransitionReasonHistogram,
FullscreenModeTransitionReason::kUserControlled);
}
progress_ = progress;
// Prevent observer callbacks from recursively setting progress.
if (setting_progress_) {
return;
}
setting_progress_ = true;
ScopedIncrementer progress_incrementer(&observer_callback_count_);
for (auto& observer : observers_) {
observer.FullscreenModelProgressUpdated(this);
}
setting_progress_ = false;
}
void FullscreenModel::OnScrollViewSizeBroadcasted(CGSize scroll_view_size) {
CHECK(base::FeatureList::IsEnabled(web::features::kSmoothScrollingDefault));
SetScrollViewHeight(scroll_view_size.height);
}
void FullscreenModel::OnScrollViewContentSizeBroadcasted(CGSize content_size) {
SetContentHeight(content_size.height);
}
void FullscreenModel::OnScrollViewContentInsetBroadcasted(
UIEdgeInsets content_inset) {
CHECK(base::FeatureList::IsEnabled(web::features::kSmoothScrollingDefault));
SetTopContentInset(content_inset.top);
}
void FullscreenModel::OnContentScrollOffsetBroadcasted(CGFloat offset) {
CHECK(base::FeatureList::IsEnabled(web::features::kSmoothScrollingDefault));
SetYContentOffset(offset);
}
void FullscreenModel::OnScrollViewIsScrollingBroadcasted(bool scrolling) {
CHECK(base::FeatureList::IsEnabled(web::features::kSmoothScrollingDefault));
SetScrollViewIsScrolling(scrolling);
}
void FullscreenModel::OnScrollViewIsZoomingBroadcasted(bool zooming) {
CHECK(base::FeatureList::IsEnabled(web::features::kSmoothScrollingDefault));
SetScrollViewIsZooming(zooming);
}
void FullscreenModel::OnScrollViewIsDraggingBroadcasted(bool dragging) {
CHECK(base::FeatureList::IsEnabled(web::features::kSmoothScrollingDefault));
SetScrollViewIsDragging(dragging);
}
void FullscreenModel::OnCollapsedTopToolbarHeightBroadcasted(CGFloat height) {
CHECK(!IsRefactorToolbarsSize());
ToolbarsHeightDidChange();
}
void FullscreenModel::OnExpandedTopToolbarHeightBroadcasted(CGFloat height) {
CHECK(!IsRefactorToolbarsSize());
ToolbarsHeightDidChange();
}
void FullscreenModel::OnCollapsedBottomToolbarHeightBroadcasted(
CGFloat height) {
CHECK(!IsRefactorToolbarsSize());
ToolbarsHeightDidChange();
}
void FullscreenModel::OnExpandedBottomToolbarHeightBroadcasted(CGFloat height) {
CHECK(!IsRefactorToolbarsSize());
ToolbarsHeightDidChange();
}
void FullscreenModel::SetToolbarsSize(ToolbarsSize* toolbars_size) {
toolbars_size_ = toolbars_size;
ToolbarsHeightDidChange();
if (IsRefactorToolbarsSize() && toolbars_size_) {
[toolbars_size addObserver:this];
}
}
void FullscreenModel::OnTopToolbarHeightChanged() {
CHECK(IsRefactorToolbarsSize());
ToolbarsHeightDidChange();
}
void FullscreenModel::OnBottomToolbarHeightChanged() {
CHECK(IsRefactorToolbarsSize());
ToolbarsHeightDidChange();
}
bool FullscreenModel::ScrollThresholdExceeded() const {
if (web::features::IsFullscreenScrollThresholdEnabled()) {
// When scrolled to the very top, the threshold should be ignored so that
// fullscreen can be smoothly exited.
if (y_content_offset_ <= 0.0) {
return true;
}
return std::abs(y_content_offset_ - offset_at_start_of_drag_) >
scroll_threshold_;
}
return true;
}