Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(85)

Unified Diff: ios/chrome/browser/ui/overscroll_actions/overscroll_actions_controller.mm

Issue 2589803002: Upstream Chrome on iOS source code [6/11]. (Closed)
Patch Set: Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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

Powered by Google App Engine
This is Rietveld 408576698