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

Side by Side Diff: ios/chrome/browser/ui/side_swipe/side_swipe_controller.mm

Issue 2587023002: Upstream Chrome on iOS source code [8/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/side_swipe/side_swipe_controller.h"
6
7 #include <memory>
8
9 #include "components/reading_list/core/reading_list_switches.h"
10 #import "components/reading_list/ios/reading_list_model.h"
11 #import "ios/chrome/browser/browser_state/chrome_browser_state.h"
12 #import "ios/chrome/browser/infobars/infobar_container_view.h"
13 #import "ios/chrome/browser/reading_list/reading_list_model_factory.h"
14 #import "ios/chrome/browser/snapshots/snapshot_cache.h"
15 #import "ios/chrome/browser/tabs/tab.h"
16 #import "ios/chrome/browser/tabs/tab_model_observer.h"
17 #import "ios/chrome/browser/ui/reading_list/reading_list_side_swipe_provider.h"
18 #import "ios/chrome/browser/ui/side_swipe/card_side_swipe_view.h"
19 #import "ios/chrome/browser/ui/side_swipe/history_side_swipe_provider.h"
20 #import "ios/chrome/browser/ui/side_swipe/side_swipe_navigation_view.h"
21 #import "ios/chrome/browser/ui/side_swipe/side_swipe_util.h"
22 #import "ios/chrome/browser/ui/side_swipe_gesture_recognizer.h"
23 #include "ios/chrome/browser/ui/ui_util.h"
24 #import "ios/web/public/web_state/web_state_observer_bridge.h"
25 #import "ios/web/web_state/ui/crw_web_controller.h"
26
27 namespace ios_internal {
28 NSString* const kSideSwipeWillStartNotification =
29 @"kSideSwipeWillStartNotification";
30 NSString* const kSideSwipeDidStopNotification =
31 @"kSideSwipeDidStopNotification";
32 } // namespace ios_internal
33
34 namespace {
35
36 enum class SwipeType { NONE, CHANGE_TAB, CHANGE_PAGE };
37
38 // Swipe starting distance from edge.
39 const CGFloat kSwipeEdge = 20;
40
41 // Distance between sections of iPad side swipe.
42 const CGFloat kIpadTabSwipeDistance = 100;
43
44 // Number of tabs to keep in the grey image cache.
45 const NSUInteger kIpadGreySwipeTabCount = 8;
46 }
47
48 @interface SideSwipeController ()<CRWWebStateObserver,
49 TabModelObserver,
50 UIGestureRecognizerDelegate> {
51 @private
52
53 TabModel* model_;
54
55 // Side swipe view for tab navigation.
56 base::scoped_nsobject<CardSideSwipeView> tabSideSwipeView_;
57
58 // Side swipe view for page navigation.
59 base::scoped_nsobject<SideSwipeNavigationView> pageSideSwipeView_;
60
61 // YES if the user is currently swiping.
62 BOOL inSwipe_;
63
64 // Swipe gesture recognizer.
65 base::scoped_nsobject<SideSwipeGestureRecognizer> swipeGestureRecognizer_;
66
67 base::scoped_nsobject<SideSwipeGestureRecognizer> panGestureRecognizer_;
68
69 // Used in iPad side swipe gesture, tracks the starting tab index.
70 NSUInteger startingTabIndex_;
71
72 // If the swipe is for a page change or a tab change.
73 SwipeType swipeType_;
74
75 // Bridge to observe the web state from Objective-C.
76 std::unique_ptr<web::WebStateObserverBridge> webStateObserverBridge_;
77
78 // Curtain over web view while waiting for it to load.
79 base::scoped_nsobject<UIView> curtain_;
80
81 // Provides forward/back action for history entries.
82 base::scoped_nsobject<HistorySideSwipeProvider> historySideSwipeProvider_;
83
84 // Provides forward action for reading list.
85 base::scoped_nsobject<ReadingListSideSwipeProvider>
86 readingListSideSwipeProvider_;
87
88 base::WeakNSProtocol<id<SideSwipeContentProvider>> currentContentProvider_;
89 }
90
91 // Load grey snapshots for the next |kIpadGreySwipeTabCount| tabs in
92 // |direction|.
93 - (void)createGreyCache:(UISwipeGestureRecognizerDirection)direction;
94 // Tell snapshot cache to clear grey cache.
95 - (void)deleteGreyCache;
96 // Handle tab side swipe for iPad. Change tabs according to swipe distance.
97 - (void)handleiPadTabSwipe:(SideSwipeGestureRecognizer*)gesture;
98 // Handle tab side swipe for iPhone. Introduces a CardSideSwipeView to convey
99 // the tab change.
100 - (void)handleiPhoneTabSwipe:(SideSwipeGestureRecognizer*)gesture;
101 // Overlays |curtain_| as a white view to hide the web view while it updates.
102 // Calls |completionHandler| when the curtain is removed.
103 - (void)addCurtainWithCompletionHandler:(ProceduralBlock)completionHandler;
104 // Removes the |curtain_| and calls |completionHandler| when the curtain is
105 // removed.
106 - (void)dismissCurtainWithCompletionHandler:(ProceduralBlock)completionHandler;
107 @end
108
109 @implementation SideSwipeController
110
111 @synthesize inSwipe = inSwipe_;
112 @synthesize swipeDelegate = swipeDelegate_;
113 @synthesize snapshotDelegate = snapshotDelegate_;
114
115 - (id)initWithTabModel:(TabModel*)model
116 browserState:(ios::ChromeBrowserState*)browserState {
117 DCHECK(model);
118 self = [super init];
119 if (self) {
120 model_ = model;
121 [model_ addObserver:self];
122 historySideSwipeProvider_.reset(
123 [[HistorySideSwipeProvider alloc] initWithTabModel:model_]);
124
125 if (!browserState->IsOffTheRecord() &&
126 reading_list::switches::IsReadingListEnabled()) {
127 readingListSideSwipeProvider_.reset([[ReadingListSideSwipeProvider alloc]
128 initWithReadingList:ReadingListModelFactory::GetForBrowserState(
129 browserState)]);
130 }
131 }
132 return self;
133 }
134
135 - (void)dealloc {
136 [model_ removeObserver:self];
137 [super dealloc];
138 }
139
140 - (void)addHorizontalGesturesToView:(UIView*)view {
141 swipeGestureRecognizer_.reset([[SideSwipeGestureRecognizer alloc]
142 initWithTarget:self
143 action:@selector(handleSwipe:)]);
144 [swipeGestureRecognizer_ setMaximumNumberOfTouches:1];
145 [swipeGestureRecognizer_ setDelegate:self];
146 [swipeGestureRecognizer_ setSwipeEdge:kSwipeEdge];
147 [view addGestureRecognizer:swipeGestureRecognizer_];
148
149 // Add a second gesture recognizer to handle swiping on the toolbar to change
150 // tabs.
151 panGestureRecognizer_.reset([[SideSwipeGestureRecognizer alloc]
152 initWithTarget:self
153 action:@selector(handlePan:)]);
154 [panGestureRecognizer_ setMaximumNumberOfTouches:1];
155 [panGestureRecognizer_ setSwipeThreshold:48];
156 [panGestureRecognizer_ setDelegate:self];
157 [view addGestureRecognizer:panGestureRecognizer_];
158 }
159
160 - (NSSet*)swipeRecognizers {
161 return [NSSet setWithObjects:swipeGestureRecognizer_.get(), nil];
162 }
163
164 - (void)setEnabled:(BOOL)enabled {
165 [swipeGestureRecognizer_ setEnabled:enabled];
166 }
167
168 - (BOOL)shouldAutorotate {
169 return !([tabSideSwipeView_ window] || inSwipe_);
170 }
171
172 // Always return yes, as this swipe should work with various recognizers,
173 // including UITextTapRecognizer, UILongPressGestureRecognizer,
174 // UIScrollViewPanGestureRecognizer and others.
175 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
176 shouldRecognizeSimultaneouslyWithGestureRecognizer:
177 (UIGestureRecognizer*)otherGestureRecognizer {
178 return YES;
179 }
180
181 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
182 shouldBeRequiredToFailByGestureRecognizer:
183 (UIGestureRecognizer*)otherGestureRecognizer {
184 // Only take precedence over a pan gesture recognizer so that moving up and
185 // down while swiping doesn't trigger overscroll actions.
186 if ([otherGestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
187 return YES;
188 }
189 return NO;
190 }
191
192 // Gestures should only be recognized within |contentArea_| or the toolbar.
193 - (BOOL)gestureRecognizerShouldBegin:(SideSwipeGestureRecognizer*)gesture {
194 if (inSwipe_) {
195 return NO;
196 }
197
198 if ([swipeDelegate_ preventSideSwipe])
199 return NO;
200
201 CGPoint location = [gesture locationInView:gesture.view];
202
203 // Both the toolbar frame and the contentView frame below are inset by
204 // -1 because CGRectContainsPoint does include points on the max X and Y
205 // edges, which will happen frequently with edge swipes from the right side.
206 // Since the toolbar and the contentView can overlap, check the toolbar frame
207 // first, and confirm the right gesture recognizer is firing.
208 CGRect toolbarFrame =
209 CGRectInset([[[swipeDelegate_ toolbarController] view] frame], -1, -1);
210 if (CGRectContainsPoint(toolbarFrame, location)) {
211 if (![gesture isEqual:panGestureRecognizer_]) {
212 return NO;
213 }
214
215 if ([[swipeDelegate_ toolbarController] isOmniboxFirstResponder] ||
216 [[swipeDelegate_ toolbarController] showingOmniboxPopup]) {
217 return NO;
218 }
219 return YES;
220 }
221
222 // Otherwise, only allow contentView touches with |swipeGestureRecognizer_|.
223 CGRect contentViewFrame =
224 CGRectInset([[swipeDelegate_ contentView] frame], -1, -1);
225 if (CGRectContainsPoint(contentViewFrame, location)) {
226 if (![gesture isEqual:swipeGestureRecognizer_]) {
227 return NO;
228 }
229 swipeType_ = SwipeType::CHANGE_PAGE;
230 return YES;
231 }
232 return NO;
233 }
234
235 - (void)createGreyCache:(UISwipeGestureRecognizerDirection)direction {
236 NSInteger dx = (direction == UISwipeGestureRecognizerDirectionLeft) ? -1 : 1;
237 NSInteger index = startingTabIndex_ + dx;
238 NSMutableArray* sessionIDs =
239 [NSMutableArray arrayWithCapacity:kIpadGreySwipeTabCount];
240 for (NSUInteger count = 0; count < kIpadGreySwipeTabCount; count++) {
241 // Wrap around edges.
242 if (index >= (NSInteger)[model_ count])
243 index = 0;
244 else if (index < 0)
245 index = [model_ count] - 1;
246
247 // Don't wrap past the starting index.
248 if (index == (NSInteger)startingTabIndex_)
249 break;
250
251 Tab* tab = [model_ tabAtIndex:index];
252 if (tab && tab.webController.usePlaceholderOverlay) {
253 [sessionIDs addObject:[tab currentSessionID]];
254 }
255 index = index + dx;
256 }
257 [[SnapshotCache sharedInstance] createGreyCache:sessionIDs];
258 for (Tab* tab in model_) {
259 tab.useGreyImageCache = YES;
260 }
261 }
262
263 - (void)deleteGreyCache {
264 [[SnapshotCache sharedInstance] removeGreyCache];
265 for (Tab* tab in model_) {
266 tab.useGreyImageCache = NO;
267 }
268 }
269
270 - (void)handlePan:(SideSwipeGestureRecognizer*)gesture {
271 if (!IsIPadIdiom()) {
272 return [self handleiPhoneTabSwipe:gesture];
273 } else {
274 return [self handleiPadTabSwipe:gesture];
275 }
276 }
277
278 - (void)handleSwipe:(SideSwipeGestureRecognizer*)gesture {
279 DCHECK(swipeType_ != SwipeType::NONE);
280 if (swipeType_ == SwipeType::CHANGE_TAB) {
281 if (!IsIPadIdiom()) {
282 return [self handleiPhoneTabSwipe:gesture];
283 } else {
284 return [self handleiPadTabSwipe:gesture];
285 }
286 }
287 if (swipeType_ == SwipeType::CHANGE_PAGE) {
288 return [self handleSwipeToNavigate:gesture];
289 }
290 NOTREACHED();
291 }
292
293 - (void)handleiPadTabSwipe:(SideSwipeGestureRecognizer*)gesture {
294 // Don't handle swipe when there are no tabs.
295 NSInteger count = [model_ count];
296 if (count == 0)
297 return;
298
299 if (gesture.state == UIGestureRecognizerStateBegan) {
300 // If the toolbar is hidden, move it to visible.
301 [[model_ currentTab] updateFullscreenWithToolbarVisible:YES];
302 [[model_ currentTab] updateSnapshotWithOverlay:YES visibleFrameOnly:YES];
303 [[NSNotificationCenter defaultCenter]
304 postNotificationName:ios_internal::kSideSwipeWillStartNotification
305 object:nil];
306 [[swipeDelegate_ tabStripController] setHighlightsSelectedTab:YES];
307 startingTabIndex_ = [model_ indexOfTab:[model_ currentTab]];
308 [self createGreyCache:gesture.direction];
309 } else if (gesture.state == UIGestureRecognizerStateChanged) {
310 // Side swipe for iPad involves changing the selected tab as the swipe moves
311 // across the width of the view. The screen is broken up into
312 // |kIpadTabSwipeDistance| / |width| segments, with the current tab in the
313 // first section. The swipe does not wrap edges.
314 CGFloat distance = [gesture locationInView:gesture.view].x;
315 if (gesture.direction == UISwipeGestureRecognizerDirectionLeft) {
316 distance = gesture.startPoint.x - distance;
317 } else {
318 distance -= gesture.startPoint.x;
319 }
320
321 NSInteger indexDelta = std::floor(distance / kIpadTabSwipeDistance);
322 // Don't wrap past the first tab.
323 if (indexDelta < count) {
324 // Flip delta when swiping forward.
325 if (IsSwipingForward(gesture.direction))
326 indexDelta = 0 - indexDelta;
327
328 Tab* currentTab = [model_ currentTab];
329 NSInteger currentIndex = [model_ indexOfTab:currentTab];
330
331 // Wrap around edges.
332 NSInteger newIndex = (NSInteger)(startingTabIndex_ + indexDelta) % count;
333
334 // C99 defines the modulo result as negative if our offset is negative.
335 if (newIndex < 0)
336 newIndex += count;
337
338 if (newIndex != currentIndex) {
339 Tab* tab = [model_ tabAtIndex:newIndex];
340 // Toggle overlay preview mode for selected tab.
341 [tab.webController setOverlayPreviewMode:YES];
342 [model_ setCurrentTab:tab];
343 // And disable overlay preview mode for last selected tab.
344 [currentTab.webController setOverlayPreviewMode:NO];
345 }
346 }
347 } else {
348 if (gesture.state == UIGestureRecognizerStateCancelled) {
349 Tab* tab = [model_ tabAtIndex:startingTabIndex_];
350 [[model_ currentTab].webController setOverlayPreviewMode:NO];
351 [model_ setCurrentTab:tab];
352 }
353 [[model_ currentTab].webController setOverlayPreviewMode:NO];
354
355 // Redisplay the view if it was in overlay preview mode.
356 [swipeDelegate_ displayTab:[model_ currentTab] isNewSelection:YES];
357 [[swipeDelegate_ tabStripController] setHighlightsSelectedTab:NO];
358 [self deleteGreyCache];
359 [[NSNotificationCenter defaultCenter]
360 postNotificationName:ios_internal::kSideSwipeDidStopNotification
361 object:nil];
362 }
363 }
364
365 - (id<SideSwipeContentProvider>)contentProviderForGesture:(BOOL)goBack {
366 if (goBack && [historySideSwipeProvider_ canGoBack]) {
367 return historySideSwipeProvider_;
368 }
369 if (!goBack && [historySideSwipeProvider_ canGoForward]) {
370 return historySideSwipeProvider_;
371 }
372 if (goBack && [readingListSideSwipeProvider_ canGoBack]) {
373 return readingListSideSwipeProvider_;
374 }
375 if (!goBack && [readingListSideSwipeProvider_ canGoForward]) {
376 return readingListSideSwipeProvider_;
377 }
378 return nil;
379 }
380
381 // Show swipe to navigate.
382 - (void)handleSwipeToNavigate:(SideSwipeGestureRecognizer*)gesture {
383 if (gesture.state == UIGestureRecognizerStateBegan) {
384 // If the toolbar is hidden, move it to visible.
385 [[model_ currentTab] updateFullscreenWithToolbarVisible:YES];
386
387 inSwipe_ = YES;
388 [swipeDelegate_ updateAccessoryViewsForSideSwipeWithVisibility:NO];
389 BOOL goBack = IsSwipingBack(gesture.direction);
390
391 currentContentProvider_.reset([self contentProviderForGesture:goBack]);
392 BOOL canNavigate = currentContentProvider_ != nil;
393
394 CGRect gestureBounds = gesture.view.bounds;
395 CGFloat headerHeight = [swipeDelegate_ headerHeight];
396 CGRect navigationFrame =
397 CGRectMake(CGRectGetMinX(gestureBounds),
398 CGRectGetMinY(gestureBounds) + headerHeight,
399 CGRectGetWidth(gestureBounds),
400 CGRectGetHeight(gestureBounds) - headerHeight);
401
402 pageSideSwipeView_.reset([[SideSwipeNavigationView alloc]
403 initWithFrame:navigationFrame
404 withDirection:gesture.direction
405 canNavigate:canNavigate
406 image:[currentContentProvider_ paneIcon]
407 rotateForward:[currentContentProvider_ rotateForwardIcon]]);
408 [pageSideSwipeView_ setTargetView:[swipeDelegate_ contentView]];
409
410 [gesture.view insertSubview:pageSideSwipeView_
411 belowSubview:[[swipeDelegate_ toolbarController] view]];
412 }
413
414 base::WeakNSObject<Tab> weakCurrentTab([model_ currentTab]);
415 [pageSideSwipeView_ handleHorizontalPan:gesture
416 onOverThresholdCompletion:^{
417 BOOL wantsBack = IsSwipingBack(gesture.direction);
418 web::WebState* webState = [weakCurrentTab webState];
419 if (wantsBack) {
420 [currentContentProvider_ goBack:webState];
421 } else {
422 [currentContentProvider_ goForward:webState];
423 }
424
425 if (webState && webState->IsLoading()) {
426 webStateObserverBridge_.reset(
427 new web::WebStateObserverBridge(webState, self));
428 [self addCurtainWithCompletionHandler:^{
429 inSwipe_ = NO;
430 }];
431 } else {
432 inSwipe_ = NO;
433 }
434 [swipeDelegate_ updateAccessoryViewsForSideSwipeWithVisibility:YES];
435 }
436 onUnderThresholdCompletion:^{
437 [swipeDelegate_ updateAccessoryViewsForSideSwipeWithVisibility:YES];
438 inSwipe_ = NO;
439 }];
440 }
441
442 // Show horizontal swipe stack view for iPhone.
443 - (void)handleiPhoneTabSwipe:(SideSwipeGestureRecognizer*)gesture {
444 if (gesture.state == UIGestureRecognizerStateBegan) {
445 // If the toolbar is hidden, move it to visible.
446 [[model_ currentTab] updateFullscreenWithToolbarVisible:YES];
447
448 inSwipe_ = YES;
449
450 CGRect frame = [[swipeDelegate_ contentView] frame];
451
452 // Add horizontal stack view controller.
453 CGFloat headerHeight =
454 [self.snapshotDelegate snapshotContentAreaForTab:[model_ currentTab]]
455 .origin.y;
456 if (tabSideSwipeView_) {
457 [tabSideSwipeView_ setFrame:frame];
458 [tabSideSwipeView_ setTopMargin:headerHeight];
459 } else {
460 tabSideSwipeView_.reset([[CardSideSwipeView alloc]
461 initWithFrame:frame
462 topMargin:headerHeight
463 model:model_]);
464 [tabSideSwipeView_ setAutoresizingMask:UIViewAutoresizingFlexibleWidth |
465 UIViewAutoresizingFlexibleHeight];
466 [tabSideSwipeView_ setDelegate:swipeDelegate_];
467 [tabSideSwipeView_ setBackgroundColor:[UIColor blackColor]];
468 }
469
470 // Ensure that there's an up-to-date snapshot of the current tab.
471 [[model_ currentTab] updateSnapshotWithOverlay:YES visibleFrameOnly:YES];
472 // Hide the infobar after snapshot has been updated (see the previous line)
473 // to avoid it obscuring the cards in the side swipe view.
474 [swipeDelegate_ updateAccessoryViewsForSideSwipeWithVisibility:NO];
475
476 // Layout tabs with new snapshots in the current orientation.
477 [tabSideSwipeView_
478 updateViewsForDirection:gesture.direction
479 withToolbar:[swipeDelegate_ toolbarController]];
480
481 // Insert behind infobar container (which is below toolbar)
482 // so card border doesn't look janky during animation.
483 DCHECK([swipeDelegate_ verifyToolbarViewPlacementInView:gesture.view]);
484 // Insert above the toolbar.
485 [gesture.view addSubview:tabSideSwipeView_];
486
487 // Remove content area so it doesn't receive any pan events.
488 [[swipeDelegate_ contentView] removeFromSuperview];
489 }
490
491 [tabSideSwipeView_ handleHorizontalPan:gesture];
492 }
493
494 - (void)addCurtainWithCompletionHandler:(ProceduralBlock)completionHandler {
495 if (!curtain_) {
496 curtain_.reset(
497 [[UIView alloc] initWithFrame:[swipeDelegate_ contentView].bounds]);
498 [curtain_ setBackgroundColor:[UIColor whiteColor]];
499 }
500 [[swipeDelegate_ contentView] addSubview:curtain_];
501
502 // Fallback in case load takes a while. 3 seconds is a balance between how
503 // long it can take a web view to clear the previous page image, and what
504 // feels like to 'too long' to see the curtain.
505 [self performSelector:@selector(dismissCurtainWithCompletionHandler:)
506 withObject:[[completionHandler copy] autorelease]
507 afterDelay:3];
508 }
509
510 - (void)dismissCurtainWithCompletionHandler:(ProceduralBlock)completionHandler {
511 [NSObject cancelPreviousPerformRequestsWithTarget:self];
512 webStateObserverBridge_.reset();
513 [curtain_ removeFromSuperview];
514 curtain_.reset();
515 completionHandler();
516 }
517
518 #pragma mark - CRWWebStateObserver Methods
519
520 - (void)webStateDidStopLoading:(web::WebState*)webState {
521 [self dismissCurtainWithCompletionHandler:^{
522 inSwipe_ = NO;
523 }];
524 }
525
526 #pragma mark - TabModelObserver Methods
527
528 - (void)tabModel:(TabModel*)model
529 didChangeActiveTab:(Tab*)newTab
530 previousTab:(Tab*)previousTab
531 atIndex:(NSUInteger)index {
532 // Toggling the gesture's enabled state off and on will effectively cancel
533 // the gesture recognizer.
534 [swipeGestureRecognizer_ setEnabled:NO];
535 [swipeGestureRecognizer_ setEnabled:YES];
536 }
537
538 @end
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698