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 |