OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 #import "ios/chrome/browser/ui/overscroll_actions/overscroll_actions_controller.
h" |
| 6 |
| 7 #import <QuartzCore/QuartzCore.h> |
| 8 |
| 9 #include <algorithm> |
| 10 #include "base/logging.h" |
| 11 #include "base/mac/objc_property_releaser.h" |
| 12 #include "base/mac/scoped_nsobject.h" |
| 13 #include "base/metrics/histogram.h" |
| 14 #import "ios/chrome/browser/ui/browser_view_controller.h" |
| 15 #import "ios/chrome/browser/ui/overscroll_actions/overscroll_actions_view.h" |
| 16 #include "ios/chrome/browser/ui/rtl_geometry.h" |
| 17 #import "ios/chrome/browser/ui/toolbar/toolbar_controller.h" |
| 18 #import "ios/chrome/browser/ui/toolbar/web_toolbar_controller.h" |
| 19 #import "ios/chrome/browser/ui/voice/voice_search_notification_names.h" |
| 20 #import "ios/web/public/web_state/crw_web_view_proxy.h" |
| 21 |
| 22 namespace { |
| 23 // This enum is used to record the overscroll actions performed by the user on |
| 24 // the histogram named |OverscrollActions|. |
| 25 enum { |
| 26 // Records each time the user selects the new tab action. |
| 27 OVERSCROLL_ACTION_NEW_TAB, |
| 28 // Records each time the user selects the refresh action. |
| 29 OVERSCROLL_ACTION_REFRESH, |
| 30 // Records each time the user selects the close tab action. |
| 31 OVERSCROLL_ACTION_CLOSE_TAB, |
| 32 // Records each time the user cancels the overscroll action. |
| 33 OVERSCROLL_ACTION_CANCELED, |
| 34 // NOTE: Add new actions in sources only immediately above this line. |
| 35 // Also, make sure the enum list for histogram |OverscrollActions| in |
| 36 // tools/histogram/histograms.xml is updated with any change in here. |
| 37 OVERSCROLL_ACTION_COUNT |
| 38 }; |
| 39 |
| 40 // The histogram used to record user actions. |
| 41 const char kOverscrollActionsHistogram[] = "Tab.PullDownGesture"; |
| 42 |
| 43 // The pulling threshold in point at which the controller will start accepting |
| 44 // actions. |
| 45 // Past this pulling value the scrollView will start to resist from pulling. |
| 46 const CGFloat kHeaderMaxExpansionThreshold = 56.0; |
| 47 // The default overall distance in point to select different actions |
| 48 // horizontally. |
| 49 const CGFloat kHorizontalPanDistance = 400.0; |
| 50 // The distance from the top content offset which will be used to detect |
| 51 // if the scrollview is scrolled to top. |
| 52 const CGFloat kScrolledToTopToleranceInPoint = 50; |
| 53 // The minimum duration between scrolling in order to allow overscroll actions. |
| 54 // In seconds. |
| 55 const CFTimeInterval kMinimumDurationBetweenScrollingInSeconds = 0.15; |
| 56 // The minimum duration that the pull must last in order to trigger an action. |
| 57 // In seconds. |
| 58 const CFTimeInterval kMinimumPullDurationToTriggerActionInSeconds = 0.2; |
| 59 // Bounce dynamic constants. |
| 60 // Since the bounce effect of the scrollview is cancelled by setting the |
| 61 // contentInsets to the value of the overscroll contentOffset, the bounce |
| 62 // bounce back have to be emulated manually using a spring simulation. |
| 63 const CGFloat kSpringTightness = 2; |
| 64 const CGFloat kSpringDampiness = 0.5; |
| 65 |
| 66 // This holds the current state of the bounce back animation. |
| 67 typedef struct { |
| 68 CGFloat yInset; |
| 69 CGFloat initialYInset; |
| 70 CGFloat headerInset; |
| 71 CGFloat velocityInset; |
| 72 CFAbsoluteTime time; |
| 73 } SpringInsetState; |
| 74 |
| 75 // Used to set the height of a view frame. |
| 76 // Implicit animations are disabled when setting the new frame. |
| 77 void SetViewFrameHeight(UIView* view, CGFloat height) { |
| 78 [CATransaction begin]; |
| 79 [CATransaction setDisableActions:YES]; |
| 80 CGRect viewFrame = view.frame; |
| 81 viewFrame.size.height = height; |
| 82 view.frame = viewFrame; |
| 83 [CATransaction commit]; |
| 84 } |
| 85 |
| 86 // Clamp a value between min and max. |
| 87 CGFloat Clamp(CGFloat value, CGFloat min, CGFloat max) { |
| 88 DCHECK(min < max); |
| 89 if (value < min) |
| 90 return min; |
| 91 if (value > max) |
| 92 return max; |
| 93 return value; |
| 94 } |
| 95 } // namespace |
| 96 |
| 97 namespace ios_internal { |
| 98 NSString* const kOverscollActionsWillStart = @"OverscollActionsWillStart"; |
| 99 NSString* const kOverscollActionsDidEnd = @"OverscollActionsDidStop"; |
| 100 } // namespace ios_internal |
| 101 |
| 102 // This protocol describes the subset of methods used between the |
| 103 // CRWWebViewScrollViewProxy and the UIWebView. |
| 104 @protocol OverscrollActionsScrollView<NSObject> |
| 105 |
| 106 @property(nonatomic, assign) UIEdgeInsets contentInset; |
| 107 @property(nonatomic, assign) CGPoint contentOffset; |
| 108 @property(nonatomic, assign) UIEdgeInsets scrollIndicatorInsets; |
| 109 @property(nonatomic, readonly) UIPanGestureRecognizer* panGestureRecognizer; |
| 110 @property(nonatomic, readonly) BOOL isZooming; |
| 111 |
| 112 - (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated; |
| 113 - (void)addGestureRecognizer:(UIGestureRecognizer*)gestureRecognizer; |
| 114 - (void)removeGestureRecognizer:(UIGestureRecognizer*)gestureRecognizer; |
| 115 |
| 116 @end |
| 117 |
| 118 @interface OverscrollActionsController ()<CRWWebViewScrollViewProxyObserver, |
| 119 UIGestureRecognizerDelegate, |
| 120 OverscrollActionsViewDelegate> { |
| 121 // Display link used to animate the bounce back effect. |
| 122 CADisplayLink* _dpLink; |
| 123 SpringInsetState _bounceState; |
| 124 NSInteger _overscrollActionLock; |
| 125 // The last time the user started scrolling the view. |
| 126 CFTimeInterval _lastScrollBeginTime; |
| 127 // Set to YES when the bounce animation must be independent of the scrollview |
| 128 // contentOffset change. |
| 129 // This is done when an action has been triggered. In that case the webview's |
| 130 // scrollview will change state depending on the action being triggered so |
| 131 // relying on the contentInset is not possible at that time. |
| 132 BOOL _performingScrollViewIndependentAnimation; |
| 133 // Force processing state changes in scrollviewDidScroll: even if |
| 134 // overscroll actions are disabled. |
| 135 // This is used to always process contentOffset changes on specific cases like |
| 136 // when playing the bounce back animation if no actions has been triggered. |
| 137 BOOL _forceStateUpdate; |
| 138 // True when the overscroll actions are disabled for loading. |
| 139 BOOL _isOverscrollActionsDisabledForLoading; |
| 140 // True when the pull gesture started close enough from the top and the |
| 141 // delegate allows it. |
| 142 // Use isOverscrollActionEnabled to take into account locking. |
| 143 BOOL _allowPullingActions; |
| 144 // Records if a transition to the overscroll state ACTION_READY was made. |
| 145 // This is used to record a cancel gesture. |
| 146 BOOL _didTransitionToActionReady; |
| 147 // Store the set of notifications that did increment the overscroll actions |
| 148 // lock. It is used in order to enforce the fact that the lock should only be |
| 149 // incremented/decremented once for a given notification. |
| 150 base::scoped_nsobject<NSMutableSet> _lockIncrementNotifications; |
| 151 // Store the notification name counterpart of another notification name. |
| 152 // Overscroll actions locking and unlocking works by listening to balanced |
| 153 // notifications. One notification lock and it's counterpart unlock. This |
| 154 // dictionary is used to retrieve the notification name from it's notification |
| 155 // counterpart name. Exemple: |
| 156 // UIKeyboardWillShowNotification trigger a lock. Its counterpart notification |
| 157 // name is UIKeyboardWillHideNotification. |
| 158 base::scoped_nsobject<NSDictionary> _lockNotificationsCounterparts; |
| 159 // A view used to catch touches on the webview. |
| 160 base::scoped_nsobject<UIView> _dummyView; |
| 161 // The proxy used to interact with the webview. |
| 162 base::scoped_nsprotocol<id<CRWWebViewProxy>> _webViewProxy; |
| 163 // The proxy used to interact with the webview's scrollview. |
| 164 base::scoped_nsobject<CRWWebViewScrollViewProxy> _webViewScrollViewProxy; |
| 165 // The scrollview driving the OverscrollActionsController when not using |
| 166 // the scrollview from the CRWWebControllerObserver. |
| 167 base::scoped_nsobject<UIScrollView> _scrollview; |
| 168 base::mac::ObjCPropertyReleaser _propertyReleaser_OverscrollActionsController; |
| 169 } |
| 170 |
| 171 // The view displayed over the header view holding the actions. |
| 172 @property(nonatomic, retain) OverscrollActionsView* overscrollActionView; |
| 173 // Initial top inset added to the scrollview for the header. |
| 174 // This property is set from the delegate headerInset and cached on first |
| 175 // call. The cached value is reset when the webview proxy is set. |
| 176 @property(nonatomic, readonly) CGFloat initialContentInset; |
| 177 // Initial top inset for the header. |
| 178 // This property is set from the delegate headerInset and cached on first |
| 179 // call. The cached value is reset when the webview proxy is set. |
| 180 @property(nonatomic, readonly) CGFloat initialHeaderInset; |
| 181 // Initial height of the header view. |
| 182 // This property is set from the delegate headerHeight and cached on first |
| 183 // call. The cached value is reset when the webview proxy is set. |
| 184 @property(nonatomic, readonly) CGFloat initialHeaderHeight; |
| 185 // Redefined to be read-write. |
| 186 @property(nonatomic, assign, readwrite) |
| 187 ios_internal::OverscrollState overscrollState; |
| 188 // Point where the horizontal gesture started when the state of the |
| 189 // overscroll controller is in OverscrollStateActionReady. |
| 190 @property(nonatomic, assign) CGPoint panPointScreenOrigin; |
| 191 // Pan gesture recognizer used to track horizontal touches. |
| 192 @property(nonatomic, retain) UIPanGestureRecognizer* panGestureRecognizer; |
| 193 |
| 194 // Registers notifications to lock the overscroll actions on certain UI states. |
| 195 - (void)registerNotifications; |
| 196 // Setup/tearDown methods are used to register values when the delegate is set. |
| 197 - (void)tearDown; |
| 198 - (void)setup; |
| 199 // Access the headerView from the delegate. |
| 200 - (UIView<RelaxedBoundsConstraintsHitTestSupport>*)headerView; |
| 201 // Locking/unlocking methods used to disable/enable the overscroll actions |
| 202 // with a reference count. |
| 203 - (void)incrementOverscrollActionLockForNotification: |
| 204 (NSNotification*)notification; |
| 205 - (void)decrementOverscrollActionLockForNotification: |
| 206 (NSNotification*)notification; |
| 207 // Indicates whether the overscroll action is allowed. |
| 208 - (BOOL)isOverscrollActionEnabled; |
| 209 // Triggers a call to delegate if an action has been triggered. |
| 210 - (void)triggerActionIfNeeded; |
| 211 // Performs work based on overscroll action state changes. |
| 212 - (void)onOverscrollStateChangeWithPreviousState: |
| 213 (ios_internal::OverscrollState)previousOverscrollState; |
| 214 // Disables all interactions on the webview except pan. |
| 215 - (void)setWebViewInteractionEnabled:(BOOL)enabled; |
| 216 // Bounce dynamic animations methods. |
| 217 // Starts the bounce animation with an initial velocity. |
| 218 - (void)startBounceWithInitialVelocity:(CGPoint)velocity; |
| 219 // Stops bounce animation. |
| 220 - (void)stopBounce; |
| 221 // Called from the display link to update the bounce dynamic animation. |
| 222 - (void)updateBounce; |
| 223 // Applies bounce state to the scroll view. |
| 224 - (void)applyBounceState; |
| 225 |
| 226 @end |
| 227 |
| 228 @implementation OverscrollActionsController |
| 229 |
| 230 @synthesize overscrollActionView = _overscrollActionView; |
| 231 @synthesize initialHeaderInset = _initialHeaderInset; |
| 232 @synthesize initialHeaderHeight = _initialHeaderHeight; |
| 233 @synthesize overscrollState = _overscrollState; |
| 234 @synthesize delegate = _delegate; |
| 235 @synthesize panPointScreenOrigin = _panPointScreenOrigin; |
| 236 @synthesize panGestureRecognizer = _panGestureRecognizer; |
| 237 |
| 238 - (instancetype)init { |
| 239 return [self initWithScrollView:nil]; |
| 240 } |
| 241 |
| 242 - (instancetype)initWithScrollView:(UIScrollView*)scrollView { |
| 243 self = [super init]; |
| 244 if (self) { |
| 245 _propertyReleaser_OverscrollActionsController.Init( |
| 246 self, [OverscrollActionsController class]); |
| 247 _overscrollActionView = |
| 248 [[OverscrollActionsView alloc] initWithFrame:CGRectZero]; |
| 249 _overscrollActionView.delegate = self; |
| 250 _scrollview.reset([scrollView retain]); |
| 251 if (_scrollview) { |
| 252 [self setup]; |
| 253 } |
| 254 _lockIncrementNotifications.reset([[NSMutableSet alloc] init]); |
| 255 |
| 256 _lockNotificationsCounterparts.reset([@{ |
| 257 UIKeyboardWillHideNotification : UIKeyboardWillShowNotification, |
| 258 kMenuWillHideNotification : kMenuWillShowNotification, |
| 259 kTabHistoryPopupWillHideNotification : |
| 260 kTabHistoryPopupWillShowNotification, |
| 261 kVoiceSearchWillHideNotification : kVoiceSearchWillShowNotification, |
| 262 kVoiceSearchBarViewButtonDeselectedNotification : |
| 263 kVoiceSearchBarViewButtonSelectedNotification, |
| 264 ios_internal::kPageInfoWillHideNotification : |
| 265 ios_internal::kPageInfoWillShowNotification, |
| 266 ios_internal::kLocationBarResignsFirstResponderNotification : |
| 267 ios_internal::kLocationBarBecomesFirstResponderNotification, |
| 268 ios_internal::kSideSwipeDidStopNotification : |
| 269 ios_internal::kSideSwipeWillStartNotification |
| 270 } retain]); |
| 271 [self registerNotifications]; |
| 272 } |
| 273 return self; |
| 274 } |
| 275 |
| 276 - (void)dealloc { |
| 277 self.overscrollActionView.delegate = nil; |
| 278 [self invalidate]; |
| 279 [super dealloc]; |
| 280 } |
| 281 |
| 282 - (void)invalidate { |
| 283 [self clear]; |
| 284 [self stopBounce]; |
| 285 [self tearDown]; |
| 286 [[NSNotificationCenter defaultCenter] removeObserver:self]; |
| 287 [self setWebViewInteractionEnabled:YES]; |
| 288 _delegate = nil; |
| 289 _webViewProxy.reset(); |
| 290 [_webViewScrollViewProxy removeObserver:self]; |
| 291 _webViewScrollViewProxy.reset(); |
| 292 } |
| 293 |
| 294 - (void)clear { |
| 295 self.overscrollState = ios_internal::OverscrollState::NO_PULL_STARTED; |
| 296 } |
| 297 |
| 298 - (void)enableOverscrollActions { |
| 299 _isOverscrollActionsDisabledForLoading = NO; |
| 300 [self setup]; |
| 301 } |
| 302 |
| 303 - (void)disableOverscrollActions { |
| 304 _isOverscrollActionsDisabledForLoading = YES; |
| 305 [self tearDown]; |
| 306 } |
| 307 |
| 308 - (void)setStyle:(ios_internal::OverscrollStyle)style { |
| 309 [self.overscrollActionView setStyle:style]; |
| 310 } |
| 311 |
| 312 #pragma mark - webViewScrollView and UIScrollView delegates implementations |
| 313 |
| 314 - (void)scrollViewDidScroll { |
| 315 if (!_forceStateUpdate && (![self isOverscrollActionEnabled] || |
| 316 _performingScrollViewIndependentAnimation)) |
| 317 return; |
| 318 |
| 319 const UIEdgeInsets insets = |
| 320 UIEdgeInsetsMake(-[self scrollView].contentOffset.y, 0, 0, 0); |
| 321 // Start pulling (on top). |
| 322 CGFloat contentOffsetFromTheTop = [self scrollView].contentOffset.y; |
| 323 if (![_webViewProxy shouldUseInsetForTopPadding]) { |
| 324 // Content offset is shifted for WKWebView when the web view's |
| 325 // |shouldUseInsetForTopPadding| is NO, to workaround bug with |
| 326 // UIScollView.contentInset (rdar://23584409). |
| 327 contentOffsetFromTheTop -= [_webViewProxy topContentPadding]; |
| 328 } |
| 329 CGFloat contentOffsetFromExpandedHeader = |
| 330 contentOffsetFromTheTop + self.initialHeaderInset; |
| 331 if (contentOffsetFromExpandedHeader >= 0) { |
| 332 // Record initial content offset and dispatch delegate on state change. |
| 333 self.overscrollState = ios_internal::OverscrollState::NO_PULL_STARTED; |
| 334 } else { |
| 335 if (contentOffsetFromExpandedHeader < -kHeaderMaxExpansionThreshold) { |
| 336 self.overscrollState = ios_internal::OverscrollState::ACTION_READY; |
| 337 [self scrollView].scrollIndicatorInsets = insets; |
| 338 } else { |
| 339 // Set the contentInset to remove the bounce that would fight with drag. |
| 340 [self setScrollViewContentInset:insets]; |
| 341 [self scrollView].scrollIndicatorInsets = insets; |
| 342 self.overscrollState = ios_internal::OverscrollState::STARTED_PULLING; |
| 343 } |
| 344 [self updateWithVerticalOffset:-contentOffsetFromExpandedHeader]; |
| 345 } |
| 346 } |
| 347 |
| 348 - (void)scrollViewWillBeginDragging { |
| 349 [self stopBounce]; |
| 350 _allowPullingActions = NO; |
| 351 _didTransitionToActionReady = NO; |
| 352 [self.overscrollActionView pullStarted]; |
| 353 if (!_performingScrollViewIndependentAnimation) |
| 354 _allowPullingActions = [self isOverscrollActionsAllowed]; |
| 355 _lastScrollBeginTime = CACurrentMediaTime(); |
| 356 } |
| 357 |
| 358 - (BOOL)isOverscrollActionsAllowed { |
| 359 const BOOL isZooming = [[self scrollView] isZooming]; |
| 360 // Check that the scrollview is scrolled to top. |
| 361 const BOOL isScrolledToTop = fabs([[self scrollView] contentOffset].y + |
| 362 [[self scrollView] contentInset].top) <= |
| 363 kScrolledToTopToleranceInPoint; |
| 364 // Check that the user is not quickly scrolling the view repeatedly. |
| 365 const BOOL isMinimumTimeBetweenScrollRespected = |
| 366 (CACurrentMediaTime() - _lastScrollBeginTime) >= |
| 367 kMinimumDurationBetweenScrollingInSeconds; |
| 368 // Finally check that the delegate allow overscroll actions. |
| 369 const BOOL delegateAllowOverscrollActions = |
| 370 [self.delegate shouldAllowOverscrollActions]; |
| 371 const BOOL isCurrentlyProcessingOverscroll = |
| 372 self.overscrollState != ios_internal::OverscrollState::NO_PULL_STARTED; |
| 373 return isCurrentlyProcessingOverscroll || |
| 374 (isScrolledToTop && isMinimumTimeBetweenScrollRespected && |
| 375 delegateAllowOverscrollActions && !isZooming); |
| 376 } |
| 377 |
| 378 - (void)scrollViewDidEndDraggingWillDecelerate:(BOOL)decelerate |
| 379 contentOffset:(CGPoint)contentOffset { |
| 380 // Content is now hidden behind toolbar, make sure that contentInset is |
| 381 // restored to initial value. |
| 382 if (contentOffset.y >= 0 || |
| 383 self.overscrollState == ios_internal::OverscrollState::NO_PULL_STARTED) { |
| 384 [self setScrollViewContentInset:UIEdgeInsetsMake(self.initialContentInset, |
| 385 0, 0, 0)]; |
| 386 } |
| 387 |
| 388 [self triggerActionIfNeeded]; |
| 389 _allowPullingActions = NO; |
| 390 } |
| 391 |
| 392 - (void)scrollViewWillEndDraggingWithVelocity:(CGPoint)velocity |
| 393 targetContentOffset: |
| 394 (inout CGPoint*)targetContentOffset { |
| 395 if (![self isOverscrollActionEnabled]) |
| 396 return; |
| 397 |
| 398 if (self.overscrollState != ios_internal::OverscrollState::NO_PULL_STARTED) { |
| 399 *targetContentOffset = [[self scrollView] contentOffset]; |
| 400 [self startBounceWithInitialVelocity:velocity]; |
| 401 } |
| 402 } |
| 403 |
| 404 - (void)webViewScrollViewProxyDidSetScrollView: |
| 405 (CRWWebViewScrollViewProxy*)webViewScrollViewProxy { |
| 406 [self setup]; |
| 407 } |
| 408 |
| 409 #pragma mark - UIScrollViewDelegate |
| 410 |
| 411 - (void)scrollViewDidScroll:(UIScrollView*)scrollView { |
| 412 DCHECK_EQ(static_cast<id>(scrollView), [self scrollView]); |
| 413 [self scrollViewDidScroll]; |
| 414 } |
| 415 |
| 416 - (void)scrollViewWillBeginDragging:(UIScrollView*)scrollView { |
| 417 DCHECK_EQ(static_cast<id>(scrollView), [self scrollView]); |
| 418 [self scrollViewWillBeginDragging]; |
| 419 } |
| 420 |
| 421 - (void)scrollViewDidEndDragging:(UIScrollView*)scrollView |
| 422 willDecelerate:(BOOL)decelerate { |
| 423 DCHECK_EQ(static_cast<id>(scrollView), [self scrollView]); |
| 424 [self scrollViewDidEndDraggingWillDecelerate:decelerate |
| 425 contentOffset:scrollView.contentOffset]; |
| 426 } |
| 427 |
| 428 - (void)scrollViewWillEndDragging:(UIScrollView*)scrollView |
| 429 withVelocity:(CGPoint)velocity |
| 430 targetContentOffset:(inout CGPoint*)targetContentOffset { |
| 431 DCHECK_EQ(static_cast<id>(scrollView), [self scrollView]); |
| 432 [self scrollViewWillEndDraggingWithVelocity:velocity |
| 433 targetContentOffset:targetContentOffset]; |
| 434 } |
| 435 |
| 436 #pragma mark - CRWWebViewScrollViewProxyObserver |
| 437 |
| 438 - (void)webViewScrollViewDidScroll: |
| 439 (CRWWebViewScrollViewProxy*)webViewScrollViewProxy { |
| 440 DCHECK_EQ(static_cast<id>(webViewScrollViewProxy), [self scrollView]); |
| 441 [self scrollViewDidScroll]; |
| 442 } |
| 443 |
| 444 - (void)webViewScrollViewWillBeginDragging: |
| 445 (CRWWebViewScrollViewProxy*)webViewScrollViewProxy { |
| 446 DCHECK_EQ(static_cast<id>(webViewScrollViewProxy), [self scrollView]); |
| 447 [self scrollViewWillBeginDragging]; |
| 448 } |
| 449 |
| 450 - (void)webViewScrollViewDidEndDragging: |
| 451 (CRWWebViewScrollViewProxy*)webViewScrollViewProxy |
| 452 willDecelerate:(BOOL)decelerate { |
| 453 DCHECK_EQ(static_cast<id>(webViewScrollViewProxy), [self scrollView]); |
| 454 [self scrollViewDidEndDraggingWillDecelerate:decelerate |
| 455 contentOffset:webViewScrollViewProxy |
| 456 .contentOffset]; |
| 457 } |
| 458 |
| 459 - (void)webViewScrollViewWillEndDragging: |
| 460 (CRWWebViewScrollViewProxy*)webViewScrollViewProxy |
| 461 withVelocity:(CGPoint)velocity |
| 462 targetContentOffset:(inout CGPoint*)targetContentOffset { |
| 463 DCHECK_EQ(static_cast<id>(webViewScrollViewProxy), [self scrollView]); |
| 464 [self scrollViewWillEndDraggingWithVelocity:velocity |
| 465 targetContentOffset:targetContentOffset]; |
| 466 } |
| 467 |
| 468 #pragma mark - Pan gesture recognizer handling |
| 469 |
| 470 - (void)panGesture:(UIPanGestureRecognizer*)gesture { |
| 471 if (gesture.state == UIGestureRecognizerStateBegan) { |
| 472 [self setWebViewInteractionEnabled:NO]; |
| 473 } else if (gesture.state == UIGestureRecognizerStateEnded || |
| 474 gesture.state == UIGestureRecognizerStateCancelled) { |
| 475 [self setWebViewInteractionEnabled:YES]; |
| 476 } |
| 477 const CGPoint panPointScreen = [gesture locationInView:nil]; |
| 478 if (self.overscrollState == ios_internal::OverscrollState::ACTION_READY) { |
| 479 const CGFloat direction = UseRTLLayout() ? -1 : 1; |
| 480 const CGFloat xOffset = direction * |
| 481 (panPointScreen.x - self.panPointScreenOrigin.x) / |
| 482 kHorizontalPanDistance; |
| 483 |
| 484 [self.overscrollActionView updateWithHorizontalOffset:xOffset]; |
| 485 } |
| 486 } |
| 487 |
| 488 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer |
| 489 shouldRecognizeSimultaneouslyWithGestureRecognizer: |
| 490 (UIGestureRecognizer*)otherGestureRecognizer { |
| 491 return YES; |
| 492 } |
| 493 |
| 494 #pragma mark - CRWWebControllerObserver methods |
| 495 |
| 496 - (void)setWebViewProxy:(id<CRWWebViewProxy>)webViewProxy |
| 497 controller:(CRWWebController*)webController { |
| 498 DCHECK([webViewProxy scrollViewProxy]); |
| 499 _initialHeaderInset = 0; |
| 500 _initialHeaderHeight = 0; |
| 501 _webViewProxy.reset([webViewProxy retain]); |
| 502 [_webViewScrollViewProxy removeObserver:self]; |
| 503 _webViewScrollViewProxy.reset([[webViewProxy scrollViewProxy] retain]); |
| 504 [_webViewScrollViewProxy addObserver:self]; |
| 505 [self enableOverscrollActions]; |
| 506 } |
| 507 |
| 508 - (void)webControllerWillClose:(CRWWebController*)webController { |
| 509 [self disableOverscrollActions]; |
| 510 [_webViewScrollViewProxy removeObserver:self]; |
| 511 _webViewScrollViewProxy.reset(); |
| 512 [webController removeObserver:self]; |
| 513 } |
| 514 |
| 515 #pragma mark - Private |
| 516 |
| 517 - (void)recordMetricForTriggeredAction:(ios_internal::OverscrollAction)action { |
| 518 switch (action) { |
| 519 case ios_internal::OverscrollAction::NONE: |
| 520 UMA_HISTOGRAM_ENUMERATION(kOverscrollActionsHistogram, |
| 521 OVERSCROLL_ACTION_CANCELED, |
| 522 OVERSCROLL_ACTION_COUNT); |
| 523 break; |
| 524 case ios_internal::OverscrollAction::NEW_TAB: |
| 525 UMA_HISTOGRAM_ENUMERATION(kOverscrollActionsHistogram, |
| 526 OVERSCROLL_ACTION_NEW_TAB, |
| 527 OVERSCROLL_ACTION_COUNT); |
| 528 break; |
| 529 case ios_internal::OverscrollAction::REFRESH: |
| 530 UMA_HISTOGRAM_ENUMERATION(kOverscrollActionsHistogram, |
| 531 OVERSCROLL_ACTION_REFRESH, |
| 532 OVERSCROLL_ACTION_COUNT); |
| 533 break; |
| 534 case ios_internal::OverscrollAction::CLOSE_TAB: |
| 535 UMA_HISTOGRAM_ENUMERATION(kOverscrollActionsHistogram, |
| 536 OVERSCROLL_ACTION_CLOSE_TAB, |
| 537 OVERSCROLL_ACTION_COUNT); |
| 538 break; |
| 539 } |
| 540 } |
| 541 |
| 542 - (void)registerNotifications { |
| 543 NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; |
| 544 for (NSString* counterpartNotificationName in _lockNotificationsCounterparts |
| 545 .get()) { |
| 546 [center addObserver:self |
| 547 selector:@selector(incrementOverscrollActionLockForNotification:) |
| 548 name:[_lockNotificationsCounterparts |
| 549 objectForKey:counterpartNotificationName] |
| 550 object:nil]; |
| 551 [center addObserver:self |
| 552 selector:@selector(decrementOverscrollActionLockForNotification:) |
| 553 name:counterpartNotificationName |
| 554 object:nil]; |
| 555 } |
| 556 [center addObserver:self |
| 557 selector:@selector(deviceOrientationDidChange) |
| 558 name:UIDeviceOrientationDidChangeNotification |
| 559 object:nil]; |
| 560 } |
| 561 |
| 562 - (void)tearDown { |
| 563 [[self scrollView] removeGestureRecognizer:self.panGestureRecognizer]; |
| 564 self.panGestureRecognizer = nil; |
| 565 } |
| 566 |
| 567 - (void)setup { |
| 568 base::scoped_nsobject<UIPanGestureRecognizer> panGesture( |
| 569 [[UIPanGestureRecognizer alloc] initWithTarget:self |
| 570 action:@selector(panGesture:)]); |
| 571 [panGesture setMaximumNumberOfTouches:1]; |
| 572 [panGesture setDelegate:self]; |
| 573 [[self scrollView] addGestureRecognizer:panGesture]; |
| 574 self.panGestureRecognizer = panGesture.get(); |
| 575 } |
| 576 |
| 577 - (id<OverscrollActionsScrollView>)scrollView { |
| 578 if (_scrollview) { |
| 579 return static_cast<id<OverscrollActionsScrollView>>(_scrollview.get()); |
| 580 } else { |
| 581 return static_cast<id<OverscrollActionsScrollView>>( |
| 582 _webViewScrollViewProxy.get()); |
| 583 } |
| 584 } |
| 585 |
| 586 - (void)setScrollViewContentInset:(UIEdgeInsets)contentInset { |
| 587 if (_scrollview) |
| 588 [_scrollview setContentInset:contentInset]; |
| 589 else |
| 590 [_webViewScrollViewProxy setContentInsetFast:contentInset]; |
| 591 } |
| 592 |
| 593 - (UIView<RelaxedBoundsConstraintsHitTestSupport>*)headerView { |
| 594 return [self.delegate headerView]; |
| 595 } |
| 596 |
| 597 - (void)incrementOverscrollActionLockForNotification:(NSNotification*)notif { |
| 598 if (![_lockIncrementNotifications containsObject:notif.name]) { |
| 599 [_lockIncrementNotifications addObject:notif.name]; |
| 600 ++_overscrollActionLock; |
| 601 } |
| 602 } |
| 603 |
| 604 - (void)decrementOverscrollActionLockForNotification:(NSNotification*)notif { |
| 605 NSString* counterpartName = |
| 606 [_lockNotificationsCounterparts objectForKey:notif.name]; |
| 607 if ([_lockIncrementNotifications containsObject:counterpartName]) { |
| 608 [_lockIncrementNotifications removeObject:counterpartName]; |
| 609 if (_overscrollActionLock > 0) |
| 610 --_overscrollActionLock; |
| 611 } |
| 612 } |
| 613 |
| 614 - (void)deviceOrientationDidChange { |
| 615 if (self.overscrollState == ios_internal::OverscrollState::NO_PULL_STARTED && |
| 616 !_performingScrollViewIndependentAnimation) |
| 617 return; |
| 618 |
| 619 const UIDeviceOrientation deviceOrientation = |
| 620 [[UIDevice currentDevice] orientation]; |
| 621 if (deviceOrientation != UIDeviceOrientationLandscapeRight && |
| 622 deviceOrientation != UIDeviceOrientationLandscapeLeft && |
| 623 deviceOrientation != UIDeviceOrientationPortrait) { |
| 624 return; |
| 625 } |
| 626 |
| 627 // If the orientation change happen while the user is still scrolling the |
| 628 // scrollview, we need to reset the pan gesture recognizer. |
| 629 // Not doing so would result in a graphic issue where the scrollview jumps |
| 630 // when scrolling after a change in UI orientation. |
| 631 [[self scrollView] panGestureRecognizer].enabled = NO; |
| 632 [[self scrollView] panGestureRecognizer].enabled = YES; |
| 633 |
| 634 [self setScrollViewContentInset:UIEdgeInsetsMake(self.initialContentInset, 0, |
| 635 0, 0)]; |
| 636 [self clear]; |
| 637 } |
| 638 |
| 639 - (BOOL)isOverscrollActionEnabled { |
| 640 return _overscrollActionLock == 0 && _allowPullingActions && |
| 641 !_isOverscrollActionsDisabledForLoading; |
| 642 } |
| 643 |
| 644 - (void)triggerActionIfNeeded { |
| 645 if ([self isOverscrollActionEnabled]) { |
| 646 const BOOL isOverscrollStateActionReady = |
| 647 self.overscrollState == ios_internal::OverscrollState::ACTION_READY; |
| 648 const BOOL isOverscrollActionNone = |
| 649 self.overscrollActionView.selectedAction == |
| 650 ios_internal::OverscrollAction::NONE; |
| 651 |
| 652 if ((!isOverscrollStateActionReady && _didTransitionToActionReady) || |
| 653 (isOverscrollStateActionReady && isOverscrollActionNone)) { |
| 654 [self |
| 655 recordMetricForTriggeredAction:ios_internal::OverscrollAction::NONE]; |
| 656 } else if (isOverscrollStateActionReady && !isOverscrollActionNone) { |
| 657 if (CACurrentMediaTime() - _lastScrollBeginTime >= |
| 658 kMinimumPullDurationToTriggerActionInSeconds) { |
| 659 _performingScrollViewIndependentAnimation = YES; |
| 660 [self setScrollViewContentInset:UIEdgeInsetsMake( |
| 661 self.initialContentInset, 0, 0, 0)]; |
| 662 CGPoint contentOffset = [[self scrollView] contentOffset]; |
| 663 contentOffset.y = -self.initialContentInset; |
| 664 [[self scrollView] setContentOffset:contentOffset animated:YES]; |
| 665 [self.overscrollActionView displayActionAnimation]; |
| 666 dispatch_async(dispatch_get_main_queue(), ^{ |
| 667 [self recordMetricForTriggeredAction:self.overscrollActionView |
| 668 .selectedAction]; |
| 669 [self.delegate overscrollActionsController:self |
| 670 didTriggerAction:self.overscrollActionView |
| 671 .selectedAction]; |
| 672 }); |
| 673 } |
| 674 } |
| 675 } |
| 676 } |
| 677 |
| 678 - (void)setOverscrollState:(ios_internal::OverscrollState)overscrollState { |
| 679 if (_overscrollState != overscrollState) { |
| 680 const ios_internal::OverscrollState previousState = _overscrollState; |
| 681 _overscrollState = overscrollState; |
| 682 [self onOverscrollStateChangeWithPreviousState:previousState]; |
| 683 } |
| 684 } |
| 685 |
| 686 - (void)onOverscrollStateChangeWithPreviousState: |
| 687 (ios_internal::OverscrollState)previousOverscrollState { |
| 688 [UIView beginAnimations:@"backgroundColor" context:NULL]; |
| 689 switch (self.overscrollState) { |
| 690 case ios_internal::OverscrollState::NO_PULL_STARTED: { |
| 691 UIView<RelaxedBoundsConstraintsHitTestSupport>* headerView = |
| 692 [self headerView]; |
| 693 if ([headerView |
| 694 respondsToSelector:@selector(setHitTestBoundsContraintRelaxed:)]) |
| 695 [headerView setHitTestBoundsContraintRelaxed:NO]; |
| 696 [self.overscrollActionView removeFromSuperview]; |
| 697 SetViewFrameHeight( |
| 698 self.overscrollActionView, |
| 699 self.initialContentInset + |
| 700 [UIApplication sharedApplication].statusBarFrame.size.height); |
| 701 self.panPointScreenOrigin = CGPointZero; |
| 702 [[NSNotificationCenter defaultCenter] |
| 703 postNotificationName:ios_internal::kOverscollActionsDidEnd |
| 704 object:self]; |
| 705 } break; |
| 706 case ios_internal::OverscrollState::STARTED_PULLING: { |
| 707 if (!self.overscrollActionView.superview) { |
| 708 if (previousOverscrollState == |
| 709 ios_internal::OverscrollState::NO_PULL_STARTED) { |
| 710 UIView* view = [self.delegate toolbarSnapshotView]; |
| 711 [self.overscrollActionView addSnapshotView:view]; |
| 712 [[NSNotificationCenter defaultCenter] |
| 713 postNotificationName:ios_internal::kOverscollActionsWillStart |
| 714 object:self]; |
| 715 } |
| 716 [CATransaction begin]; |
| 717 [CATransaction setDisableActions:YES]; |
| 718 self.overscrollActionView.backgroundView.alpha = 1; |
| 719 [self.overscrollActionView updateWithVerticalOffset:0]; |
| 720 [self.overscrollActionView updateWithHorizontalOffset:0]; |
| 721 self.overscrollActionView.frame = [self headerView].bounds; |
| 722 DCHECK([self headerView]); |
| 723 UIView<RelaxedBoundsConstraintsHitTestSupport>* headerView = |
| 724 [self headerView]; |
| 725 if ([headerView |
| 726 respondsToSelector:@selector( |
| 727 setHitTestBoundsContraintRelaxed:)]) |
| 728 [headerView setHitTestBoundsContraintRelaxed:YES]; |
| 729 [headerView addSubview:self.overscrollActionView]; |
| 730 [CATransaction commit]; |
| 731 } |
| 732 } break; |
| 733 case ios_internal::OverscrollState::ACTION_READY: { |
| 734 _didTransitionToActionReady = YES; |
| 735 if (CGPointEqualToPoint(self.panPointScreenOrigin, CGPointZero)) { |
| 736 CGPoint panPointScreen = [self.panGestureRecognizer locationInView:nil]; |
| 737 self.panPointScreenOrigin = panPointScreen; |
| 738 } |
| 739 } break; |
| 740 } |
| 741 [UIView commitAnimations]; |
| 742 } |
| 743 |
| 744 - (void)setWebViewInteractionEnabled:(BOOL)enabled { |
| 745 // All interactions are disabled except pan. |
| 746 for (UIGestureRecognizer* gesture in [_webViewProxy gestureRecognizers]) { |
| 747 [gesture setEnabled:enabled]; |
| 748 } |
| 749 for (UIGestureRecognizer* gesture in |
| 750 [_webViewScrollViewProxy gestureRecognizers]) { |
| 751 if (![gesture isKindOfClass:[UIPanGestureRecognizer class]]) { |
| 752 [gesture setEnabled:enabled]; |
| 753 } |
| 754 } |
| 755 // Add a dummy view on top of the webview in order to catch touches on some |
| 756 // specific subviews. |
| 757 if (!enabled) { |
| 758 if (!_dummyView) |
| 759 _dummyView.reset([[UIView alloc] init]); |
| 760 [_dummyView setFrame:[_webViewProxy bounds]]; |
| 761 [_webViewProxy addSubview:_dummyView]; |
| 762 } else { |
| 763 [_dummyView removeFromSuperview]; |
| 764 } |
| 765 } |
| 766 |
| 767 - (void)updateWithVerticalOffset:(CGFloat)verticalOffset { |
| 768 self.overscrollActionView.backgroundView.alpha = |
| 769 1.0 - |
| 770 Clamp(verticalOffset / (kHeaderMaxExpansionThreshold / 2.0), 0.0, 1.0); |
| 771 SetViewFrameHeight(self.overscrollActionView, |
| 772 self.initialHeaderHeight + verticalOffset); |
| 773 [self.overscrollActionView updateWithVerticalOffset:verticalOffset]; |
| 774 } |
| 775 |
| 776 - (CGFloat)initialContentInset { |
| 777 // Content inset is not used for displaying header if the web view's |
| 778 // |shouldUseInsetForTopPadding| is NO, instead the whole web view |
| 779 // frame is changed. |
| 780 if (!_scrollview && ![_webViewProxy shouldUseInsetForTopPadding]) |
| 781 return 0; |
| 782 return self.initialHeaderInset; |
| 783 } |
| 784 |
| 785 - (CGFloat)initialHeaderInset { |
| 786 if (_initialHeaderInset == 0) { |
| 787 _initialHeaderInset = |
| 788 [[self delegate] overscrollActionsControllerHeaderInset:self]; |
| 789 } |
| 790 return _initialHeaderInset; |
| 791 } |
| 792 |
| 793 - (CGFloat)initialHeaderHeight { |
| 794 if (_initialHeaderHeight == 0) { |
| 795 _initialHeaderHeight = [[self delegate] overscrollHeaderHeight]; |
| 796 } |
| 797 return _initialHeaderHeight; |
| 798 } |
| 799 |
| 800 #pragma mark - Bounce dynamic |
| 801 |
| 802 - (void)startBounceWithInitialVelocity:(CGPoint)velocity { |
| 803 [self stopBounce]; |
| 804 CADisplayLink* dpLink = |
| 805 [CADisplayLink displayLinkWithTarget:self |
| 806 selector:@selector(updateBounce)]; |
| 807 [dpLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; |
| 808 _dpLink = dpLink; |
| 809 memset(&_bounceState, 0, sizeof(_bounceState)); |
| 810 if (self.overscrollState == ios_internal::OverscrollState::ACTION_READY) { |
| 811 const UIEdgeInsets insets = UIEdgeInsetsMake( |
| 812 -[self scrollView].contentOffset.y + self.initialContentInset, 0, 0, 0); |
| 813 [self setScrollViewContentInset:insets]; |
| 814 [[self scrollView] setScrollIndicatorInsets:insets]; |
| 815 } |
| 816 _bounceState.yInset = [self scrollView].contentInset.top; |
| 817 _bounceState.initialYInset = _bounceState.yInset; |
| 818 _bounceState.headerInset = self.initialContentInset; |
| 819 _bounceState.time = CACurrentMediaTime(); |
| 820 _bounceState.velocityInset = -velocity.y * 1000.0; |
| 821 } |
| 822 |
| 823 - (void)stopBounce { |
| 824 [_dpLink invalidate]; |
| 825 _dpLink = nil; |
| 826 if (_performingScrollViewIndependentAnimation) { |
| 827 self.overscrollState = ios_internal::OverscrollState::NO_PULL_STARTED; |
| 828 _performingScrollViewIndependentAnimation = NO; |
| 829 } |
| 830 } |
| 831 |
| 832 - (void)updateBounce { |
| 833 const double time = CACurrentMediaTime(); |
| 834 const double dt = time - _bounceState.time; |
| 835 CGFloat force = -_bounceState.yInset * kSpringTightness; |
| 836 if (_bounceState.yInset > _bounceState.headerInset) |
| 837 force -= _bounceState.velocityInset * kSpringDampiness; |
| 838 _bounceState.velocityInset += force; |
| 839 _bounceState.yInset += _bounceState.velocityInset * dt; |
| 840 _bounceState.time = time; |
| 841 [self applyBounceState]; |
| 842 if (fabs(_bounceState.yInset - _bounceState.headerInset) < 0.5) |
| 843 [self stopBounce]; |
| 844 } |
| 845 |
| 846 - (void)applyBounceState { |
| 847 if (_bounceState.yInset - _bounceState.headerInset < 0.5) |
| 848 _bounceState.yInset = _bounceState.headerInset; |
| 849 if (_performingScrollViewIndependentAnimation) { |
| 850 [self updateWithVerticalOffset:_bounceState.yInset - |
| 851 _bounceState.headerInset]; |
| 852 } else { |
| 853 const UIEdgeInsets insets = UIEdgeInsetsMake(_bounceState.yInset, 0, 0, 0); |
| 854 _forceStateUpdate = YES; |
| 855 [self setScrollViewContentInset:insets]; |
| 856 [self scrollView].scrollIndicatorInsets = insets; |
| 857 _forceStateUpdate = NO; |
| 858 } |
| 859 } |
| 860 |
| 861 #pragma mark - OverscrollActionsViewDelegate |
| 862 |
| 863 - (void)overscrollActionsViewDidTapTriggerAction: |
| 864 (OverscrollActionsView*)overscrollActionsView { |
| 865 [self.overscrollActionView displayActionAnimation]; |
| 866 [self |
| 867 recordMetricForTriggeredAction:self.overscrollActionView.selectedAction]; |
| 868 |
| 869 // Reset all pan gesture recognizers. |
| 870 _allowPullingActions = NO; |
| 871 _panGestureRecognizer.enabled = NO; |
| 872 _panGestureRecognizer.enabled = YES; |
| 873 [self scrollView].panGestureRecognizer.enabled = NO; |
| 874 [self scrollView].panGestureRecognizer.enabled = YES; |
| 875 [self startBounceWithInitialVelocity:CGPointZero]; |
| 876 [self.delegate |
| 877 overscrollActionsController:self |
| 878 didTriggerAction:self.overscrollActionView.selectedAction]; |
| 879 } |
| 880 |
| 881 @end |
OLD | NEW |