| Index: ios/chrome/browser/ui/fullscreen_controller.mm
|
| diff --git a/ios/chrome/browser/ui/fullscreen_controller.mm b/ios/chrome/browser/ui/fullscreen_controller.mm
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..0f3632e09dfa84c896ac0a0942617d44040d6508
|
| --- /dev/null
|
| +++ b/ios/chrome/browser/ui/fullscreen_controller.mm
|
| @@ -0,0 +1,841 @@
|
| +// 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"
|
| +#include "base/mac/objc_property_releaser.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/crw_web_view_proxy.h"
|
| +#import "ios/web/web_state/ui/crw_web_controller.h"
|
| +
|
| +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;
|
| +// Default duration for the delay before hiding the omnibox.
|
| +const double kDefaultHideOmniboxDelaySeconds = 3.0;
|
| +// Duration for the delay before hiding the omnibox.
|
| +double gHideOmniboxDelaySeconds = kDefaultHideOmniboxDelaySeconds;
|
| +// 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.
|
| + base::scoped_nsprotocol<id<CRWWebViewProxy>> webViewProxy_;
|
| + base::mac::ObjCPropertyReleaser propertyReleaser_FullScreenController_;
|
| +}
|
| +
|
| +// Access to the UIWebView's UIScrollView.
|
| +@property(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, retain)
|
| + UITapGestureRecognizer* userInteractionGestureRecognizer;
|
| +// The delegate responsible for providing the header height and moving the
|
| +// header.
|
| +@property(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;
|
| +// TODO(shreyasv): Make the following methods act on a WebViewScrollView proxy
|
| +// instead of taking in a UIScrollView directly.
|
| +// 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) {
|
| + propertyReleaser_FullScreenController_.Init(self,
|
| + [FullScreenController class]);
|
| + [self release];
|
| + return nil;
|
| + }
|
| + if ((self = [super init])) {
|
| + propertyReleaser_FullScreenController_.Init(self,
|
| + [FullScreenController class]);
|
| + 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(jbbegue): Evaluate using a listener instead of a notification
|
| + // crbug/451373.
|
| + [center addObserver:self
|
| + selector:@selector(overscrollActionsWillStart)
|
| + name:ios_internal::kOverscollActionsWillStart
|
| + object:nil];
|
| + [center addObserver:self
|
| + selector:@selector(overscrollActionsDidEnd)
|
| + name:ios_internal::kOverscollActionsDidEnd
|
| + 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];
|
| + [super dealloc];
|
| +}
|
| +
|
| +- (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
|
| + forceUpdate:(BOOL)forceUpdate {
|
| + // 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_.reset([webViewProxy retain]);
|
| + [[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];
|
| + }
|
| +}
|
| +
|
| +- (void)webControllerWillClose:(CRWWebController*)webController {
|
| + [webController removeObserver:self];
|
| +}
|
| +
|
| +#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 forceUpdate:YES];
|
| +}
|
| +
|
| +#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)setHideOmniboxDelaySeconds:(double)hideOmniboxDelaySeconds {
|
| + gHideOmniboxDelaySeconds = hideOmniboxDelaySeconds;
|
| +}
|
| +
|
| ++ (void)resetHideOmniboxDelaySeconds {
|
| + gHideOmniboxDelaySeconds = kDefaultHideOmniboxDelaySeconds;
|
| +}
|
| +
|
| ++ (void)setEnabledForTests:(BOOL)enabled {
|
| + gEnabledForTests = enabled;
|
| +}
|
| +
|
| +@end
|
|
|