| Index: ios/chrome/browser/ui/overscroll_actions/overscroll_actions_controller.mm
|
| diff --git a/ios/chrome/browser/ui/overscroll_actions/overscroll_actions_controller.mm b/ios/chrome/browser/ui/overscroll_actions/overscroll_actions_controller.mm
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..a3f360ffaf8611c7886aa0dbae8b8679c1dcf8e0
|
| --- /dev/null
|
| +++ b/ios/chrome/browser/ui/overscroll_actions/overscroll_actions_controller.mm
|
| @@ -0,0 +1,881 @@
|
| +// Copyright 2015 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/overscroll_actions/overscroll_actions_controller.h"
|
| +
|
| +#import <QuartzCore/QuartzCore.h>
|
| +
|
| +#include <algorithm>
|
| +#include "base/logging.h"
|
| +#include "base/mac/objc_property_releaser.h"
|
| +#include "base/mac/scoped_nsobject.h"
|
| +#include "base/metrics/histogram.h"
|
| +#import "ios/chrome/browser/ui/browser_view_controller.h"
|
| +#import "ios/chrome/browser/ui/overscroll_actions/overscroll_actions_view.h"
|
| +#include "ios/chrome/browser/ui/rtl_geometry.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"
|
| +#import "ios/web/public/web_state/crw_web_view_proxy.h"
|
| +
|
| +namespace {
|
| +// This enum is used to record the overscroll actions performed by the user on
|
| +// the histogram named |OverscrollActions|.
|
| +enum {
|
| + // Records each time the user selects the new tab action.
|
| + OVERSCROLL_ACTION_NEW_TAB,
|
| + // Records each time the user selects the refresh action.
|
| + OVERSCROLL_ACTION_REFRESH,
|
| + // Records each time the user selects the close tab action.
|
| + OVERSCROLL_ACTION_CLOSE_TAB,
|
| + // Records each time the user cancels the overscroll action.
|
| + OVERSCROLL_ACTION_CANCELED,
|
| + // NOTE: Add new actions in sources only immediately above this line.
|
| + // Also, make sure the enum list for histogram |OverscrollActions| in
|
| + // tools/histogram/histograms.xml is updated with any change in here.
|
| + OVERSCROLL_ACTION_COUNT
|
| +};
|
| +
|
| +// The histogram used to record user actions.
|
| +const char kOverscrollActionsHistogram[] = "Tab.PullDownGesture";
|
| +
|
| +// The pulling threshold in point at which the controller will start accepting
|
| +// actions.
|
| +// Past this pulling value the scrollView will start to resist from pulling.
|
| +const CGFloat kHeaderMaxExpansionThreshold = 56.0;
|
| +// The default overall distance in point to select different actions
|
| +// horizontally.
|
| +const CGFloat kHorizontalPanDistance = 400.0;
|
| +// The distance from the top content offset which will be used to detect
|
| +// if the scrollview is scrolled to top.
|
| +const CGFloat kScrolledToTopToleranceInPoint = 50;
|
| +// The minimum duration between scrolling in order to allow overscroll actions.
|
| +// In seconds.
|
| +const CFTimeInterval kMinimumDurationBetweenScrollingInSeconds = 0.15;
|
| +// The minimum duration that the pull must last in order to trigger an action.
|
| +// In seconds.
|
| +const CFTimeInterval kMinimumPullDurationToTriggerActionInSeconds = 0.2;
|
| +// Bounce dynamic constants.
|
| +// Since the bounce effect of the scrollview is cancelled by setting the
|
| +// contentInsets to the value of the overscroll contentOffset, the bounce
|
| +// bounce back have to be emulated manually using a spring simulation.
|
| +const CGFloat kSpringTightness = 2;
|
| +const CGFloat kSpringDampiness = 0.5;
|
| +
|
| +// This holds the current state of the bounce back animation.
|
| +typedef struct {
|
| + CGFloat yInset;
|
| + CGFloat initialYInset;
|
| + CGFloat headerInset;
|
| + CGFloat velocityInset;
|
| + CFAbsoluteTime time;
|
| +} SpringInsetState;
|
| +
|
| +// Used to set the height of a view frame.
|
| +// Implicit animations are disabled when setting the new frame.
|
| +void SetViewFrameHeight(UIView* view, CGFloat height) {
|
| + [CATransaction begin];
|
| + [CATransaction setDisableActions:YES];
|
| + CGRect viewFrame = view.frame;
|
| + viewFrame.size.height = height;
|
| + view.frame = viewFrame;
|
| + [CATransaction commit];
|
| +}
|
| +
|
| +// Clamp a value between min and max.
|
| +CGFloat Clamp(CGFloat value, CGFloat min, CGFloat max) {
|
| + DCHECK(min < max);
|
| + if (value < min)
|
| + return min;
|
| + if (value > max)
|
| + return max;
|
| + return value;
|
| +}
|
| +} // namespace
|
| +
|
| +namespace ios_internal {
|
| +NSString* const kOverscollActionsWillStart = @"OverscollActionsWillStart";
|
| +NSString* const kOverscollActionsDidEnd = @"OverscollActionsDidStop";
|
| +} // namespace ios_internal
|
| +
|
| +// This protocol describes the subset of methods used between the
|
| +// CRWWebViewScrollViewProxy and the UIWebView.
|
| +@protocol OverscrollActionsScrollView<NSObject>
|
| +
|
| +@property(nonatomic, assign) UIEdgeInsets contentInset;
|
| +@property(nonatomic, assign) CGPoint contentOffset;
|
| +@property(nonatomic, assign) UIEdgeInsets scrollIndicatorInsets;
|
| +@property(nonatomic, readonly) UIPanGestureRecognizer* panGestureRecognizer;
|
| +@property(nonatomic, readonly) BOOL isZooming;
|
| +
|
| +- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated;
|
| +- (void)addGestureRecognizer:(UIGestureRecognizer*)gestureRecognizer;
|
| +- (void)removeGestureRecognizer:(UIGestureRecognizer*)gestureRecognizer;
|
| +
|
| +@end
|
| +
|
| +@interface OverscrollActionsController ()<CRWWebViewScrollViewProxyObserver,
|
| + UIGestureRecognizerDelegate,
|
| + OverscrollActionsViewDelegate> {
|
| + // Display link used to animate the bounce back effect.
|
| + CADisplayLink* _dpLink;
|
| + SpringInsetState _bounceState;
|
| + NSInteger _overscrollActionLock;
|
| + // The last time the user started scrolling the view.
|
| + CFTimeInterval _lastScrollBeginTime;
|
| + // Set to YES when the bounce animation must be independent of the scrollview
|
| + // contentOffset change.
|
| + // This is done when an action has been triggered. In that case the webview's
|
| + // scrollview will change state depending on the action being triggered so
|
| + // relying on the contentInset is not possible at that time.
|
| + BOOL _performingScrollViewIndependentAnimation;
|
| + // Force processing state changes in scrollviewDidScroll: even if
|
| + // overscroll actions are disabled.
|
| + // This is used to always process contentOffset changes on specific cases like
|
| + // when playing the bounce back animation if no actions has been triggered.
|
| + BOOL _forceStateUpdate;
|
| + // True when the overscroll actions are disabled for loading.
|
| + BOOL _isOverscrollActionsDisabledForLoading;
|
| + // True when the pull gesture started close enough from the top and the
|
| + // delegate allows it.
|
| + // Use isOverscrollActionEnabled to take into account locking.
|
| + BOOL _allowPullingActions;
|
| + // Records if a transition to the overscroll state ACTION_READY was made.
|
| + // This is used to record a cancel gesture.
|
| + BOOL _didTransitionToActionReady;
|
| + // Store the set of notifications that did increment the overscroll actions
|
| + // lock. It is used in order to enforce the fact that the lock should only be
|
| + // incremented/decremented once for a given notification.
|
| + base::scoped_nsobject<NSMutableSet> _lockIncrementNotifications;
|
| + // Store the notification name counterpart of another notification name.
|
| + // Overscroll actions locking and unlocking works by listening to balanced
|
| + // notifications. One notification lock and it's counterpart unlock. This
|
| + // dictionary is used to retrieve the notification name from it's notification
|
| + // counterpart name. Exemple:
|
| + // UIKeyboardWillShowNotification trigger a lock. Its counterpart notification
|
| + // name is UIKeyboardWillHideNotification.
|
| + base::scoped_nsobject<NSDictionary> _lockNotificationsCounterparts;
|
| + // A view used to catch touches on the webview.
|
| + base::scoped_nsobject<UIView> _dummyView;
|
| + // The proxy used to interact with the webview.
|
| + base::scoped_nsprotocol<id<CRWWebViewProxy>> _webViewProxy;
|
| + // The proxy used to interact with the webview's scrollview.
|
| + base::scoped_nsobject<CRWWebViewScrollViewProxy> _webViewScrollViewProxy;
|
| + // The scrollview driving the OverscrollActionsController when not using
|
| + // the scrollview from the CRWWebControllerObserver.
|
| + base::scoped_nsobject<UIScrollView> _scrollview;
|
| + base::mac::ObjCPropertyReleaser _propertyReleaser_OverscrollActionsController;
|
| +}
|
| +
|
| +// The view displayed over the header view holding the actions.
|
| +@property(nonatomic, retain) OverscrollActionsView* overscrollActionView;
|
| +// Initial top inset added to the scrollview for the header.
|
| +// This property is set from the delegate headerInset and cached on first
|
| +// call. The cached value is reset when the webview proxy is set.
|
| +@property(nonatomic, readonly) CGFloat initialContentInset;
|
| +// Initial top inset for the header.
|
| +// This property is set from the delegate headerInset and cached on first
|
| +// call. The cached value is reset when the webview proxy is set.
|
| +@property(nonatomic, readonly) CGFloat initialHeaderInset;
|
| +// Initial height of the header view.
|
| +// This property is set from the delegate headerHeight and cached on first
|
| +// call. The cached value is reset when the webview proxy is set.
|
| +@property(nonatomic, readonly) CGFloat initialHeaderHeight;
|
| +// Redefined to be read-write.
|
| +@property(nonatomic, assign, readwrite)
|
| + ios_internal::OverscrollState overscrollState;
|
| +// Point where the horizontal gesture started when the state of the
|
| +// overscroll controller is in OverscrollStateActionReady.
|
| +@property(nonatomic, assign) CGPoint panPointScreenOrigin;
|
| +// Pan gesture recognizer used to track horizontal touches.
|
| +@property(nonatomic, retain) UIPanGestureRecognizer* panGestureRecognizer;
|
| +
|
| +// Registers notifications to lock the overscroll actions on certain UI states.
|
| +- (void)registerNotifications;
|
| +// Setup/tearDown methods are used to register values when the delegate is set.
|
| +- (void)tearDown;
|
| +- (void)setup;
|
| +// Access the headerView from the delegate.
|
| +- (UIView<RelaxedBoundsConstraintsHitTestSupport>*)headerView;
|
| +// Locking/unlocking methods used to disable/enable the overscroll actions
|
| +// with a reference count.
|
| +- (void)incrementOverscrollActionLockForNotification:
|
| + (NSNotification*)notification;
|
| +- (void)decrementOverscrollActionLockForNotification:
|
| + (NSNotification*)notification;
|
| +// Indicates whether the overscroll action is allowed.
|
| +- (BOOL)isOverscrollActionEnabled;
|
| +// Triggers a call to delegate if an action has been triggered.
|
| +- (void)triggerActionIfNeeded;
|
| +// Performs work based on overscroll action state changes.
|
| +- (void)onOverscrollStateChangeWithPreviousState:
|
| + (ios_internal::OverscrollState)previousOverscrollState;
|
| +// Disables all interactions on the webview except pan.
|
| +- (void)setWebViewInteractionEnabled:(BOOL)enabled;
|
| +// Bounce dynamic animations methods.
|
| +// Starts the bounce animation with an initial velocity.
|
| +- (void)startBounceWithInitialVelocity:(CGPoint)velocity;
|
| +// Stops bounce animation.
|
| +- (void)stopBounce;
|
| +// Called from the display link to update the bounce dynamic animation.
|
| +- (void)updateBounce;
|
| +// Applies bounce state to the scroll view.
|
| +- (void)applyBounceState;
|
| +
|
| +@end
|
| +
|
| +@implementation OverscrollActionsController
|
| +
|
| +@synthesize overscrollActionView = _overscrollActionView;
|
| +@synthesize initialHeaderInset = _initialHeaderInset;
|
| +@synthesize initialHeaderHeight = _initialHeaderHeight;
|
| +@synthesize overscrollState = _overscrollState;
|
| +@synthesize delegate = _delegate;
|
| +@synthesize panPointScreenOrigin = _panPointScreenOrigin;
|
| +@synthesize panGestureRecognizer = _panGestureRecognizer;
|
| +
|
| +- (instancetype)init {
|
| + return [self initWithScrollView:nil];
|
| +}
|
| +
|
| +- (instancetype)initWithScrollView:(UIScrollView*)scrollView {
|
| + self = [super init];
|
| + if (self) {
|
| + _propertyReleaser_OverscrollActionsController.Init(
|
| + self, [OverscrollActionsController class]);
|
| + _overscrollActionView =
|
| + [[OverscrollActionsView alloc] initWithFrame:CGRectZero];
|
| + _overscrollActionView.delegate = self;
|
| + _scrollview.reset([scrollView retain]);
|
| + if (_scrollview) {
|
| + [self setup];
|
| + }
|
| + _lockIncrementNotifications.reset([[NSMutableSet alloc] init]);
|
| +
|
| + _lockNotificationsCounterparts.reset([@{
|
| + UIKeyboardWillHideNotification : UIKeyboardWillShowNotification,
|
| + kMenuWillHideNotification : kMenuWillShowNotification,
|
| + kTabHistoryPopupWillHideNotification :
|
| + kTabHistoryPopupWillShowNotification,
|
| + kVoiceSearchWillHideNotification : kVoiceSearchWillShowNotification,
|
| + kVoiceSearchBarViewButtonDeselectedNotification :
|
| + kVoiceSearchBarViewButtonSelectedNotification,
|
| + ios_internal::kPageInfoWillHideNotification :
|
| + ios_internal::kPageInfoWillShowNotification,
|
| + ios_internal::kLocationBarResignsFirstResponderNotification :
|
| + ios_internal::kLocationBarBecomesFirstResponderNotification,
|
| + ios_internal::kSideSwipeDidStopNotification :
|
| + ios_internal::kSideSwipeWillStartNotification
|
| + } retain]);
|
| + [self registerNotifications];
|
| + }
|
| + return self;
|
| +}
|
| +
|
| +- (void)dealloc {
|
| + self.overscrollActionView.delegate = nil;
|
| + [self invalidate];
|
| + [super dealloc];
|
| +}
|
| +
|
| +- (void)invalidate {
|
| + [self clear];
|
| + [self stopBounce];
|
| + [self tearDown];
|
| + [[NSNotificationCenter defaultCenter] removeObserver:self];
|
| + [self setWebViewInteractionEnabled:YES];
|
| + _delegate = nil;
|
| + _webViewProxy.reset();
|
| + [_webViewScrollViewProxy removeObserver:self];
|
| + _webViewScrollViewProxy.reset();
|
| +}
|
| +
|
| +- (void)clear {
|
| + self.overscrollState = ios_internal::OverscrollState::NO_PULL_STARTED;
|
| +}
|
| +
|
| +- (void)enableOverscrollActions {
|
| + _isOverscrollActionsDisabledForLoading = NO;
|
| + [self setup];
|
| +}
|
| +
|
| +- (void)disableOverscrollActions {
|
| + _isOverscrollActionsDisabledForLoading = YES;
|
| + [self tearDown];
|
| +}
|
| +
|
| +- (void)setStyle:(ios_internal::OverscrollStyle)style {
|
| + [self.overscrollActionView setStyle:style];
|
| +}
|
| +
|
| +#pragma mark - webViewScrollView and UIScrollView delegates implementations
|
| +
|
| +- (void)scrollViewDidScroll {
|
| + if (!_forceStateUpdate && (![self isOverscrollActionEnabled] ||
|
| + _performingScrollViewIndependentAnimation))
|
| + return;
|
| +
|
| + const UIEdgeInsets insets =
|
| + UIEdgeInsetsMake(-[self scrollView].contentOffset.y, 0, 0, 0);
|
| + // Start pulling (on top).
|
| + CGFloat contentOffsetFromTheTop = [self scrollView].contentOffset.y;
|
| + if (![_webViewProxy shouldUseInsetForTopPadding]) {
|
| + // Content offset is shifted for WKWebView when the web view's
|
| + // |shouldUseInsetForTopPadding| is NO, to workaround bug with
|
| + // UIScollView.contentInset (rdar://23584409).
|
| + contentOffsetFromTheTop -= [_webViewProxy topContentPadding];
|
| + }
|
| + CGFloat contentOffsetFromExpandedHeader =
|
| + contentOffsetFromTheTop + self.initialHeaderInset;
|
| + if (contentOffsetFromExpandedHeader >= 0) {
|
| + // Record initial content offset and dispatch delegate on state change.
|
| + self.overscrollState = ios_internal::OverscrollState::NO_PULL_STARTED;
|
| + } else {
|
| + if (contentOffsetFromExpandedHeader < -kHeaderMaxExpansionThreshold) {
|
| + self.overscrollState = ios_internal::OverscrollState::ACTION_READY;
|
| + [self scrollView].scrollIndicatorInsets = insets;
|
| + } else {
|
| + // Set the contentInset to remove the bounce that would fight with drag.
|
| + [self setScrollViewContentInset:insets];
|
| + [self scrollView].scrollIndicatorInsets = insets;
|
| + self.overscrollState = ios_internal::OverscrollState::STARTED_PULLING;
|
| + }
|
| + [self updateWithVerticalOffset:-contentOffsetFromExpandedHeader];
|
| + }
|
| +}
|
| +
|
| +- (void)scrollViewWillBeginDragging {
|
| + [self stopBounce];
|
| + _allowPullingActions = NO;
|
| + _didTransitionToActionReady = NO;
|
| + [self.overscrollActionView pullStarted];
|
| + if (!_performingScrollViewIndependentAnimation)
|
| + _allowPullingActions = [self isOverscrollActionsAllowed];
|
| + _lastScrollBeginTime = CACurrentMediaTime();
|
| +}
|
| +
|
| +- (BOOL)isOverscrollActionsAllowed {
|
| + const BOOL isZooming = [[self scrollView] isZooming];
|
| + // Check that the scrollview is scrolled to top.
|
| + const BOOL isScrolledToTop = fabs([[self scrollView] contentOffset].y +
|
| + [[self scrollView] contentInset].top) <=
|
| + kScrolledToTopToleranceInPoint;
|
| + // Check that the user is not quickly scrolling the view repeatedly.
|
| + const BOOL isMinimumTimeBetweenScrollRespected =
|
| + (CACurrentMediaTime() - _lastScrollBeginTime) >=
|
| + kMinimumDurationBetweenScrollingInSeconds;
|
| + // Finally check that the delegate allow overscroll actions.
|
| + const BOOL delegateAllowOverscrollActions =
|
| + [self.delegate shouldAllowOverscrollActions];
|
| + const BOOL isCurrentlyProcessingOverscroll =
|
| + self.overscrollState != ios_internal::OverscrollState::NO_PULL_STARTED;
|
| + return isCurrentlyProcessingOverscroll ||
|
| + (isScrolledToTop && isMinimumTimeBetweenScrollRespected &&
|
| + delegateAllowOverscrollActions && !isZooming);
|
| +}
|
| +
|
| +- (void)scrollViewDidEndDraggingWillDecelerate:(BOOL)decelerate
|
| + contentOffset:(CGPoint)contentOffset {
|
| + // Content is now hidden behind toolbar, make sure that contentInset is
|
| + // restored to initial value.
|
| + if (contentOffset.y >= 0 ||
|
| + self.overscrollState == ios_internal::OverscrollState::NO_PULL_STARTED) {
|
| + [self setScrollViewContentInset:UIEdgeInsetsMake(self.initialContentInset,
|
| + 0, 0, 0)];
|
| + }
|
| +
|
| + [self triggerActionIfNeeded];
|
| + _allowPullingActions = NO;
|
| +}
|
| +
|
| +- (void)scrollViewWillEndDraggingWithVelocity:(CGPoint)velocity
|
| + targetContentOffset:
|
| + (inout CGPoint*)targetContentOffset {
|
| + if (![self isOverscrollActionEnabled])
|
| + return;
|
| +
|
| + if (self.overscrollState != ios_internal::OverscrollState::NO_PULL_STARTED) {
|
| + *targetContentOffset = [[self scrollView] contentOffset];
|
| + [self startBounceWithInitialVelocity:velocity];
|
| + }
|
| +}
|
| +
|
| +- (void)webViewScrollViewProxyDidSetScrollView:
|
| + (CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
|
| + [self setup];
|
| +}
|
| +
|
| +#pragma mark - UIScrollViewDelegate
|
| +
|
| +- (void)scrollViewDidScroll:(UIScrollView*)scrollView {
|
| + DCHECK_EQ(static_cast<id>(scrollView), [self scrollView]);
|
| + [self scrollViewDidScroll];
|
| +}
|
| +
|
| +- (void)scrollViewWillBeginDragging:(UIScrollView*)scrollView {
|
| + DCHECK_EQ(static_cast<id>(scrollView), [self scrollView]);
|
| + [self scrollViewWillBeginDragging];
|
| +}
|
| +
|
| +- (void)scrollViewDidEndDragging:(UIScrollView*)scrollView
|
| + willDecelerate:(BOOL)decelerate {
|
| + DCHECK_EQ(static_cast<id>(scrollView), [self scrollView]);
|
| + [self scrollViewDidEndDraggingWillDecelerate:decelerate
|
| + contentOffset:scrollView.contentOffset];
|
| +}
|
| +
|
| +- (void)scrollViewWillEndDragging:(UIScrollView*)scrollView
|
| + withVelocity:(CGPoint)velocity
|
| + targetContentOffset:(inout CGPoint*)targetContentOffset {
|
| + DCHECK_EQ(static_cast<id>(scrollView), [self scrollView]);
|
| + [self scrollViewWillEndDraggingWithVelocity:velocity
|
| + targetContentOffset:targetContentOffset];
|
| +}
|
| +
|
| +#pragma mark - CRWWebViewScrollViewProxyObserver
|
| +
|
| +- (void)webViewScrollViewDidScroll:
|
| + (CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
|
| + DCHECK_EQ(static_cast<id>(webViewScrollViewProxy), [self scrollView]);
|
| + [self scrollViewDidScroll];
|
| +}
|
| +
|
| +- (void)webViewScrollViewWillBeginDragging:
|
| + (CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
|
| + DCHECK_EQ(static_cast<id>(webViewScrollViewProxy), [self scrollView]);
|
| + [self scrollViewWillBeginDragging];
|
| +}
|
| +
|
| +- (void)webViewScrollViewDidEndDragging:
|
| + (CRWWebViewScrollViewProxy*)webViewScrollViewProxy
|
| + willDecelerate:(BOOL)decelerate {
|
| + DCHECK_EQ(static_cast<id>(webViewScrollViewProxy), [self scrollView]);
|
| + [self scrollViewDidEndDraggingWillDecelerate:decelerate
|
| + contentOffset:webViewScrollViewProxy
|
| + .contentOffset];
|
| +}
|
| +
|
| +- (void)webViewScrollViewWillEndDragging:
|
| + (CRWWebViewScrollViewProxy*)webViewScrollViewProxy
|
| + withVelocity:(CGPoint)velocity
|
| + targetContentOffset:(inout CGPoint*)targetContentOffset {
|
| + DCHECK_EQ(static_cast<id>(webViewScrollViewProxy), [self scrollView]);
|
| + [self scrollViewWillEndDraggingWithVelocity:velocity
|
| + targetContentOffset:targetContentOffset];
|
| +}
|
| +
|
| +#pragma mark - Pan gesture recognizer handling
|
| +
|
| +- (void)panGesture:(UIPanGestureRecognizer*)gesture {
|
| + if (gesture.state == UIGestureRecognizerStateBegan) {
|
| + [self setWebViewInteractionEnabled:NO];
|
| + } else if (gesture.state == UIGestureRecognizerStateEnded ||
|
| + gesture.state == UIGestureRecognizerStateCancelled) {
|
| + [self setWebViewInteractionEnabled:YES];
|
| + }
|
| + const CGPoint panPointScreen = [gesture locationInView:nil];
|
| + if (self.overscrollState == ios_internal::OverscrollState::ACTION_READY) {
|
| + const CGFloat direction = UseRTLLayout() ? -1 : 1;
|
| + const CGFloat xOffset = direction *
|
| + (panPointScreen.x - self.panPointScreenOrigin.x) /
|
| + kHorizontalPanDistance;
|
| +
|
| + [self.overscrollActionView updateWithHorizontalOffset:xOffset];
|
| + }
|
| +}
|
| +
|
| +- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
|
| + shouldRecognizeSimultaneouslyWithGestureRecognizer:
|
| + (UIGestureRecognizer*)otherGestureRecognizer {
|
| + return YES;
|
| +}
|
| +
|
| +#pragma mark - CRWWebControllerObserver methods
|
| +
|
| +- (void)setWebViewProxy:(id<CRWWebViewProxy>)webViewProxy
|
| + controller:(CRWWebController*)webController {
|
| + DCHECK([webViewProxy scrollViewProxy]);
|
| + _initialHeaderInset = 0;
|
| + _initialHeaderHeight = 0;
|
| + _webViewProxy.reset([webViewProxy retain]);
|
| + [_webViewScrollViewProxy removeObserver:self];
|
| + _webViewScrollViewProxy.reset([[webViewProxy scrollViewProxy] retain]);
|
| + [_webViewScrollViewProxy addObserver:self];
|
| + [self enableOverscrollActions];
|
| +}
|
| +
|
| +- (void)webControllerWillClose:(CRWWebController*)webController {
|
| + [self disableOverscrollActions];
|
| + [_webViewScrollViewProxy removeObserver:self];
|
| + _webViewScrollViewProxy.reset();
|
| + [webController removeObserver:self];
|
| +}
|
| +
|
| +#pragma mark - Private
|
| +
|
| +- (void)recordMetricForTriggeredAction:(ios_internal::OverscrollAction)action {
|
| + switch (action) {
|
| + case ios_internal::OverscrollAction::NONE:
|
| + UMA_HISTOGRAM_ENUMERATION(kOverscrollActionsHistogram,
|
| + OVERSCROLL_ACTION_CANCELED,
|
| + OVERSCROLL_ACTION_COUNT);
|
| + break;
|
| + case ios_internal::OverscrollAction::NEW_TAB:
|
| + UMA_HISTOGRAM_ENUMERATION(kOverscrollActionsHistogram,
|
| + OVERSCROLL_ACTION_NEW_TAB,
|
| + OVERSCROLL_ACTION_COUNT);
|
| + break;
|
| + case ios_internal::OverscrollAction::REFRESH:
|
| + UMA_HISTOGRAM_ENUMERATION(kOverscrollActionsHistogram,
|
| + OVERSCROLL_ACTION_REFRESH,
|
| + OVERSCROLL_ACTION_COUNT);
|
| + break;
|
| + case ios_internal::OverscrollAction::CLOSE_TAB:
|
| + UMA_HISTOGRAM_ENUMERATION(kOverscrollActionsHistogram,
|
| + OVERSCROLL_ACTION_CLOSE_TAB,
|
| + OVERSCROLL_ACTION_COUNT);
|
| + break;
|
| + }
|
| +}
|
| +
|
| +- (void)registerNotifications {
|
| + NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
|
| + for (NSString* counterpartNotificationName in _lockNotificationsCounterparts
|
| + .get()) {
|
| + [center addObserver:self
|
| + selector:@selector(incrementOverscrollActionLockForNotification:)
|
| + name:[_lockNotificationsCounterparts
|
| + objectForKey:counterpartNotificationName]
|
| + object:nil];
|
| + [center addObserver:self
|
| + selector:@selector(decrementOverscrollActionLockForNotification:)
|
| + name:counterpartNotificationName
|
| + object:nil];
|
| + }
|
| + [center addObserver:self
|
| + selector:@selector(deviceOrientationDidChange)
|
| + name:UIDeviceOrientationDidChangeNotification
|
| + object:nil];
|
| +}
|
| +
|
| +- (void)tearDown {
|
| + [[self scrollView] removeGestureRecognizer:self.panGestureRecognizer];
|
| + self.panGestureRecognizer = nil;
|
| +}
|
| +
|
| +- (void)setup {
|
| + base::scoped_nsobject<UIPanGestureRecognizer> panGesture(
|
| + [[UIPanGestureRecognizer alloc] initWithTarget:self
|
| + action:@selector(panGesture:)]);
|
| + [panGesture setMaximumNumberOfTouches:1];
|
| + [panGesture setDelegate:self];
|
| + [[self scrollView] addGestureRecognizer:panGesture];
|
| + self.panGestureRecognizer = panGesture.get();
|
| +}
|
| +
|
| +- (id<OverscrollActionsScrollView>)scrollView {
|
| + if (_scrollview) {
|
| + return static_cast<id<OverscrollActionsScrollView>>(_scrollview.get());
|
| + } else {
|
| + return static_cast<id<OverscrollActionsScrollView>>(
|
| + _webViewScrollViewProxy.get());
|
| + }
|
| +}
|
| +
|
| +- (void)setScrollViewContentInset:(UIEdgeInsets)contentInset {
|
| + if (_scrollview)
|
| + [_scrollview setContentInset:contentInset];
|
| + else
|
| + [_webViewScrollViewProxy setContentInsetFast:contentInset];
|
| +}
|
| +
|
| +- (UIView<RelaxedBoundsConstraintsHitTestSupport>*)headerView {
|
| + return [self.delegate headerView];
|
| +}
|
| +
|
| +- (void)incrementOverscrollActionLockForNotification:(NSNotification*)notif {
|
| + if (![_lockIncrementNotifications containsObject:notif.name]) {
|
| + [_lockIncrementNotifications addObject:notif.name];
|
| + ++_overscrollActionLock;
|
| + }
|
| +}
|
| +
|
| +- (void)decrementOverscrollActionLockForNotification:(NSNotification*)notif {
|
| + NSString* counterpartName =
|
| + [_lockNotificationsCounterparts objectForKey:notif.name];
|
| + if ([_lockIncrementNotifications containsObject:counterpartName]) {
|
| + [_lockIncrementNotifications removeObject:counterpartName];
|
| + if (_overscrollActionLock > 0)
|
| + --_overscrollActionLock;
|
| + }
|
| +}
|
| +
|
| +- (void)deviceOrientationDidChange {
|
| + if (self.overscrollState == ios_internal::OverscrollState::NO_PULL_STARTED &&
|
| + !_performingScrollViewIndependentAnimation)
|
| + return;
|
| +
|
| + const UIDeviceOrientation deviceOrientation =
|
| + [[UIDevice currentDevice] orientation];
|
| + if (deviceOrientation != UIDeviceOrientationLandscapeRight &&
|
| + deviceOrientation != UIDeviceOrientationLandscapeLeft &&
|
| + deviceOrientation != UIDeviceOrientationPortrait) {
|
| + return;
|
| + }
|
| +
|
| + // If the orientation change happen while the user is still scrolling the
|
| + // scrollview, we need to reset the pan gesture recognizer.
|
| + // Not doing so would result in a graphic issue where the scrollview jumps
|
| + // when scrolling after a change in UI orientation.
|
| + [[self scrollView] panGestureRecognizer].enabled = NO;
|
| + [[self scrollView] panGestureRecognizer].enabled = YES;
|
| +
|
| + [self setScrollViewContentInset:UIEdgeInsetsMake(self.initialContentInset, 0,
|
| + 0, 0)];
|
| + [self clear];
|
| +}
|
| +
|
| +- (BOOL)isOverscrollActionEnabled {
|
| + return _overscrollActionLock == 0 && _allowPullingActions &&
|
| + !_isOverscrollActionsDisabledForLoading;
|
| +}
|
| +
|
| +- (void)triggerActionIfNeeded {
|
| + if ([self isOverscrollActionEnabled]) {
|
| + const BOOL isOverscrollStateActionReady =
|
| + self.overscrollState == ios_internal::OverscrollState::ACTION_READY;
|
| + const BOOL isOverscrollActionNone =
|
| + self.overscrollActionView.selectedAction ==
|
| + ios_internal::OverscrollAction::NONE;
|
| +
|
| + if ((!isOverscrollStateActionReady && _didTransitionToActionReady) ||
|
| + (isOverscrollStateActionReady && isOverscrollActionNone)) {
|
| + [self
|
| + recordMetricForTriggeredAction:ios_internal::OverscrollAction::NONE];
|
| + } else if (isOverscrollStateActionReady && !isOverscrollActionNone) {
|
| + if (CACurrentMediaTime() - _lastScrollBeginTime >=
|
| + kMinimumPullDurationToTriggerActionInSeconds) {
|
| + _performingScrollViewIndependentAnimation = YES;
|
| + [self setScrollViewContentInset:UIEdgeInsetsMake(
|
| + self.initialContentInset, 0, 0, 0)];
|
| + CGPoint contentOffset = [[self scrollView] contentOffset];
|
| + contentOffset.y = -self.initialContentInset;
|
| + [[self scrollView] setContentOffset:contentOffset animated:YES];
|
| + [self.overscrollActionView displayActionAnimation];
|
| + dispatch_async(dispatch_get_main_queue(), ^{
|
| + [self recordMetricForTriggeredAction:self.overscrollActionView
|
| + .selectedAction];
|
| + [self.delegate overscrollActionsController:self
|
| + didTriggerAction:self.overscrollActionView
|
| + .selectedAction];
|
| + });
|
| + }
|
| + }
|
| + }
|
| +}
|
| +
|
| +- (void)setOverscrollState:(ios_internal::OverscrollState)overscrollState {
|
| + if (_overscrollState != overscrollState) {
|
| + const ios_internal::OverscrollState previousState = _overscrollState;
|
| + _overscrollState = overscrollState;
|
| + [self onOverscrollStateChangeWithPreviousState:previousState];
|
| + }
|
| +}
|
| +
|
| +- (void)onOverscrollStateChangeWithPreviousState:
|
| + (ios_internal::OverscrollState)previousOverscrollState {
|
| + [UIView beginAnimations:@"backgroundColor" context:NULL];
|
| + switch (self.overscrollState) {
|
| + case ios_internal::OverscrollState::NO_PULL_STARTED: {
|
| + UIView<RelaxedBoundsConstraintsHitTestSupport>* headerView =
|
| + [self headerView];
|
| + if ([headerView
|
| + respondsToSelector:@selector(setHitTestBoundsContraintRelaxed:)])
|
| + [headerView setHitTestBoundsContraintRelaxed:NO];
|
| + [self.overscrollActionView removeFromSuperview];
|
| + SetViewFrameHeight(
|
| + self.overscrollActionView,
|
| + self.initialContentInset +
|
| + [UIApplication sharedApplication].statusBarFrame.size.height);
|
| + self.panPointScreenOrigin = CGPointZero;
|
| + [[NSNotificationCenter defaultCenter]
|
| + postNotificationName:ios_internal::kOverscollActionsDidEnd
|
| + object:self];
|
| + } break;
|
| + case ios_internal::OverscrollState::STARTED_PULLING: {
|
| + if (!self.overscrollActionView.superview) {
|
| + if (previousOverscrollState ==
|
| + ios_internal::OverscrollState::NO_PULL_STARTED) {
|
| + UIView* view = [self.delegate toolbarSnapshotView];
|
| + [self.overscrollActionView addSnapshotView:view];
|
| + [[NSNotificationCenter defaultCenter]
|
| + postNotificationName:ios_internal::kOverscollActionsWillStart
|
| + object:self];
|
| + }
|
| + [CATransaction begin];
|
| + [CATransaction setDisableActions:YES];
|
| + self.overscrollActionView.backgroundView.alpha = 1;
|
| + [self.overscrollActionView updateWithVerticalOffset:0];
|
| + [self.overscrollActionView updateWithHorizontalOffset:0];
|
| + self.overscrollActionView.frame = [self headerView].bounds;
|
| + DCHECK([self headerView]);
|
| + UIView<RelaxedBoundsConstraintsHitTestSupport>* headerView =
|
| + [self headerView];
|
| + if ([headerView
|
| + respondsToSelector:@selector(
|
| + setHitTestBoundsContraintRelaxed:)])
|
| + [headerView setHitTestBoundsContraintRelaxed:YES];
|
| + [headerView addSubview:self.overscrollActionView];
|
| + [CATransaction commit];
|
| + }
|
| + } break;
|
| + case ios_internal::OverscrollState::ACTION_READY: {
|
| + _didTransitionToActionReady = YES;
|
| + if (CGPointEqualToPoint(self.panPointScreenOrigin, CGPointZero)) {
|
| + CGPoint panPointScreen = [self.panGestureRecognizer locationInView:nil];
|
| + self.panPointScreenOrigin = panPointScreen;
|
| + }
|
| + } break;
|
| + }
|
| + [UIView commitAnimations];
|
| +}
|
| +
|
| +- (void)setWebViewInteractionEnabled:(BOOL)enabled {
|
| + // All interactions are disabled except pan.
|
| + for (UIGestureRecognizer* gesture in [_webViewProxy gestureRecognizers]) {
|
| + [gesture setEnabled:enabled];
|
| + }
|
| + for (UIGestureRecognizer* gesture in
|
| + [_webViewScrollViewProxy gestureRecognizers]) {
|
| + if (![gesture isKindOfClass:[UIPanGestureRecognizer class]]) {
|
| + [gesture setEnabled:enabled];
|
| + }
|
| + }
|
| + // Add a dummy view on top of the webview in order to catch touches on some
|
| + // specific subviews.
|
| + if (!enabled) {
|
| + if (!_dummyView)
|
| + _dummyView.reset([[UIView alloc] init]);
|
| + [_dummyView setFrame:[_webViewProxy bounds]];
|
| + [_webViewProxy addSubview:_dummyView];
|
| + } else {
|
| + [_dummyView removeFromSuperview];
|
| + }
|
| +}
|
| +
|
| +- (void)updateWithVerticalOffset:(CGFloat)verticalOffset {
|
| + self.overscrollActionView.backgroundView.alpha =
|
| + 1.0 -
|
| + Clamp(verticalOffset / (kHeaderMaxExpansionThreshold / 2.0), 0.0, 1.0);
|
| + SetViewFrameHeight(self.overscrollActionView,
|
| + self.initialHeaderHeight + verticalOffset);
|
| + [self.overscrollActionView updateWithVerticalOffset:verticalOffset];
|
| +}
|
| +
|
| +- (CGFloat)initialContentInset {
|
| + // Content inset is not used for displaying header if the web view's
|
| + // |shouldUseInsetForTopPadding| is NO, instead the whole web view
|
| + // frame is changed.
|
| + if (!_scrollview && ![_webViewProxy shouldUseInsetForTopPadding])
|
| + return 0;
|
| + return self.initialHeaderInset;
|
| +}
|
| +
|
| +- (CGFloat)initialHeaderInset {
|
| + if (_initialHeaderInset == 0) {
|
| + _initialHeaderInset =
|
| + [[self delegate] overscrollActionsControllerHeaderInset:self];
|
| + }
|
| + return _initialHeaderInset;
|
| +}
|
| +
|
| +- (CGFloat)initialHeaderHeight {
|
| + if (_initialHeaderHeight == 0) {
|
| + _initialHeaderHeight = [[self delegate] overscrollHeaderHeight];
|
| + }
|
| + return _initialHeaderHeight;
|
| +}
|
| +
|
| +#pragma mark - Bounce dynamic
|
| +
|
| +- (void)startBounceWithInitialVelocity:(CGPoint)velocity {
|
| + [self stopBounce];
|
| + CADisplayLink* dpLink =
|
| + [CADisplayLink displayLinkWithTarget:self
|
| + selector:@selector(updateBounce)];
|
| + [dpLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
|
| + _dpLink = dpLink;
|
| + memset(&_bounceState, 0, sizeof(_bounceState));
|
| + if (self.overscrollState == ios_internal::OverscrollState::ACTION_READY) {
|
| + const UIEdgeInsets insets = UIEdgeInsetsMake(
|
| + -[self scrollView].contentOffset.y + self.initialContentInset, 0, 0, 0);
|
| + [self setScrollViewContentInset:insets];
|
| + [[self scrollView] setScrollIndicatorInsets:insets];
|
| + }
|
| + _bounceState.yInset = [self scrollView].contentInset.top;
|
| + _bounceState.initialYInset = _bounceState.yInset;
|
| + _bounceState.headerInset = self.initialContentInset;
|
| + _bounceState.time = CACurrentMediaTime();
|
| + _bounceState.velocityInset = -velocity.y * 1000.0;
|
| +}
|
| +
|
| +- (void)stopBounce {
|
| + [_dpLink invalidate];
|
| + _dpLink = nil;
|
| + if (_performingScrollViewIndependentAnimation) {
|
| + self.overscrollState = ios_internal::OverscrollState::NO_PULL_STARTED;
|
| + _performingScrollViewIndependentAnimation = NO;
|
| + }
|
| +}
|
| +
|
| +- (void)updateBounce {
|
| + const double time = CACurrentMediaTime();
|
| + const double dt = time - _bounceState.time;
|
| + CGFloat force = -_bounceState.yInset * kSpringTightness;
|
| + if (_bounceState.yInset > _bounceState.headerInset)
|
| + force -= _bounceState.velocityInset * kSpringDampiness;
|
| + _bounceState.velocityInset += force;
|
| + _bounceState.yInset += _bounceState.velocityInset * dt;
|
| + _bounceState.time = time;
|
| + [self applyBounceState];
|
| + if (fabs(_bounceState.yInset - _bounceState.headerInset) < 0.5)
|
| + [self stopBounce];
|
| +}
|
| +
|
| +- (void)applyBounceState {
|
| + if (_bounceState.yInset - _bounceState.headerInset < 0.5)
|
| + _bounceState.yInset = _bounceState.headerInset;
|
| + if (_performingScrollViewIndependentAnimation) {
|
| + [self updateWithVerticalOffset:_bounceState.yInset -
|
| + _bounceState.headerInset];
|
| + } else {
|
| + const UIEdgeInsets insets = UIEdgeInsetsMake(_bounceState.yInset, 0, 0, 0);
|
| + _forceStateUpdate = YES;
|
| + [self setScrollViewContentInset:insets];
|
| + [self scrollView].scrollIndicatorInsets = insets;
|
| + _forceStateUpdate = NO;
|
| + }
|
| +}
|
| +
|
| +#pragma mark - OverscrollActionsViewDelegate
|
| +
|
| +- (void)overscrollActionsViewDidTapTriggerAction:
|
| + (OverscrollActionsView*)overscrollActionsView {
|
| + [self.overscrollActionView displayActionAnimation];
|
| + [self
|
| + recordMetricForTriggeredAction:self.overscrollActionView.selectedAction];
|
| +
|
| + // Reset all pan gesture recognizers.
|
| + _allowPullingActions = NO;
|
| + _panGestureRecognizer.enabled = NO;
|
| + _panGestureRecognizer.enabled = YES;
|
| + [self scrollView].panGestureRecognizer.enabled = NO;
|
| + [self scrollView].panGestureRecognizer.enabled = YES;
|
| + [self startBounceWithInitialVelocity:CGPointZero];
|
| + [self.delegate
|
| + overscrollActionsController:self
|
| + didTriggerAction:self.overscrollActionView.selectedAction];
|
| +}
|
| +
|
| +@end
|
|
|