blob: 068383424da4501a98496b68edd0247d3c786457 [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_mediator.h"
#import "base/check_op.h"
#import "base/memory/ptr_util.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_animator.h"
#import "ios/chrome/browser/fullscreen/ui_bundled/fullscreen_content_adjustment_util.h"
#import "ios/chrome/browser/fullscreen/ui_bundled/fullscreen_controller_observer.h"
#import "ios/chrome/browser/fullscreen/ui_bundled/fullscreen_metrics.h"
#import "ios/chrome/browser/fullscreen/ui_bundled/fullscreen_model.h"
#import "ios/chrome/browser/fullscreen/ui_bundled/fullscreen_web_view_resizer.h"
#import "ios/chrome/common/ui/util/ui_util.h"
#import "ios/web/public/web_state.h"
FullscreenMediator::FullscreenMediator(FullscreenController* controller,
FullscreenModel* model)
: controller_(controller),
model_(model),
resizer_([[FullscreenWebViewResizer alloc] initWithModel:model]) {
DCHECK(controller_);
DCHECK(model_);
model_->AddObserver(this);
}
FullscreenMediator::~FullscreenMediator() {
// Disconnect() is expected to be called before deallocation.
DCHECK(!controller_);
DCHECK(!model_);
}
void FullscreenMediator::SetWebState(web::WebState* webState) {
resizer_.webState = webState;
}
void FullscreenMediator::SetIsBrowserTraitCollectionUpdating(bool updating) {
if (updating_browser_trait_collection_ == updating) {
return;
}
updating_browser_trait_collection_ = updating;
if (updating_browser_trait_collection_) {
resizer_.compensateFrameChangeByOffset = NO;
scrolled_to_top_during_trait_collection_updates_ =
model_->is_scrolled_to_top();
} else {
resizer_.compensateFrameChangeByOffset = YES;
if (scrolled_to_top_during_trait_collection_updates_) {
// If the content was scrolled to the top when the trait collection began
// updating, changes in toolbar heights may cause the top of the page to
// become hidden. Ensure that the page remains scrolled to the top after
// the trait collection finishes updating.
web::WebState* web_state = resizer_.webState;
if (web_state) {
MoveContentBelowHeader(web_state->GetWebViewProxy(), model_);
}
scrolled_to_top_during_trait_collection_updates_ = false;
}
}
}
void FullscreenMediator::EnterFullscreen() {
if (model_->enabled()) {
AnimateWithStyle(FullscreenAnimatorStyle::ENTER_FULLSCREEN);
}
}
void FullscreenMediator::ExitFullscreen(
FullscreenExitReason fullscreen_exit_reason) {
if (model_->IsForceFullscreenMode()) {
return;
}
// Instruct the model to ignore the remainder of the current scroll when
// starting this animator. This prevents the toolbar from immediately being
// hidden if AnimateModelReset() is called while a scroll view is
// decelerating.
model_->IgnoreRemainderOfCurrentScroll();
fullscreen_exit_reason_ = fullscreen_exit_reason;
AnimateWithStyle(FullscreenAnimatorStyle::EXIT_FULLSCREEN);
}
void FullscreenMediator::ForceEnterFullscreen() {
model_->ForceEnterFullscreen();
}
void FullscreenMediator::ExitFullscreenWithoutAnimation() {
model_->ResetForNavigation();
}
void FullscreenMediator::Disconnect() {
for (auto& observer : observers_) {
observer.FullscreenControllerWillShutDown(controller_);
}
resizer_.webState = nullptr;
resizer_ = nil;
[animator_ stopAnimation:YES];
animator_ = nil;
model_->RemoveObserver(this);
model_ = nullptr;
controller_ = nullptr;
}
void FullscreenMediator::FullscreenModelToolbarHeightsUpdated(
FullscreenModel* model) {
for (auto& observer : observers_) {
observer.FullscreenViewportInsetRangeChanged(controller_,
model_->min_toolbar_insets(),
model_->max_toolbar_insets());
}
BOOL compensateFrameChangeByOffset = resizer_.compensateFrameChangeByOffset;
// The compensateFrameChangeByOffset property determines whether the webview's
// contentOffset should be adjusted to visually "pin" the web content during
// fullscreen transitions. This makes the content appear to move as a whole
// rather than scrolling internally. However, when toolbar heights are updated
// (as in FullscreenModelToolbarHeightsUpdated), setting
// compensateFrameChangeByOffset to YES would incorrectly shift the webview's
// contentOffset, leading to unexpected content positioning. To prevent this,
// compensateFrameChangeByOffset is set to NO here, ensuring the contentOffset
// remains unchanged during the update.
resizer_.compensateFrameChangeByOffset = NO;
// Changes in the toolbar heights modifies the visible viewport so the WebView
// needs to be resized as needed.
[resizer_ updateForCurrentState];
// Restore the compensateFrameChangeByOffset property to its original value.
resizer_.compensateFrameChangeByOffset = compensateFrameChangeByOffset;
}
void FullscreenMediator::FullscreenModelProgressUpdated(
FullscreenModel* model) {
DCHECK_EQ(model_, model);
// Stops the animation only if there is a current animation running.
if (animator_ && animator_.state == UIViewAnimatingStateActive) {
StopAnimating(true /* update_model */);
}
for (auto& observer : observers_) {
observer.FullscreenProgressUpdated(controller_, model_->progress());
}
[resizer_ updateForCurrentState];
}
void FullscreenMediator::FullscreenModelEnabledStateChanged(
FullscreenModel* model) {
DCHECK_EQ(model_, model);
// Stops the animation only if there is a current animation running.
if (animator_ && animator_.state == UIViewAnimatingStateActive) {
StopAnimating(true /* update_model */);
}
for (auto& observer : observers_) {
observer.FullscreenEnabledStateChanged(controller_, model->enabled());
}
}
void FullscreenMediator::FullscreenModelScrollEventStarted(
FullscreenModel* model) {
DCHECK_EQ(model_, model);
start_progress_ = model_->progress();
StopAnimating(true /* update_model */);
// Show the toolbars if the user begins a scroll past the bottom edge of the
// screen and the toolbars have been fully collapsed.
if (model_->enabled() && model_->is_scrolled_to_bottom() &&
AreCGFloatsEqual(model_->progress(), 0.0) &&
model_->can_collapse_toolbar()) {
ExitFullscreen(FullscreenExitReason::kForcedByCode);
}
}
void FullscreenMediator::FullscreenModelScrollEventEnded(
FullscreenModel* model) {
DCHECK_EQ(model_, model);
if (base::FeatureList::IsEnabled(web::features::kSmoothScrollingDefault)) {
if (model_->progress() >= 0.5) {
fullscreen_exit_reason_ =
FullscreenExitReason::kUserInitiatedFinishedByCode;
AnimateWithStyle(FullscreenAnimatorStyle::EXIT_FULLSCREEN);
} else {
AnimateWithStyle(FullscreenAnimatorStyle::ENTER_FULLSCREEN);
}
return;
}
bool scrolled_to_bottom = model_->enabled() &&
model_->is_scrolled_to_bottom() &&
AreCGFloatsEqual(model_->progress(), 0.0) &&
model_->can_collapse_toolbar();
if (scrolled_to_bottom) {
if (has_reached_bottom_once_) {
// Subsequent times reaching the bottom: exit fullscreen.
fullscreen_exit_reason_ = FullscreenExitReason::kBottomReached;
AnimateWithStyle(FullscreenAnimatorStyle::EXIT_FULLSCREEN);
} else {
// First time reaching the bottom.
has_reached_bottom_once_ = true;
}
} else {
has_reached_bottom_once_ = false;
AnimateWithStyle(
AnimatorStyleFromScrollDirection(model_->GetLastScrollDirection()));
}
}
void FullscreenMediator::FullscreenModelWasReset(FullscreenModel* model) {
has_reached_bottom_once_ = false;
// Stop any in-progress animations. Don't update the model because this
// callback occurs after the model's state is reset, and updating the model
// the with active animator's current value would overwrite the reset value.
StopAnimating(false /* update_model */);
// Update observers for the reset progress value and set the inset range in
// case this is a new WebState.
for (auto& observer : observers_) {
observer.FullscreenViewportInsetRangeChanged(
controller_, controller_->GetMinViewportInsets(),
controller_->GetMaxViewportInsets());
observer.FullscreenProgressUpdated(controller_, model_->progress());
}
[resizer_ updateForCurrentState];
}
void FullscreenMediator::AnimateWithStyle(FullscreenAnimatorStyle style) {
if (animator_ && animator_.style == style) {
return;
}
StopAnimating(true);
DCHECK(!animator_);
// Early return if there is no progress change.
CGFloat start_progress = model_->progress();
CGFloat final_progress = GetFinalFullscreenProgressForAnimation(style);
if (AreCGFloatsEqual(start_progress, final_progress)) {
return;
}
// Create the animator and set up its completion block.
animator_ = [[FullscreenAnimator alloc] initWithStartProgress:start_progress
style:style];
base::WeakPtr<FullscreenMediator> weak_mediator = weak_factory_.GetWeakPtr();
[animator_ addAnimations:^{
// Updates the WebView frame during the animation to have it animated.
FullscreenMediator* mediator = weak_mediator.get();
if (mediator) {
[mediator->resizer_ forceToUpdateToProgress:final_progress];
}
}];
[animator_ addCompletion:^(UIViewAnimatingPosition finalPosition) {
DCHECK_EQ(finalPosition, UIViewAnimatingPositionEnd);
FullscreenMediator* mediator = weak_mediator.get();
if (!mediator) {
return;
}
mediator->model_->AnimationEndedWithProgress(final_progress);
mediator->animator_ = nil;
// Histogram for entering or exiting Fullscreen mode based on
// FullscreenModeTransitionReason value.
if (model_->progress() == 0) {
base::UmaHistogramEnumeration(
kEnterFullscreenModeTransitionReasonHistogram,
FullscreenModeTransitionReason::kUserInitiatedFinishedByCode);
} else if (model_->progress() == 1) {
RecordFullscreenExitMode();
}
for (auto& observer : mediator->observers_) {
observer.FullscreenDidAnimate(mediator->controller_, style);
}
}];
// Notify observers that the animation will occur.
for (auto& observer : observers_) {
observer.FullscreenWillAnimate(controller_, animator_);
}
// Only start the animator if animations have been added.
if (animator_.hasAnimations) {
[animator_ startAnimation];
} else {
animator_ = nil;
}
}
void FullscreenMediator::StopAnimating(bool update_model) {
if (!animator_) {
return;
}
DCHECK_EQ(animator_.state, UIViewAnimatingStateActive);
if (update_model) {
model_->AnimationEndedWithProgress(animator_.currentProgress);
}
[animator_ stopAnimation:YES];
animator_ = nil;
}
void FullscreenMediator::ResizeHorizontalInsets() {
for (auto& observer : observers_) {
observer.ResizeHorizontalInsets(controller_);
}
}
FullscreenAnimatorStyle FullscreenMediator::AnimatorStyleFromScrollDirection(
FullscreenModelScrollDirection direction) {
switch (direction) {
case FullscreenModelScrollDirection::kUp:
fullscreen_exit_reason_ =
FullscreenExitReason::kUserInitiatedFinishedByCode;
return FullscreenAnimatorStyle::EXIT_FULLSCREEN;
case FullscreenModelScrollDirection::kNone:
// Leave in fullscreen, if still in fullscreen.
fullscreen_exit_reason_ = FullscreenExitReason::kNoChange;
return AreCGFloatsEqual(model_->progress(), 0.0)
? FullscreenAnimatorStyle::ENTER_FULLSCREEN
: FullscreenAnimatorStyle::EXIT_FULLSCREEN;
case FullscreenModelScrollDirection::kDown:
return FullscreenAnimatorStyle::ENTER_FULLSCREEN;
}
}
void FullscreenMediator::RecordFullscreenExitMode() {
CHECK(fullscreen_exit_reason_.has_value());
FullscreenModeTransitionReason reason =
ConvertFullscreenExitReasonToTransitionReason(
fullscreen_exit_reason_.value());
base::UmaHistogramEnumeration(kExitFullscreenModeTransitionReasonHistogram,
reason);
}
FullscreenModeTransitionReason
FullscreenMediator::ConvertFullscreenExitReasonToTransitionReason(
FullscreenExitReason exit_reason) {
switch (exit_reason) {
case FullscreenExitReason::kUserTapped:
return FullscreenModeTransitionReason::kUserTapped;
case FullscreenExitReason::kBottomReached:
return FullscreenModeTransitionReason::kBottomReached;
case FullscreenExitReason::kForcedByCode:
return FullscreenModeTransitionReason::kForcedByCode;
case FullscreenExitReason::kUserInitiatedFinishedByCode:
return FullscreenModeTransitionReason::kUserInitiatedFinishedByCode;
case FullscreenExitReason::kNoChange:
return FullscreenModeTransitionReason::kNoChange;
case FullscreenExitReason::kUserControlled:
NOTREACHED();
}
}