Index: ios/chrome/browser/ui/side_swipe/card_side_swipe_view.mm |
diff --git a/ios/chrome/browser/ui/side_swipe/card_side_swipe_view.mm b/ios/chrome/browser/ui/side_swipe/card_side_swipe_view.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..4f2a3e7da62ac083fd5d0ea3a85211dc0749dff3 |
--- /dev/null |
+++ b/ios/chrome/browser/ui/side_swipe/card_side_swipe_view.mm |
@@ -0,0 +1,401 @@ |
+// Copyright 2012 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/card_side_swipe_view.h" |
+ |
+#include <cmath> |
+ |
+#include "base/ios/device_util.h" |
+#include "base/metrics/user_metrics.h" |
+#include "base/metrics/user_metrics_action.h" |
+#include "base/strings/sys_string_conversions.h" |
+#include "ios/chrome/browser/chrome_url_constants.h" |
+#import "ios/chrome/browser/tabs/tab.h" |
+#import "ios/chrome/browser/tabs/tab_model.h" |
+#import "ios/chrome/browser/ui/background_generator.h" |
+#import "ios/chrome/browser/ui/ntp/new_tab_page_panel_protocol.h" |
+#include "ios/chrome/browser/ui/rtl_geometry.h" |
+#import "ios/chrome/browser/ui/side_swipe/side_swipe_util.h" |
+#import "ios/chrome/browser/ui/side_swipe_gesture_recognizer.h" |
+#import "ios/chrome/browser/ui/toolbar/web_toolbar_controller.h" |
+#include "ios/chrome/browser/ui/ui_util.h" |
+#import "ios/chrome/browser/ui/uikit_ui_util.h" |
+#include "ios/chrome/grit/ios_theme_resources.h" |
+#import "ios/web/web_state/ui/crw_web_controller.h" |
+#include "ui/base/resource/resource_bundle.h" |
+#include "url/gurl.h" |
+ |
+using base::UserMetricsAction; |
+ |
+namespace { |
+// Spacing between cards. |
+const CGFloat kCardHorizontalSpacing = 30; |
+ |
+// Portion of the screen an edge card can be dragged. |
+const CGFloat kEdgeCardDragPercentage = 0.35; |
+ |
+// Card animation times. |
+const NSTimeInterval kAnimationDuration = 0.15; |
+ |
+// Reduce size in -smallGreyImage by this factor. |
+const CGFloat kResizeFactor = 4; |
+} // anonymous namespace |
+ |
+@implementation SwipeView |
+ |
+- (id)initWithFrame:(CGRect)frame topMargin:(CGFloat)topMargin { |
+ self = [super initWithFrame:frame]; |
+ if (self) { |
+ image_.reset([[UIImageView alloc] initWithFrame:CGRectZero]); |
+ [image_ setClipsToBounds:YES]; |
+ [image_ setContentMode:UIViewContentModeScaleAspectFill]; |
+ [self addSubview:image_]; |
+ |
+ toolbarHolder_.reset([[UIImageView alloc] initWithFrame:CGRectZero]); |
+ [self addSubview:toolbarHolder_]; |
+ |
+ shadowView_.reset([[UIImageView alloc] initWithFrame:self.bounds]); |
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance(); |
+ gfx::Image shadow = rb.GetNativeImageNamed(IDR_IOS_TOOLBAR_SHADOW); |
+ [shadowView_ setImage:shadow.ToUIImage()]; |
+ [self addSubview:shadowView_]; |
+ |
+ // All subviews are as wide as the parent |
+ NSMutableArray* constraints = [NSMutableArray array]; |
+ for (UIView* view in self.subviews) { |
+ [view setTranslatesAutoresizingMaskIntoConstraints:NO]; |
+ [constraints addObject:[view.leadingAnchor |
+ constraintEqualToAnchor:self.leadingAnchor]]; |
+ [constraints addObject:[view.trailingAnchor |
+ constraintEqualToAnchor:self.trailingAnchor]]; |
+ } |
+ |
+ [constraints addObjectsFromArray:@[ |
+ [[image_ topAnchor] constraintEqualToAnchor:self.topAnchor |
+ constant:topMargin], |
+ [[image_ bottomAnchor] constraintEqualToAnchor:self.bottomAnchor], |
+ [[toolbarHolder_ topAnchor] constraintEqualToAnchor:self.topAnchor |
+ constant:-StatusBarHeight()], |
+ [[toolbarHolder_ heightAnchor] |
+ constraintEqualToConstant:topMargin + StatusBarHeight()], |
+ [[shadowView_ topAnchor] constraintEqualToAnchor:self.topAnchor |
+ constant:topMargin], |
+ [[shadowView_ heightAnchor] |
+ constraintEqualToConstant:kNewTabPageShadowHeight] |
+ ]]; |
+ |
+ [NSLayoutConstraint activateConstraints:constraints]; |
+ } |
+ return self; |
+} |
+ |
+- (void)layoutSubviews { |
+ [super layoutSubviews]; |
+ [self updateImageBoundsAndZoom]; |
+} |
+ |
+- (void)updateImageBoundsAndZoom { |
+ UIImage* image = [image_ image]; |
+ if (image) { |
+ CGSize imageSize = image.size; |
+ CGSize viewSize = [image_ frame].size; |
+ CGFloat zoomRatio = std::max(viewSize.height / imageSize.height, |
+ viewSize.width / imageSize.width); |
+ [image_ layer].contentsRect = |
+ CGRectMake(0.0, 0.0, viewSize.width / (zoomRatio * imageSize.width), |
+ viewSize.height / (zoomRatio * imageSize.height)); |
+ } |
+} |
+ |
+- (void)setImage:(UIImage*)image { |
+ [image_ setImage:image]; |
+ [self updateImageBoundsAndZoom]; |
+} |
+ |
+- (void)setToolbarImage:(UIImage*)image isNewTabPage:(BOOL)isNewTabPage { |
+ [toolbarHolder_ setImage:image]; |
+ [shadowView_ setHidden:isNewTabPage]; |
+} |
+ |
+@end |
+ |
+@interface CardSideSwipeView () |
+// Pan touches ended or were cancelled. |
+- (void)finishPan; |
+// Is the current card is an edge card based on swipe direction. |
+- (BOOL)isEdgeSwipe; |
+// Initialize card based on model_ index. |
+- (void)setupCard:(SwipeView*)card |
+ withIndex:(NSInteger)index |
+ withToolbar:(WebToolbarController*)toolbarController; |
+// Build a |kResizeFactor| sized greyscaled version of |image|. |
+- (UIImage*)smallGreyImage:(UIImage*)image; |
+@end |
+ |
+@implementation CardSideSwipeView |
+ |
+@synthesize delegate = delegate_; |
+@synthesize topMargin = topMargin_; |
+ |
+- (id)initWithFrame:(CGRect)frame |
+ topMargin:(CGFloat)topMargin |
+ model:(TabModel*)model { |
+ self = [super initWithFrame:frame]; |
+ if (self) { |
+ model_ = model; |
+ currentPoint_ = CGPointZero; |
+ topMargin_ = topMargin; |
+ |
+ base::scoped_nsobject<UIView> background( |
+ [[UIView alloc] initWithFrame:CGRectZero]); |
+ [self addSubview:background]; |
+ |
+ [background setTranslatesAutoresizingMaskIntoConstraints:NO]; |
+ [NSLayoutConstraint activateConstraints:@[ |
+ [[background rightAnchor] constraintEqualToAnchor:self.rightAnchor], |
+ [[background leftAnchor] constraintEqualToAnchor:self.leftAnchor], |
+ [[background topAnchor] constraintEqualToAnchor:self.topAnchor |
+ constant:-StatusBarHeight()], |
+ [[background bottomAnchor] constraintEqualToAnchor:self.bottomAnchor] |
+ ]]; |
+ |
+ InstallBackgroundInView(background); |
+ |
+ rightCard_.reset( |
+ [[SwipeView alloc] initWithFrame:CGRectZero topMargin:topMargin]); |
+ leftCard_.reset( |
+ [[SwipeView alloc] initWithFrame:CGRectZero topMargin:topMargin]); |
+ [rightCard_ setTranslatesAutoresizingMaskIntoConstraints:NO]; |
+ [leftCard_ setTranslatesAutoresizingMaskIntoConstraints:NO]; |
+ [self addSubview:rightCard_]; |
+ [self addSubview:leftCard_]; |
+ AddSameSizeConstraint(rightCard_, self); |
+ AddSameSizeConstraint(leftCard_, self); |
+ } |
+ return self; |
+} |
+ |
+- (CGRect)cardFrame { |
+ return self.bounds; |
+} |
+ |
+// Set up left and right card views depending on current tab and swipe |
+// direction. |
+- (void)updateViewsForDirection:(UISwipeGestureRecognizerDirection)direction |
+ withToolbar:(WebToolbarController*)tbc { |
+ direction_ = direction; |
+ CGRect cardFrame = [self cardFrame]; |
+ NSUInteger currentIndex = [model_ indexOfTab:model_.currentTab]; |
+ CGFloat offset = UseRTLLayout() ? -1 : 1; |
+ if (direction_ == UISwipeGestureRecognizerDirectionRight) { |
+ [self setupCard:rightCard_ withIndex:currentIndex withToolbar:tbc]; |
+ [rightCard_ setFrame:cardFrame]; |
+ [self setupCard:leftCard_ withIndex:currentIndex - offset withToolbar:tbc]; |
+ cardFrame.origin.x -= cardFrame.size.width + kCardHorizontalSpacing; |
+ [leftCard_ setFrame:cardFrame]; |
+ } else { |
+ [self setupCard:leftCard_ withIndex:currentIndex withToolbar:tbc]; |
+ [leftCard_ setFrame:cardFrame]; |
+ [self setupCard:rightCard_ withIndex:currentIndex + offset withToolbar:tbc]; |
+ cardFrame.origin.x += cardFrame.size.width + kCardHorizontalSpacing; |
+ [rightCard_ setFrame:cardFrame]; |
+ } |
+ [tbc resetToolbarAfterSideSwipeSnapshot]; |
+} |
+ |
+- (UIImage*)smallGreyImage:(UIImage*)image { |
+ CGRect smallSize = CGRectMake(0, 0, image.size.width / kResizeFactor, |
+ image.size.height / kResizeFactor); |
+ // Using CIFilter here on iOS 5+ might be faster, but it doesn't easily |
+ // allow for resizing. At the max size, it's still too slow for side swipe. |
+ UIGraphicsBeginImageContextWithOptions(smallSize.size, YES, 0); |
+ [image drawInRect:smallSize blendMode:kCGBlendModeLuminosity alpha:1.0]; |
+ UIImage* greyImage = UIGraphicsGetImageFromCurrentImageContext(); |
+ UIGraphicsEndImageContext(); |
+ return greyImage; |
+} |
+ |
+// Create card view based on TabModel index. |
+- (void)setupCard:(SwipeView*)card |
+ withIndex:(NSInteger)index |
+ withToolbar:(WebToolbarController*)toolbarController { |
+ if (index < 0 || index >= (NSInteger)[model_ count]) { |
+ [card setHidden:YES]; |
+ return; |
+ } |
+ [card setHidden:NO]; |
+ |
+ Tab* tab = [model_ tabAtIndex:index]; |
+ BOOL isNTP = tab.url.host() == kChromeUINewTabHost; |
+ [toolbarController updateToolbarForSideSwipeSnapshot:tab]; |
+ UIImage* toolbarView = CaptureViewWithOption([toolbarController view], |
+ [[UIScreen mainScreen] scale], |
+ kClientSideRendering); |
+ [card setToolbarImage:toolbarView isNewTabPage:isNTP]; |
+ |
+ // Converting snapshotted images to grey takes too much time for single core |
+ // devices. Instead, show the colored image for single core devices and the |
+ // grey image for multi core devices. |
+ dispatch_queue_t priorityQueue = |
+ dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul); |
+ [tab retrieveSnapshot:^(UIImage* image) { |
+ if (tab.webController.usePlaceholderOverlay && |
+ !ios::device_util::IsSingleCoreDevice()) { |
+ [card setImage:[CRWWebController defaultSnapshotImage]]; |
+ dispatch_async(priorityQueue, ^{ |
+ UIImage* greyImage = [self smallGreyImage:image]; |
+ dispatch_async(dispatch_get_main_queue(), ^{ |
+ [card setImage:greyImage]; |
+ }); |
+ }); |
+ } else { |
+ [card setImage:image]; |
+ } |
+ }]; |
+} |
+ |
+// Place cards around |currentPoint_.x|, and lean towards each card near the |
+// X edges of |bounds|. Shrink cards as they are dragged towards the middle of |
+// the |bounds|, and edge cards only drag |kEdgeCardDragPercentage| of |bounds|. |
+- (void)updateCardPositions { |
+ CGRect bounds = [self cardFrame]; |
+ [rightCard_ setFrame:bounds]; |
+ [leftCard_ setFrame:bounds]; |
+ |
+ CGFloat width = CGRectGetWidth([self cardFrame]); |
+ CGPoint center = CGPointMake(bounds.origin.x + bounds.size.width / 2, |
+ bounds.origin.y + bounds.size.height / 2); |
+ if ([self isEdgeSwipe]) { |
+ // If an edge card, don't allow the card to be dragged across the screen. |
+ // Instead, drag across |kEdgeCardDragPercentage| of the screen. |
+ center.x = currentPoint_.x - width / 2 - |
+ (currentPoint_.x - width) / width * |
+ (width * (1 - kEdgeCardDragPercentage)); |
+ [leftCard_ setCenter:center]; |
+ center.x = currentPoint_.x / width * (width * kEdgeCardDragPercentage) + |
+ bounds.size.width / 2; |
+ [rightCard_ setCenter:center]; |
+ } else { |
+ // Place cards around the finger as it is dragged across the screen. |
+ // Place the finger between the cards in the middle of the screen, on the |
+ // right card border when on the left side of the screen, and on the left |
+ // card border when on the right side of the screen. |
+ CGFloat rightXBuffer = kCardHorizontalSpacing * currentPoint_.x / width; |
+ CGFloat leftXBuffer = kCardHorizontalSpacing - rightXBuffer; |
+ |
+ center.x = currentPoint_.x - leftXBuffer - width / 2; |
+ [leftCard_ setCenter:center]; |
+ |
+ center.x = currentPoint_.x + rightXBuffer + width / 2; |
+ [rightCard_ setCenter:center]; |
+ } |
+} |
+ |
+// Update layout with new touch event. |
+- (void)handleHorizontalPan:(SideSwipeGestureRecognizer*)gesture { |
+ currentPoint_ = [gesture locationInView:self]; |
+ currentPoint_.x -= gesture.swipeOffset; |
+ |
+ // Since it's difficult to touch the very edge of the screen (touches tend to |
+ // sit near x ~ 4), push the touch towards the edge. |
+ CGFloat width = CGRectGetWidth([self cardFrame]); |
+ CGFloat half = floor(width / 2); |
+ CGFloat padding = floor(std::abs(currentPoint_.x - half) / half); |
+ |
+ // Push towards the edges. |
+ if (currentPoint_.x > half) |
+ currentPoint_.x += padding; |
+ else |
+ currentPoint_.x -= padding; |
+ |
+ // But don't go past the edges. |
+ if (currentPoint_.x < 0) |
+ currentPoint_.x = 0; |
+ else if (currentPoint_.x > width) |
+ currentPoint_.x = width; |
+ |
+ [self updateCardPositions]; |
+ |
+ if (gesture.state == UIGestureRecognizerStateEnded || |
+ gesture.state == UIGestureRecognizerStateCancelled || |
+ gesture.state == UIGestureRecognizerStateFailed) { |
+ [self finishPan]; |
+ } |
+} |
+ |
+- (BOOL)isEdgeSwipe { |
+ NSUInteger currentIndex = [model_ indexOfTab:model_.currentTab]; |
+ return (IsSwipingBack(direction_) && currentIndex == 0) || |
+ (IsSwipingForward(direction_) && currentIndex == [model_ count] - 1); |
+} |
+ |
+// Update the current tab and animate the proper card view if the |
+// |currentPoint_| is past the center of |bounds|. |
+- (void)finishPan { |
+ NSUInteger currentIndex = [model_ indexOfTab:model_.currentTab]; |
+ // Something happened and now currentTab is gone. End card side swipe and let |
+ // BVC show no tabs UI. |
+ if (currentIndex == NSNotFound) |
+ return [delegate_ sideSwipeViewDismissAnimationDidEnd:self]; |
+ |
+ CGRect finalSize = [self cardFrame]; |
+ CGFloat width = CGRectGetWidth([self cardFrame]); |
+ CGRect leftFrame, rightFrame; |
+ SwipeView* dominantCard; |
+ Tab* destinationTab = model_.currentTab; |
+ CGFloat offset = UseRTLLayout() ? -1 : 1; |
+ if (direction_ == UISwipeGestureRecognizerDirectionRight) { |
+ // If swipe is right and |currentPoint_.x| is over the first 1/3, move left. |
+ if (currentPoint_.x > width / 3.0 && ![self isEdgeSwipe]) { |
+ destinationTab = [model_ tabAtIndex:currentIndex - offset]; |
+ dominantCard = leftCard_; |
+ rightFrame = leftFrame = finalSize; |
+ rightFrame.origin.x += rightFrame.size.width + kCardHorizontalSpacing; |
+ base::RecordAction(UserMetricsAction("MobileStackSwipeCompleted")); |
+ } else { |
+ dominantCard = rightCard_; |
+ leftFrame = rightFrame = finalSize; |
+ leftFrame.origin.x -= rightFrame.size.width + kCardHorizontalSpacing; |
+ base::RecordAction(UserMetricsAction("MobileStackSwipeCancelled")); |
+ } |
+ } else { |
+ // If swipe is left and |currentPoint_.x| is over the first 1/3, move right. |
+ if (currentPoint_.x < (width / 3.0) * 2.0 && ![self isEdgeSwipe]) { |
+ destinationTab = [model_ tabAtIndex:currentIndex + offset]; |
+ dominantCard = rightCard_; |
+ leftFrame = rightFrame = finalSize; |
+ leftFrame.origin.x -= rightFrame.size.width + kCardHorizontalSpacing; |
+ base::RecordAction(UserMetricsAction("MobileStackSwipeCompleted")); |
+ } else { |
+ dominantCard = leftCard_; |
+ rightFrame = leftFrame = finalSize; |
+ rightFrame.origin.x += rightFrame.size.width + kCardHorizontalSpacing; |
+ base::RecordAction(UserMetricsAction("MobileStackSwipeCancelled")); |
+ } |
+ } |
+ |
+ // Changing the model even when the tab is the same at the end of the |
+ // animation allows the UI to recover. |
+ [model_ setCurrentTab:destinationTab]; |
+ |
+ // Make sure the dominant card animates on top. |
+ [dominantCard.superview bringSubviewToFront:dominantCard]; |
+ |
+ [UIView animateWithDuration:kAnimationDuration |
+ animations:^{ |
+ [leftCard_ setTransform:CGAffineTransformIdentity]; |
+ [rightCard_ setTransform:CGAffineTransformIdentity]; |
+ [leftCard_ setFrame:leftFrame]; |
+ [rightCard_ setFrame:rightFrame]; |
+ } |
+ completion:^(BOOL finished) { |
+ [leftCard_ setImage:nil]; |
+ [rightCard_ setImage:nil]; |
+ [leftCard_ setToolbarImage:nil isNewTabPage:NO]; |
+ [rightCard_ setToolbarImage:nil isNewTabPage:NO]; |
+ [delegate_ sideSwipeViewDismissAnimationDidEnd:self]; |
+ }]; |
+} |
+ |
+@end |