blob: e2eae0ceeadba799b2970f7b003301db27245f56 [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/ui/fullscreen_controller.h"
#include <cmath>
#include "base/logging.h"
#import "ios/chrome/browser/ui/browser_view_controller.h"
#import "ios/chrome/browser/ui/overscroll_actions/overscroll_actions_controller.h"
#import "ios/chrome/browser/ui/tabs/tab_strip_controller.h"
#import "ios/chrome/browser/ui/toolbar/toolbar_controller.h"
#import "ios/chrome/browser/ui/toolbar/web_toolbar_controller.h"
#import "ios/chrome/browser/ui/voice/voice_search_notification_names.h"
#include "ios/web/public/navigation_item.h"
#import "ios/web/public/navigation_manager.h"
#include "ios/web/public/ssl_status.h"
#import "ios/web/public/web_state/ui/crw_web_view_proxy.h"
#import "ios/web/web_state/ui/crw_web_controller.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
NSString* const kSetupForTestingWillCloseAllTabsNotification =
@"kSetupForTestingWillCloseAllTabsNotification";
using web::NavigationManager;
namespace {
class ScopedIncrementer {
public:
explicit ScopedIncrementer(int* value) : value_(value) { ++(*value_); }
~ScopedIncrementer() { --(*value_); }
private:
int* value_;
};
CGFloat kPrecision = 0.00001;
// Duration for the delay before showing the omnibox.
const double kShowOmniboxDelaySeconds = 0.5;
// Indicates if the FullScreenController returns nil from |init|. Used for
// testing purposes.
BOOL gEnabledForTests = YES;
// Compares that two CGFloat a and b are within a range of kPrecision of each
// other.
BOOL CGFloatEquals(CGFloat a, CGFloat b) {
CGFloat delta = std::abs(a - b);
return delta < kPrecision;
}
} // anonymous namespace.
@interface FullScreenController ()<UIGestureRecognizerDelegate> {
// Used to detect movement in the scrollview produced by this class.
int selfTriggered_;
// Used to detect if the keyboard is visible.
BOOL keyboardIsVisible_;
// Used to detect that the OverscrollActionsController is displaying its UI.
// The FullScreenController is disabled when the OverscrollActionsController's
// UI is displayed.
BOOL overscrollActionsInProgress_;
// Counter used to keep track of the number of actions currently disabling
// full screen.
uint fullScreenLock_;
// CRWWebViewProxy object allows web view manipulations.
id<CRWWebViewProxy> webViewProxy_;
}
// Access to the UIWebView's UIScrollView.
@property(weak, nonatomic, readonly) CRWWebViewScrollViewProxy* scrollViewProxy;
// The navigation controller of the page.
@property(nonatomic, readonly, assign) NavigationManager* navigationManager;
// The gesture recognizer set on the scrollview to detect tap. Must be readwrite
// for property releaser to work.
@property(nonatomic, readwrite, strong)
UITapGestureRecognizer* userInteractionGestureRecognizer;
// The delegate responsible for providing the header height and moving the
// header.
@property(weak, nonatomic, readonly) id<FullScreenControllerDelegate> delegate;
// Current height of the header, in points. This is a pass-through method that
// fetches the header height from the FullScreenControllerDelegate.
@property(nonatomic, readonly) CGFloat headerHeight;
// |top| field of UIScrollView.contentInset value caused by header.
// Always 0 for WKWebView, as it does not support contentInset.
@property(nonatomic, readonly) CGFloat topContentInsetCausedByHeader;
// Last known y offset of the content in the scroll view during a scroll. Used
// to infer the direction of the current scroll.
@property(nonatomic, assign) CGFloat previousContentOffset;
// Last known y offset requested on the scroll view. In general the same value
// as previous content offset unless the offset was corrected by the controller
// to slide from under the toolbar.
@property(nonatomic, assign) CGFloat previousRequestedContentOffset;
// Whether or not the content of the scroll view fits entirely on screen when
// the toolbar is visible.
@property(nonatomic, readonly) BOOL contentFitsWithToolbarVisible;
// During a drag operation stores and remember the length of the latest scroll
// down operation. If a scroll up move happens later during the same gesture
// this will be used to delay the apparition of the header.
@property(nonatomic, assign) CGFloat lastScrollDownDistance;
// Tracks whether the current scrollview movements are triggered by the user or
// programmatically.
@property(nonatomic, assign) BOOL isUserTriggered;
// Tracks if fullscreen is currently disabled because of page load.
@property(nonatomic, assign) BOOL isFullScreenDisabledForLoading;
// Tracks if fullscreen is currently disabled because of unsecured page.
@property(nonatomic, readonly, assign) BOOL isFullScreenDisabledForSSLStatus;
// Tracks if fullscreen is currently disabled.
@property(nonatomic, readonly, assign) BOOL isFullScreenDisabled;
// Tracks if fullscreen is temporarily disabled for the current page.
@property(nonatomic, readonly, assign) BOOL isFullScreenDisabledTemporarily;
// Tracks if fullscreen is permanently disabled for the current page.
@property(nonatomic, readonly, assign) BOOL isFullScreenDisabledPermanently;
// Skip next attempt to correct the scroll offset for the toolbar height. This
// is necessary when programatically scrolling down the y offset.
@property(nonatomic, assign) BOOL skipNextScrollOffsetForHeader;
// Incremented each time a timed request to remove the header is sent,
// decremented when the timer fires. When it reach zero, the header is moved.
@property(nonatomic, assign) unsigned int delayedHideHeaderCount;
// ID of the session (each Tab represents a session).
@property(nonatomic, copy) NSString* sessionID;
// Returns if the given entry will be displayed with an error padlock. If this
// is the case, the toolbar should never be hidden on this entry.
- (BOOL)isEntryBrokenSSL:(web::NavigationItem*)item;
// Called at the start of a user scroll.
- (void)webViewScrollViewWillStartScrolling:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy;
// Called at the end of a scroll.
- (void)webViewScrollViewDidStopScrolling:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy;
// Called before and after the keyboard is appearing. Used to allow scroll
// events triggered by the keyboard appearing to go through.
- (void)keyboardStart:(NSNotification*)notification;
- (void)keyboardEnd:(NSNotification*)notification;
// Called before and after an action that disables full screen. The version
// resetting the timer will ensure that the header stay on screen for a little
// while.
- (void)incrementFullScreenLock;
- (void)decrementFullScreenLock;
// Called when the application is about to be the foreground application.
- (void)applicationWillEnterForeground:(NSNotification*)notification;
// Called from -webViewScrollViewDidScroll: Returns YES if the scroll should be
// ignored.
- (BOOL)shouldIgnoreScroll:(CRWWebViewScrollViewProxy*)webViewScrollViewProxy;
// Processes a scroll event triggered by a user action.
- (void)userTriggeredWebViewScrollViewDidScroll:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy;
// Processes a scroll event triggered by code (these could be initiated via
// Javascript, find in page or simply the keyboard sliding in and out).
- (void)codeTriggeredWebViewScrollViewDidScroll:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy;
// Returns YES if |scrollView_| is for the current tab.
- (BOOL)isScrollViewForCurrentTab;
// Shows the header. The header is hidden after kHideOmniboxDelaySeconds if the
// page requested fullscreen explicitly.
- (void)triggerHeader;
// Sets top inset to content view, and updates scroll view content offset to
// counteract the change in the content's view frame.
- (void)setContentViewTopContentPadding:(CGFloat)newTopInset;
// Hide the header if it is possible to do so.
- (void)hideHeaderIfPossible;
// Shows or hides the header as directed by |visible|. If necessary the delegate
// will be called synchronously with the desired offset and |animate| value.
// This method can be called when it is desirable to show or hide the header
// programmatically. It must be called when the header size changes.
- (void)moveHeaderToRestingPosition:(BOOL)visible animate:(BOOL)animate;
@end
@implementation FullScreenController
@synthesize delegate = delegate_;
@synthesize navigationManager = navigationManager_;
@synthesize previousContentOffset = previousContentOffset_;
@synthesize previousRequestedContentOffset = previousRequestedContentOffset_;
@synthesize lastScrollDownDistance = lastScrollDownDistance_;
@synthesize immediateDragDown = immediateDragDown_;
@synthesize isUserTriggered = userTriggered_;
@synthesize isFullScreenDisabledForLoading = isFullScreenDisabledForLoading_;
@synthesize skipNextScrollOffsetForHeader = skipNextScrollOffsetForHeader_;
@synthesize delayedHideHeaderCount = delayedHideHeaderCount_;
@synthesize sessionID = sessionID_;
@synthesize userInteractionGestureRecognizer =
userInteractionGestureRecognizer_;
- (id)initWithDelegate:(id<FullScreenControllerDelegate>)delegate
navigationManager:(NavigationManager*)navigationManager
sessionID:(NSString*)sessionID {
if (!gEnabledForTests)
return nil;
if ((self = [super init])) {
DCHECK(sessionID);
DCHECK(delegate);
delegate_ = delegate;
sessionID_ = [sessionID copy];
navigationManager_ = navigationManager;
NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
[center addObserver:self
selector:@selector(keyboardStart:)
name:UIKeyboardWillShowNotification
object:nil];
[center addObserver:self
selector:@selector(keyboardEnd:)
name:UIKeyboardWillHideNotification
object:nil];
[center addObserver:self
selector:@selector(incrementFullScreenLock)
name:kMenuWillShowNotification
object:nil];
[center addObserver:self
selector:@selector(decrementFullScreenLock)
name:kMenuWillHideNotification
object:nil];
[center addObserver:self
selector:@selector(triggerHeader)
name:kWillStartTabStripTabAnimation
object:nil];
[center addObserver:self
selector:@selector(incrementFullScreenLock)
name:kTabHistoryPopupWillShowNotification
object:nil];
[center addObserver:self
selector:@selector(decrementFullScreenLock)
name:kTabHistoryPopupWillHideNotification
object:nil];
[center addObserver:self
selector:@selector(incrementFullScreenLock)
name:kVoiceSearchWillShowNotification
object:nil];
[center addObserver:self
selector:@selector(decrementFullScreenLock)
name:kVoiceSearchWillHideNotification
object:nil];
[center addObserver:self
selector:@selector(incrementFullScreenLock)
name:kVoiceSearchBarViewButtonSelectedNotification
object:nil];
[center addObserver:self
selector:@selector(decrementFullScreenLock)
name:kVoiceSearchBarViewButtonDeselectedNotification
object:nil];
[center addObserver:self
selector:@selector(applicationWillEnterForeground:)
name:UIApplicationWillEnterForegroundNotification
object:nil];
[center addObserver:self
selector:@selector(triggerHeader)
name:kSetupForTestingWillCloseAllTabsNotification
object:nil];
[center addObserver:self
selector:@selector(incrementFullScreenLock)
name:ios_internal::kPageInfoWillShowNotification
object:nil];
[center addObserver:self
selector:@selector(decrementFullScreenLock)
name:ios_internal::kPageInfoWillHideNotification
object:nil];
[center
addObserver:self
selector:@selector(incrementFullScreenLock)
name:ios_internal::kLocationBarBecomesFirstResponderNotification
object:nil];
[center
addObserver:self
selector:@selector(decrementFullScreenLock)
name:ios_internal::kLocationBarResignsFirstResponderNotification
object:nil];
[center addObserver:self
selector:@selector(incrementFullScreenLock)
name:kTabStripDragStarted
object:nil];
[center addObserver:self
selector:@selector(decrementFullScreenLock)
name:kTabStripDragEnded
object:nil];
[center addObserver:self
selector:@selector(incrementFullScreenLock)
name:ios_internal::kSideSwipeWillStartNotification
object:nil];
[center addObserver:self
selector:@selector(decrementFullScreenLock)
name:ios_internal::kSideSwipeDidStopNotification
object:nil];
// TODO(crbug.com/451373): Evaluate using listeners instead of
// notifications.
[center addObserver:self
selector:@selector(overscrollActionsWillStart)
name:kOverscrollActionsWillStart
object:nil];
[center addObserver:self
selector:@selector(overscrollActionsDidEnd)
name:kOverscrollActionsDidEnd
object:nil];
[self moveHeaderToRestingPosition:YES];
}
return self;
}
- (void)invalidate {
delegate_ = nil;
navigationManager_ = NULL;
[self.scrollViewProxy removeObserver:self];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (CRWWebViewScrollViewProxy*)scrollViewProxy {
return [webViewProxy_ scrollViewProxy];
}
- (CGFloat)headerHeight {
return [self.delegate headerHeight];
}
- (CGFloat)topContentInsetCausedByHeader {
if ([webViewProxy_ shouldUseInsetForTopPadding]) {
// If the web view's |shouldUseInsetForTopPadding| is YES, fullscreen
// header insets the content by modifying content inset.
return self.headerHeight;
}
return 0.0f;
}
- (void)moveHeaderToRestingPosition:(BOOL)visible {
[self moveHeaderToRestingPosition:visible animate:YES];
}
- (void)moveHeaderToRestingPosition:(BOOL)visible animate:(BOOL)animate {
// If there is no delegate there is no need to do anything as the headerHeight
// cannot be obtained.
if (!self.delegate)
return;
DCHECK(visible || !self.isFullScreenDisabled);
// The desired final position of the header.
CGFloat headerPosition = visible ? 0.0 : self.headerHeight;
// Check if there is anything to do.
CGFloat delta = self.delegate.currentHeaderOffset - headerPosition;
if (CGFloatEquals(delta, 0.0))
return;
// Do not further act on scrollview changes.
ScopedIncrementer stack(&(self->selfTriggered_));
// If the scrollview is not the current scrollview, don't update the UI.
if (![self isScrollViewForCurrentTab])
return;
if (self.scrollViewProxy.contentOffset.y < 0.0 && delta < 0.0) {
// If the delta is negative this means the header must be hidden more. Check
// if the scrollview extents to the right place, there may be a need to
// scroll it up.
[self.delegate fullScreenController:self
drawHeaderViewFromOffset:headerPosition
onWebViewProxy:webViewProxy_
changeTopContentPadding:NO
scrollingToOffset:0.0f];
} else {
if (!visible && ![webViewProxy_ shouldUseInsetForTopPadding]) {
// The header will be hidden, so if the content view is not using the
// content inset, it is necessary to decrease the top padding, so more
// content is visible to the user.
CGFloat newTopContentPadding = self.headerHeight - headerPosition;
CGFloat topContentPaddingChange =
[webViewProxy_ topContentPadding] - newTopContentPadding;
if (topContentPaddingChange <= self.scrollViewProxy.contentOffset.y) {
// Padding can be decreased immediately and without animation as there
// is enough content present behind the header.
[self setContentViewTopContentPadding:newTopContentPadding];
} else {
// Header is taller that amount of hidden content, hence animated hide
// is required.
[self.delegate fullScreenController:self
drawHeaderViewFromOffset:headerPosition
onWebViewProxy:webViewProxy_
changeTopContentPadding:YES
scrollingToOffset:0.0f];
return;
}
}
// Only move the header, the content doesn't need to move.
[self.delegate fullScreenController:self
drawHeaderViewFromOffset:headerPosition
animate:animate];
}
}
- (void)disableFullScreen {
[self moveHeaderToRestingPosition:YES];
self.isFullScreenDisabledForLoading = YES;
}
- (void)enableFullScreen {
self.isFullScreenDisabledForLoading = NO;
}
- (void)shouldSkipNextScrollOffsetForHeader {
self.skipNextScrollOffsetForHeader = YES;
}
- (void)moveContentBelowHeader {
DCHECK(delegate_);
DCHECK(webViewProxy_);
[self moveHeaderToRestingPosition:YES animate:NO];
CGPoint contentOffset = self.scrollViewProxy.contentOffset;
contentOffset.y = 0;
self.scrollViewProxy.contentOffset = contentOffset;
}
#pragma mark - private methods
- (BOOL)isEntryBrokenSSL:(web::NavigationItem*)item {
if (!item)
return NO;
// Only BROKEN results in an error (vs. a warning); see toolbar_model_impl.cc.
// TODO(qsr): Find a way to share this logic with the omnibox.
const web::SSLStatus& ssl = item->GetSSL();
switch (ssl.security_style) {
case web::SECURITY_STYLE_UNKNOWN:
case web::SECURITY_STYLE_UNAUTHENTICATED:
case web::SECURITY_STYLE_AUTHENTICATED:
return NO;
case web::SECURITY_STYLE_AUTHENTICATION_BROKEN:
return YES;
default:
NOTREACHED();
return YES;
}
}
- (BOOL)isFullScreenDisabled {
return self.isFullScreenDisabledTemporarily ||
self.isFullScreenDisabledPermanently;
}
- (BOOL)isFullScreenDisabledTemporarily {
return fullScreenLock_ > 0 || self.isFullScreenDisabledForLoading;
}
- (BOOL)isFullScreenDisabledForSSLStatus {
return self.navigationManager &&
[self isEntryBrokenSSL:self.navigationManager->GetVisibleItem()];
}
- (BOOL)isFullScreenDisabledPermanently {
return UIAccessibilityIsVoiceOverRunning() ||
self.isFullScreenDisabledForSSLStatus ||
CGRectIsEmpty(self.scrollViewProxy.frame);
}
- (void)hideHeaderIfPossible {
// Covers a number of conditions, like a menu being up.
if (self.isFullScreenDisabled)
return;
// Another FullScreenController is in control.
if (![self isScrollViewForCurrentTab])
return;
// No autohide if the content needs to move.
if (self.scrollViewProxy.contentOffset.y < 0.0)
return;
// It is quite safe to move the toolbar away.
[self moveHeaderToRestingPosition:NO];
}
- (void)incrementFullScreenLock {
// This method may be called late enough that it is unsafe to access the
// delegate.
fullScreenLock_++;
}
- (void)decrementFullScreenLock {
// The corresponding notification for incrementing the lock may have been
// posted before the FullScreenController was initialized. This can occur
// when entering a URL or search query from the NTP since the CRWWebController
// begins loading the page before the keyboard is dismissed.
if (fullScreenLock_ > 0)
fullScreenLock_--;
}
- (void)keyboardStart:(NSNotification*)notification {
if (!keyboardIsVisible_) {
keyboardIsVisible_ = YES;
[self incrementFullScreenLock];
}
[self moveHeaderToRestingPosition:YES];
}
- (void)keyboardEnd:(NSNotification*)notification {
if (keyboardIsVisible_) {
keyboardIsVisible_ = NO;
[self decrementFullScreenLock];
}
}
- (void)applicationWillEnterForeground:(NSNotification*)notification {
if (!self.isFullScreenDisabled && [self isScrollViewForCurrentTab]) {
dispatch_time_t popTime = dispatch_time(
DISPATCH_TIME_NOW, (int64_t)(kShowOmniboxDelaySeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^{
[self triggerHeader];
});
}
}
- (void)webViewScrollViewWillStartScrolling:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
self.isUserTriggered = YES;
self.lastScrollDownDistance = 0.0;
}
- (void)webViewScrollViewDidStopScrolling:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
self.isUserTriggered = NO;
// If an overscroll action is in progress, it means the header is already
// shown, trying to reset its position would interfere with the
// OverscrollActionsController.
if (!overscrollActionsInProgress_) {
CGFloat threshold = self.headerHeight / 2.0;
BOOL visible = self.delegate.currentHeaderOffset < threshold ||
self.isFullScreenDisabled;
[self moveHeaderToRestingPosition:visible];
}
}
- (BOOL)shouldIgnoreScroll:(CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
if (overscrollActionsInProgress_)
return YES;
if (![self isScrollViewForCurrentTab])
return YES;
BOOL shouldIgnore = selfTriggered_ || webViewScrollViewProxy.isZooming ||
self.headerHeight == 0.0 || !self.delegate;
if (self.isUserTriggered)
return shouldIgnore;
// Ignore simple realignment moves by 1 one pixel on retina display, called
// sometimes at the end of an animation.
CGFloat moveMagnitude = std::abs(self.previousContentOffset -
webViewScrollViewProxy.contentOffset.y);
shouldIgnore = shouldIgnore || moveMagnitude <= 0.5;
// Never let the background show. The keyboard may sometimes center the
// input fields in such a way that the inset of the scrollview is showing.
// In those cases the header must be popped up unconditionally.
CGFloat headerOffset = self.headerHeight - self.delegate.currentHeaderOffset;
if (webViewScrollViewProxy.contentOffset.y + headerOffset < 0.0)
shouldIgnore = NO;
return shouldIgnore;
}
- (BOOL)contentFitsWithToolbarVisible {
CGFloat viewportHeight = CGRectGetHeight(self.scrollViewProxy.frame) -
self.topContentInsetCausedByHeader;
return self.scrollViewProxy.contentSize.height <= viewportHeight;
}
- (void)userTriggeredWebViewScrollViewDidScroll:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
// Calculate the relative move compared to the last checked position: positive
// values are scroll up, negative are scroll down.
CGFloat verticalDelta =
webViewScrollViewProxy.contentOffset.y - self.previousContentOffset;
// Scroll view is scrolled all the way to the top. Ignore the bouce up.
BOOL isContentAtTop = webViewScrollViewProxy.contentOffset.y <=
-self.topContentInsetCausedByHeader;
BOOL ignoreScrollAtContentTop = isContentAtTop && (0.0f < verticalDelta);
// Scroll view is scrolled all the way to the bottom. Ignore the bounce down.
// Also ignore the scroll up if the page is visible with the toolbar on-screen
// as the toolbar should not be hidden in that case.
BOOL ignoreScrollAtContentBottom =
(webViewScrollViewProxy.contentOffset.y +
webViewScrollViewProxy.frame.size.height >=
webViewScrollViewProxy.contentSize.height) &&
(verticalDelta < 0.0 || [self contentFitsWithToolbarVisible]);
if (ignoreScrollAtContentTop || ignoreScrollAtContentBottom)
verticalDelta = 0.0;
if (!self.immediateDragDown) {
// Accumulate or reset the lastScrollDownDistance. Scrolling up consumes
// twice as fast as scrolling down accumulates.
if (verticalDelta > 0.0)
self.lastScrollDownDistance += verticalDelta;
else
self.lastScrollDownDistance += verticalDelta * 2.0;
if (self.lastScrollDownDistance < 0.0)
self.lastScrollDownDistance = 0.0;
}
// Changes the header offset and informs the delegate to perform the move.
CGFloat newHeaderOffset = self.delegate.currentHeaderOffset;
if (verticalDelta > 0.0 || webViewScrollViewProxy.contentOffset.y <= 0.0 ||
self.lastScrollDownDistance <= 0.0) {
newHeaderOffset += verticalDelta;
}
if (newHeaderOffset < 0.0)
newHeaderOffset = 0.0;
else if (newHeaderOffset > self.headerHeight)
newHeaderOffset = self.headerHeight;
[self.delegate fullScreenController:self
drawHeaderViewFromOffset:newHeaderOffset
animate:NO];
}
- (void)codeTriggeredWebViewScrollViewDidScroll:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
if (webViewScrollViewProxy.contentOffset.y >= 0.0 && !keyboardIsVisible_)
return;
BOOL isFullyVisible = CGFloatEquals(self.delegate.currentHeaderOffset, 0.0);
if (keyboardIsVisible_) {
DCHECK(isFullyVisible);
return;
}
CGFloat newOffset;
if ([self contentFitsWithToolbarVisible] && !keyboardIsVisible_) {
// Align the content just below the header if the scroll view's content fits
// entirely on screen when the toolbar visible and if the keyboard is not
// visible.
// Note: The keyboard is visible when the user is editing a text field
// at the bottom of the page and the page is scrolled to make it visible
// for the user. Avoid changing the offset in this case.
newOffset = -self.headerHeight;
} else {
newOffset = webViewScrollViewProxy.contentOffset.y;
// Correct the offset to take into account the fact that the header is
// obscuring the top of the view when scrolling down.
if ((webViewScrollViewProxy.contentOffset.y <=
self.previousRequestedContentOffset ||
keyboardIsVisible_) &&
!self.skipNextScrollOffsetForHeader)
newOffset -= self.headerHeight;
// Make sure the content is not too low.
if (newOffset < -self.headerHeight)
newOffset = -self.headerHeight;
}
if (isFullyVisible) {
// As the header is already visible, just move the scrollview.
webViewScrollViewProxy.contentOffset =
CGPointMake(webViewScrollViewProxy.contentOffset.x, newOffset);
}
}
- (BOOL)isScrollViewForCurrentTab {
return [self.delegate isTabWithIDCurrent:self.sessionID];
}
- (void)triggerHeader {
if (self.isFullScreenDisabled || ![self isScrollViewForCurrentTab])
return;
[self moveHeaderToRestingPosition:YES];
}
- (void)setContentViewTopContentPadding:(CGFloat)newTopPadding {
[webViewProxy_ setTopContentPadding:newTopPadding];
}
- (void)setToolbarInsetsForHeaderOffset:(CGFloat)headerOffset {
// Make space for the header in the scroll view.
CGFloat topInset = self.headerHeight - headerOffset;
UIEdgeInsets insets = self.scrollViewProxy.contentInset;
insets.top = topInset;
[self setContentViewTopContentPadding:topInset];
}
#pragma mark -
#pragma mark CRWWebControllerObserver methods
- (void)setWebViewProxy:(id<CRWWebViewProxy>)webViewProxy
controller:(CRWWebController*)webController {
DCHECK([webViewProxy scrollViewProxy]);
webViewProxy_ = webViewProxy;
[[webViewProxy scrollViewProxy] addObserver:self];
}
- (void)pageLoaded:(CRWWebController*)webController {
[self enableFullScreen];
web::WebState* webState = webController.webState;
if (webState) {
BOOL MIMETypeIsPDF = webState->GetContentsMimeType() == "application/pdf";
[webViewProxy_ setShouldUseInsetForTopPadding:MIMETypeIsPDF];
}
}
#pragma mark -
#pragma mark CRWWebViewScrollViewObserver
- (void)webViewScrollViewDidScroll:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
CGFloat previousRequestedContentOffset =
webViewScrollViewProxy.contentOffset.y;
if ([self shouldIgnoreScroll:webViewScrollViewProxy]) {
// Do not act on those events, just record the eventual move.
self.previousContentOffset = previousRequestedContentOffset;
self.previousRequestedContentOffset = previousRequestedContentOffset;
return;
}
// Ignore any scroll moves called recursively.
ScopedIncrementer stack(&(self->selfTriggered_));
if (self.isUserTriggered) {
if (!self.isFullScreenDisabled)
[self userTriggeredWebViewScrollViewDidScroll:webViewScrollViewProxy];
} else {
[self codeTriggeredWebViewScrollViewDidScroll:webViewScrollViewProxy];
}
self.previousContentOffset = webViewScrollViewProxy.contentOffset.y;
self.previousRequestedContentOffset = previousRequestedContentOffset;
}
- (void)webViewScrollViewWillBeginDragging:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
[self webViewScrollViewWillStartScrolling:webViewScrollViewProxy];
}
- (void)webViewScrollViewDidEndDragging:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy
willDecelerate:(BOOL)decelerate {
DCHECK(self.delegate);
if (!decelerate)
[self webViewScrollViewDidStopScrolling:webViewScrollViewProxy];
}
- (void)webViewScrollViewDidEndScrollingAnimation:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
self.skipNextScrollOffsetForHeader = NO;
}
- (void)webViewScrollViewDidEndDecelerating:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
DCHECK(self.delegate);
[self webViewScrollViewDidStopScrolling:webViewScrollViewProxy];
}
- (BOOL)webViewScrollViewShouldScrollToTop:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
if (webViewScrollViewProxy.contentInset.top != self.headerHeight) {
// Move the toolbar first so the origin of the page moves down.
[self moveHeaderToRestingPosition:YES];
}
return YES;
}
#pragma mark -
#pragma mark CRWWebViewScrollViewProxyObserver
- (void)webViewScrollViewProxyDidSetScrollView:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
webViewScrollViewProxy.contentOffset = CGPointMake(0.0, -self.headerHeight);
[self setToolbarInsetsForHeaderOffset:0.0];
}
#pragma mark - UIGestureRecognizerDelegate
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer*)gestureRecognizer {
return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:
(UIGestureRecognizer*)otherGestureRecognizer {
// This is necessary for the gesture recognizer to receive all the touches.
// If the default value of NO is returned the default recognizers on the
// webview do take precedence.
return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
shouldReceiveTouch:(UITouch*)touch {
return YES;
}
#pragma mark - Overscroll actions notifications handling
- (void)overscrollActionsWillStart {
[self incrementFullScreenLock];
overscrollActionsInProgress_ = YES;
}
- (void)overscrollActionsDidEnd {
[self decrementFullScreenLock];
overscrollActionsInProgress_ = NO;
}
#pragma mark - Used for testing
+ (void)setEnabledForTests:(BOOL)enabled {
gEnabledForTests = enabled;
}
@end