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

Unified 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 side-by-side diff with in-line comments
Download patch
Index: ios/chrome/browser/ui/side_swipe/side_swipe_controller.mm
diff --git a/ios/chrome/browser/ui/side_swipe/side_swipe_controller.mm b/ios/chrome/browser/ui/side_swipe/side_swipe_controller.mm
new file mode 100644
index 0000000000000000000000000000000000000000..3d4a3fba5cfd17e6bedd4796003f96a138bd2b37
--- /dev/null
+++ b/ios/chrome/browser/ui/side_swipe/side_swipe_controller.mm
@@ -0,0 +1,538 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ios/chrome/browser/ui/side_swipe/side_swipe_controller.h"
+
+#include <memory>
+
+#include "components/reading_list/core/reading_list_switches.h"
+#import "components/reading_list/ios/reading_list_model.h"
+#import "ios/chrome/browser/browser_state/chrome_browser_state.h"
+#import "ios/chrome/browser/infobars/infobar_container_view.h"
+#import "ios/chrome/browser/reading_list/reading_list_model_factory.h"
+#import "ios/chrome/browser/snapshots/snapshot_cache.h"
+#import "ios/chrome/browser/tabs/tab.h"
+#import "ios/chrome/browser/tabs/tab_model_observer.h"
+#import "ios/chrome/browser/ui/reading_list/reading_list_side_swipe_provider.h"
+#import "ios/chrome/browser/ui/side_swipe/card_side_swipe_view.h"
+#import "ios/chrome/browser/ui/side_swipe/history_side_swipe_provider.h"
+#import "ios/chrome/browser/ui/side_swipe/side_swipe_navigation_view.h"
+#import "ios/chrome/browser/ui/side_swipe/side_swipe_util.h"
+#import "ios/chrome/browser/ui/side_swipe_gesture_recognizer.h"
+#include "ios/chrome/browser/ui/ui_util.h"
+#import "ios/web/public/web_state/web_state_observer_bridge.h"
+#import "ios/web/web_state/ui/crw_web_controller.h"
+
+namespace ios_internal {
+NSString* const kSideSwipeWillStartNotification =
+ @"kSideSwipeWillStartNotification";
+NSString* const kSideSwipeDidStopNotification =
+ @"kSideSwipeDidStopNotification";
+} // namespace ios_internal
+
+namespace {
+
+enum class SwipeType { NONE, CHANGE_TAB, CHANGE_PAGE };
+
+// Swipe starting distance from edge.
+const CGFloat kSwipeEdge = 20;
+
+// Distance between sections of iPad side swipe.
+const CGFloat kIpadTabSwipeDistance = 100;
+
+// Number of tabs to keep in the grey image cache.
+const NSUInteger kIpadGreySwipeTabCount = 8;
+}
+
+@interface SideSwipeController ()<CRWWebStateObserver,
+ TabModelObserver,
+ UIGestureRecognizerDelegate> {
+ @private
+
+ TabModel* model_;
+
+ // Side swipe view for tab navigation.
+ base::scoped_nsobject<CardSideSwipeView> tabSideSwipeView_;
+
+ // Side swipe view for page navigation.
+ base::scoped_nsobject<SideSwipeNavigationView> pageSideSwipeView_;
+
+ // YES if the user is currently swiping.
+ BOOL inSwipe_;
+
+ // Swipe gesture recognizer.
+ base::scoped_nsobject<SideSwipeGestureRecognizer> swipeGestureRecognizer_;
+
+ base::scoped_nsobject<SideSwipeGestureRecognizer> panGestureRecognizer_;
+
+ // Used in iPad side swipe gesture, tracks the starting tab index.
+ NSUInteger startingTabIndex_;
+
+ // If the swipe is for a page change or a tab change.
+ SwipeType swipeType_;
+
+ // Bridge to observe the web state from Objective-C.
+ std::unique_ptr<web::WebStateObserverBridge> webStateObserverBridge_;
+
+ // Curtain over web view while waiting for it to load.
+ base::scoped_nsobject<UIView> curtain_;
+
+ // Provides forward/back action for history entries.
+ base::scoped_nsobject<HistorySideSwipeProvider> historySideSwipeProvider_;
+
+ // Provides forward action for reading list.
+ base::scoped_nsobject<ReadingListSideSwipeProvider>
+ readingListSideSwipeProvider_;
+
+ base::WeakNSProtocol<id<SideSwipeContentProvider>> currentContentProvider_;
+}
+
+// Load grey snapshots for the next |kIpadGreySwipeTabCount| tabs in
+// |direction|.
+- (void)createGreyCache:(UISwipeGestureRecognizerDirection)direction;
+// Tell snapshot cache to clear grey cache.
+- (void)deleteGreyCache;
+// Handle tab side swipe for iPad. Change tabs according to swipe distance.
+- (void)handleiPadTabSwipe:(SideSwipeGestureRecognizer*)gesture;
+// Handle tab side swipe for iPhone. Introduces a CardSideSwipeView to convey
+// the tab change.
+- (void)handleiPhoneTabSwipe:(SideSwipeGestureRecognizer*)gesture;
+// Overlays |curtain_| as a white view to hide the web view while it updates.
+// Calls |completionHandler| when the curtain is removed.
+- (void)addCurtainWithCompletionHandler:(ProceduralBlock)completionHandler;
+// Removes the |curtain_| and calls |completionHandler| when the curtain is
+// removed.
+- (void)dismissCurtainWithCompletionHandler:(ProceduralBlock)completionHandler;
+@end
+
+@implementation SideSwipeController
+
+@synthesize inSwipe = inSwipe_;
+@synthesize swipeDelegate = swipeDelegate_;
+@synthesize snapshotDelegate = snapshotDelegate_;
+
+- (id)initWithTabModel:(TabModel*)model
+ browserState:(ios::ChromeBrowserState*)browserState {
+ DCHECK(model);
+ self = [super init];
+ if (self) {
+ model_ = model;
+ [model_ addObserver:self];
+ historySideSwipeProvider_.reset(
+ [[HistorySideSwipeProvider alloc] initWithTabModel:model_]);
+
+ if (!browserState->IsOffTheRecord() &&
+ reading_list::switches::IsReadingListEnabled()) {
+ readingListSideSwipeProvider_.reset([[ReadingListSideSwipeProvider alloc]
+ initWithReadingList:ReadingListModelFactory::GetForBrowserState(
+ browserState)]);
+ }
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [model_ removeObserver:self];
+ [super dealloc];
+}
+
+- (void)addHorizontalGesturesToView:(UIView*)view {
+ swipeGestureRecognizer_.reset([[SideSwipeGestureRecognizer alloc]
+ initWithTarget:self
+ action:@selector(handleSwipe:)]);
+ [swipeGestureRecognizer_ setMaximumNumberOfTouches:1];
+ [swipeGestureRecognizer_ setDelegate:self];
+ [swipeGestureRecognizer_ setSwipeEdge:kSwipeEdge];
+ [view addGestureRecognizer:swipeGestureRecognizer_];
+
+ // Add a second gesture recognizer to handle swiping on the toolbar to change
+ // tabs.
+ panGestureRecognizer_.reset([[SideSwipeGestureRecognizer alloc]
+ initWithTarget:self
+ action:@selector(handlePan:)]);
+ [panGestureRecognizer_ setMaximumNumberOfTouches:1];
+ [panGestureRecognizer_ setSwipeThreshold:48];
+ [panGestureRecognizer_ setDelegate:self];
+ [view addGestureRecognizer:panGestureRecognizer_];
+}
+
+- (NSSet*)swipeRecognizers {
+ return [NSSet setWithObjects:swipeGestureRecognizer_.get(), nil];
+}
+
+- (void)setEnabled:(BOOL)enabled {
+ [swipeGestureRecognizer_ setEnabled:enabled];
+}
+
+- (BOOL)shouldAutorotate {
+ return !([tabSideSwipeView_ window] || inSwipe_);
+}
+
+// Always return yes, as this swipe should work with various recognizers,
+// including UITextTapRecognizer, UILongPressGestureRecognizer,
+// UIScrollViewPanGestureRecognizer and others.
+- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
+ shouldRecognizeSimultaneouslyWithGestureRecognizer:
+ (UIGestureRecognizer*)otherGestureRecognizer {
+ return YES;
+}
+
+- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
+ shouldBeRequiredToFailByGestureRecognizer:
+ (UIGestureRecognizer*)otherGestureRecognizer {
+ // Only take precedence over a pan gesture recognizer so that moving up and
+ // down while swiping doesn't trigger overscroll actions.
+ if ([otherGestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
+ return YES;
+ }
+ return NO;
+}
+
+// Gestures should only be recognized within |contentArea_| or the toolbar.
+- (BOOL)gestureRecognizerShouldBegin:(SideSwipeGestureRecognizer*)gesture {
+ if (inSwipe_) {
+ return NO;
+ }
+
+ if ([swipeDelegate_ preventSideSwipe])
+ return NO;
+
+ CGPoint location = [gesture locationInView:gesture.view];
+
+ // Both the toolbar frame and the contentView frame below are inset by
+ // -1 because CGRectContainsPoint does include points on the max X and Y
+ // edges, which will happen frequently with edge swipes from the right side.
+ // Since the toolbar and the contentView can overlap, check the toolbar frame
+ // first, and confirm the right gesture recognizer is firing.
+ CGRect toolbarFrame =
+ CGRectInset([[[swipeDelegate_ toolbarController] view] frame], -1, -1);
+ if (CGRectContainsPoint(toolbarFrame, location)) {
+ if (![gesture isEqual:panGestureRecognizer_]) {
+ return NO;
+ }
+
+ if ([[swipeDelegate_ toolbarController] isOmniboxFirstResponder] ||
+ [[swipeDelegate_ toolbarController] showingOmniboxPopup]) {
+ return NO;
+ }
+ return YES;
+ }
+
+ // Otherwise, only allow contentView touches with |swipeGestureRecognizer_|.
+ CGRect contentViewFrame =
+ CGRectInset([[swipeDelegate_ contentView] frame], -1, -1);
+ if (CGRectContainsPoint(contentViewFrame, location)) {
+ if (![gesture isEqual:swipeGestureRecognizer_]) {
+ return NO;
+ }
+ swipeType_ = SwipeType::CHANGE_PAGE;
+ return YES;
+ }
+ return NO;
+}
+
+- (void)createGreyCache:(UISwipeGestureRecognizerDirection)direction {
+ NSInteger dx = (direction == UISwipeGestureRecognizerDirectionLeft) ? -1 : 1;
+ NSInteger index = startingTabIndex_ + dx;
+ NSMutableArray* sessionIDs =
+ [NSMutableArray arrayWithCapacity:kIpadGreySwipeTabCount];
+ for (NSUInteger count = 0; count < kIpadGreySwipeTabCount; count++) {
+ // Wrap around edges.
+ if (index >= (NSInteger)[model_ count])
+ index = 0;
+ else if (index < 0)
+ index = [model_ count] - 1;
+
+ // Don't wrap past the starting index.
+ if (index == (NSInteger)startingTabIndex_)
+ break;
+
+ Tab* tab = [model_ tabAtIndex:index];
+ if (tab && tab.webController.usePlaceholderOverlay) {
+ [sessionIDs addObject:[tab currentSessionID]];
+ }
+ index = index + dx;
+ }
+ [[SnapshotCache sharedInstance] createGreyCache:sessionIDs];
+ for (Tab* tab in model_) {
+ tab.useGreyImageCache = YES;
+ }
+}
+
+- (void)deleteGreyCache {
+ [[SnapshotCache sharedInstance] removeGreyCache];
+ for (Tab* tab in model_) {
+ tab.useGreyImageCache = NO;
+ }
+}
+
+- (void)handlePan:(SideSwipeGestureRecognizer*)gesture {
+ if (!IsIPadIdiom()) {
+ return [self handleiPhoneTabSwipe:gesture];
+ } else {
+ return [self handleiPadTabSwipe:gesture];
+ }
+}
+
+- (void)handleSwipe:(SideSwipeGestureRecognizer*)gesture {
+ DCHECK(swipeType_ != SwipeType::NONE);
+ if (swipeType_ == SwipeType::CHANGE_TAB) {
+ if (!IsIPadIdiom()) {
+ return [self handleiPhoneTabSwipe:gesture];
+ } else {
+ return [self handleiPadTabSwipe:gesture];
+ }
+ }
+ if (swipeType_ == SwipeType::CHANGE_PAGE) {
+ return [self handleSwipeToNavigate:gesture];
+ }
+ NOTREACHED();
+}
+
+- (void)handleiPadTabSwipe:(SideSwipeGestureRecognizer*)gesture {
+ // Don't handle swipe when there are no tabs.
+ NSInteger count = [model_ count];
+ if (count == 0)
+ return;
+
+ if (gesture.state == UIGestureRecognizerStateBegan) {
+ // If the toolbar is hidden, move it to visible.
+ [[model_ currentTab] updateFullscreenWithToolbarVisible:YES];
+ [[model_ currentTab] updateSnapshotWithOverlay:YES visibleFrameOnly:YES];
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:ios_internal::kSideSwipeWillStartNotification
+ object:nil];
+ [[swipeDelegate_ tabStripController] setHighlightsSelectedTab:YES];
+ startingTabIndex_ = [model_ indexOfTab:[model_ currentTab]];
+ [self createGreyCache:gesture.direction];
+ } else if (gesture.state == UIGestureRecognizerStateChanged) {
+ // Side swipe for iPad involves changing the selected tab as the swipe moves
+ // across the width of the view. The screen is broken up into
+ // |kIpadTabSwipeDistance| / |width| segments, with the current tab in the
+ // first section. The swipe does not wrap edges.
+ CGFloat distance = [gesture locationInView:gesture.view].x;
+ if (gesture.direction == UISwipeGestureRecognizerDirectionLeft) {
+ distance = gesture.startPoint.x - distance;
+ } else {
+ distance -= gesture.startPoint.x;
+ }
+
+ NSInteger indexDelta = std::floor(distance / kIpadTabSwipeDistance);
+ // Don't wrap past the first tab.
+ if (indexDelta < count) {
+ // Flip delta when swiping forward.
+ if (IsSwipingForward(gesture.direction))
+ indexDelta = 0 - indexDelta;
+
+ Tab* currentTab = [model_ currentTab];
+ NSInteger currentIndex = [model_ indexOfTab:currentTab];
+
+ // Wrap around edges.
+ NSInteger newIndex = (NSInteger)(startingTabIndex_ + indexDelta) % count;
+
+ // C99 defines the modulo result as negative if our offset is negative.
+ if (newIndex < 0)
+ newIndex += count;
+
+ if (newIndex != currentIndex) {
+ Tab* tab = [model_ tabAtIndex:newIndex];
+ // Toggle overlay preview mode for selected tab.
+ [tab.webController setOverlayPreviewMode:YES];
+ [model_ setCurrentTab:tab];
+ // And disable overlay preview mode for last selected tab.
+ [currentTab.webController setOverlayPreviewMode:NO];
+ }
+ }
+ } else {
+ if (gesture.state == UIGestureRecognizerStateCancelled) {
+ Tab* tab = [model_ tabAtIndex:startingTabIndex_];
+ [[model_ currentTab].webController setOverlayPreviewMode:NO];
+ [model_ setCurrentTab:tab];
+ }
+ [[model_ currentTab].webController setOverlayPreviewMode:NO];
+
+ // Redisplay the view if it was in overlay preview mode.
+ [swipeDelegate_ displayTab:[model_ currentTab] isNewSelection:YES];
+ [[swipeDelegate_ tabStripController] setHighlightsSelectedTab:NO];
+ [self deleteGreyCache];
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:ios_internal::kSideSwipeDidStopNotification
+ object:nil];
+ }
+}
+
+- (id<SideSwipeContentProvider>)contentProviderForGesture:(BOOL)goBack {
+ if (goBack && [historySideSwipeProvider_ canGoBack]) {
+ return historySideSwipeProvider_;
+ }
+ if (!goBack && [historySideSwipeProvider_ canGoForward]) {
+ return historySideSwipeProvider_;
+ }
+ if (goBack && [readingListSideSwipeProvider_ canGoBack]) {
+ return readingListSideSwipeProvider_;
+ }
+ if (!goBack && [readingListSideSwipeProvider_ canGoForward]) {
+ return readingListSideSwipeProvider_;
+ }
+ return nil;
+}
+
+// Show swipe to navigate.
+- (void)handleSwipeToNavigate:(SideSwipeGestureRecognizer*)gesture {
+ if (gesture.state == UIGestureRecognizerStateBegan) {
+ // If the toolbar is hidden, move it to visible.
+ [[model_ currentTab] updateFullscreenWithToolbarVisible:YES];
+
+ inSwipe_ = YES;
+ [swipeDelegate_ updateAccessoryViewsForSideSwipeWithVisibility:NO];
+ BOOL goBack = IsSwipingBack(gesture.direction);
+
+ currentContentProvider_.reset([self contentProviderForGesture:goBack]);
+ BOOL canNavigate = currentContentProvider_ != nil;
+
+ CGRect gestureBounds = gesture.view.bounds;
+ CGFloat headerHeight = [swipeDelegate_ headerHeight];
+ CGRect navigationFrame =
+ CGRectMake(CGRectGetMinX(gestureBounds),
+ CGRectGetMinY(gestureBounds) + headerHeight,
+ CGRectGetWidth(gestureBounds),
+ CGRectGetHeight(gestureBounds) - headerHeight);
+
+ pageSideSwipeView_.reset([[SideSwipeNavigationView alloc]
+ initWithFrame:navigationFrame
+ withDirection:gesture.direction
+ canNavigate:canNavigate
+ image:[currentContentProvider_ paneIcon]
+ rotateForward:[currentContentProvider_ rotateForwardIcon]]);
+ [pageSideSwipeView_ setTargetView:[swipeDelegate_ contentView]];
+
+ [gesture.view insertSubview:pageSideSwipeView_
+ belowSubview:[[swipeDelegate_ toolbarController] view]];
+ }
+
+ base::WeakNSObject<Tab> weakCurrentTab([model_ currentTab]);
+ [pageSideSwipeView_ handleHorizontalPan:gesture
+ onOverThresholdCompletion:^{
+ BOOL wantsBack = IsSwipingBack(gesture.direction);
+ web::WebState* webState = [weakCurrentTab webState];
+ if (wantsBack) {
+ [currentContentProvider_ goBack:webState];
+ } else {
+ [currentContentProvider_ goForward:webState];
+ }
+
+ if (webState && webState->IsLoading()) {
+ webStateObserverBridge_.reset(
+ new web::WebStateObserverBridge(webState, self));
+ [self addCurtainWithCompletionHandler:^{
+ inSwipe_ = NO;
+ }];
+ } else {
+ inSwipe_ = NO;
+ }
+ [swipeDelegate_ updateAccessoryViewsForSideSwipeWithVisibility:YES];
+ }
+ onUnderThresholdCompletion:^{
+ [swipeDelegate_ updateAccessoryViewsForSideSwipeWithVisibility:YES];
+ inSwipe_ = NO;
+ }];
+}
+
+// Show horizontal swipe stack view for iPhone.
+- (void)handleiPhoneTabSwipe:(SideSwipeGestureRecognizer*)gesture {
+ if (gesture.state == UIGestureRecognizerStateBegan) {
+ // If the toolbar is hidden, move it to visible.
+ [[model_ currentTab] updateFullscreenWithToolbarVisible:YES];
+
+ inSwipe_ = YES;
+
+ CGRect frame = [[swipeDelegate_ contentView] frame];
+
+ // Add horizontal stack view controller.
+ CGFloat headerHeight =
+ [self.snapshotDelegate snapshotContentAreaForTab:[model_ currentTab]]
+ .origin.y;
+ if (tabSideSwipeView_) {
+ [tabSideSwipeView_ setFrame:frame];
+ [tabSideSwipeView_ setTopMargin:headerHeight];
+ } else {
+ tabSideSwipeView_.reset([[CardSideSwipeView alloc]
+ initWithFrame:frame
+ topMargin:headerHeight
+ model:model_]);
+ [tabSideSwipeView_ setAutoresizingMask:UIViewAutoresizingFlexibleWidth |
+ UIViewAutoresizingFlexibleHeight];
+ [tabSideSwipeView_ setDelegate:swipeDelegate_];
+ [tabSideSwipeView_ setBackgroundColor:[UIColor blackColor]];
+ }
+
+ // Ensure that there's an up-to-date snapshot of the current tab.
+ [[model_ currentTab] updateSnapshotWithOverlay:YES visibleFrameOnly:YES];
+ // Hide the infobar after snapshot has been updated (see the previous line)
+ // to avoid it obscuring the cards in the side swipe view.
+ [swipeDelegate_ updateAccessoryViewsForSideSwipeWithVisibility:NO];
+
+ // Layout tabs with new snapshots in the current orientation.
+ [tabSideSwipeView_
+ updateViewsForDirection:gesture.direction
+ withToolbar:[swipeDelegate_ toolbarController]];
+
+ // Insert behind infobar container (which is below toolbar)
+ // so card border doesn't look janky during animation.
+ DCHECK([swipeDelegate_ verifyToolbarViewPlacementInView:gesture.view]);
+ // Insert above the toolbar.
+ [gesture.view addSubview:tabSideSwipeView_];
+
+ // Remove content area so it doesn't receive any pan events.
+ [[swipeDelegate_ contentView] removeFromSuperview];
+ }
+
+ [tabSideSwipeView_ handleHorizontalPan:gesture];
+}
+
+- (void)addCurtainWithCompletionHandler:(ProceduralBlock)completionHandler {
+ if (!curtain_) {
+ curtain_.reset(
+ [[UIView alloc] initWithFrame:[swipeDelegate_ contentView].bounds]);
+ [curtain_ setBackgroundColor:[UIColor whiteColor]];
+ }
+ [[swipeDelegate_ contentView] addSubview:curtain_];
+
+ // Fallback in case load takes a while. 3 seconds is a balance between how
+ // long it can take a web view to clear the previous page image, and what
+ // feels like to 'too long' to see the curtain.
+ [self performSelector:@selector(dismissCurtainWithCompletionHandler:)
+ withObject:[[completionHandler copy] autorelease]
+ afterDelay:3];
+}
+
+- (void)dismissCurtainWithCompletionHandler:(ProceduralBlock)completionHandler {
+ [NSObject cancelPreviousPerformRequestsWithTarget:self];
+ webStateObserverBridge_.reset();
+ [curtain_ removeFromSuperview];
+ curtain_.reset();
+ completionHandler();
+}
+
+#pragma mark - CRWWebStateObserver Methods
+
+- (void)webStateDidStopLoading:(web::WebState*)webState {
+ [self dismissCurtainWithCompletionHandler:^{
+ inSwipe_ = NO;
+ }];
+}
+
+#pragma mark - TabModelObserver Methods
+
+- (void)tabModel:(TabModel*)model
+ didChangeActiveTab:(Tab*)newTab
+ previousTab:(Tab*)previousTab
+ atIndex:(NSUInteger)index {
+ // Toggling the gesture's enabled state off and on will effectively cancel
+ // the gesture recognizer.
+ [swipeGestureRecognizer_ setEnabled:NO];
+ [swipeGestureRecognizer_ setEnabled:YES];
+}
+
+@end

Powered by Google App Engine
This is Rietveld 408576698