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

Side by Side 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 unified diff | Download patch
OLDNEW
(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
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698