OLD | NEW |
(Empty) | |
| 1 // Copyright 2012 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/card_side_swipe_view.h" |
| 6 |
| 7 #include <cmath> |
| 8 |
| 9 #include "base/ios/device_util.h" |
| 10 #include "base/metrics/user_metrics.h" |
| 11 #include "base/metrics/user_metrics_action.h" |
| 12 #include "base/strings/sys_string_conversions.h" |
| 13 #include "ios/chrome/browser/chrome_url_constants.h" |
| 14 #import "ios/chrome/browser/tabs/tab.h" |
| 15 #import "ios/chrome/browser/tabs/tab_model.h" |
| 16 #import "ios/chrome/browser/ui/background_generator.h" |
| 17 #import "ios/chrome/browser/ui/ntp/new_tab_page_panel_protocol.h" |
| 18 #include "ios/chrome/browser/ui/rtl_geometry.h" |
| 19 #import "ios/chrome/browser/ui/side_swipe/side_swipe_util.h" |
| 20 #import "ios/chrome/browser/ui/side_swipe_gesture_recognizer.h" |
| 21 #import "ios/chrome/browser/ui/toolbar/web_toolbar_controller.h" |
| 22 #include "ios/chrome/browser/ui/ui_util.h" |
| 23 #import "ios/chrome/browser/ui/uikit_ui_util.h" |
| 24 #include "ios/chrome/grit/ios_theme_resources.h" |
| 25 #import "ios/web/web_state/ui/crw_web_controller.h" |
| 26 #include "ui/base/resource/resource_bundle.h" |
| 27 #include "url/gurl.h" |
| 28 |
| 29 using base::UserMetricsAction; |
| 30 |
| 31 namespace { |
| 32 // Spacing between cards. |
| 33 const CGFloat kCardHorizontalSpacing = 30; |
| 34 |
| 35 // Portion of the screen an edge card can be dragged. |
| 36 const CGFloat kEdgeCardDragPercentage = 0.35; |
| 37 |
| 38 // Card animation times. |
| 39 const NSTimeInterval kAnimationDuration = 0.15; |
| 40 |
| 41 // Reduce size in -smallGreyImage by this factor. |
| 42 const CGFloat kResizeFactor = 4; |
| 43 } // anonymous namespace |
| 44 |
| 45 @implementation SwipeView |
| 46 |
| 47 - (id)initWithFrame:(CGRect)frame topMargin:(CGFloat)topMargin { |
| 48 self = [super initWithFrame:frame]; |
| 49 if (self) { |
| 50 image_.reset([[UIImageView alloc] initWithFrame:CGRectZero]); |
| 51 [image_ setClipsToBounds:YES]; |
| 52 [image_ setContentMode:UIViewContentModeScaleAspectFill]; |
| 53 [self addSubview:image_]; |
| 54 |
| 55 toolbarHolder_.reset([[UIImageView alloc] initWithFrame:CGRectZero]); |
| 56 [self addSubview:toolbarHolder_]; |
| 57 |
| 58 shadowView_.reset([[UIImageView alloc] initWithFrame:self.bounds]); |
| 59 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); |
| 60 gfx::Image shadow = rb.GetNativeImageNamed(IDR_IOS_TOOLBAR_SHADOW); |
| 61 [shadowView_ setImage:shadow.ToUIImage()]; |
| 62 [self addSubview:shadowView_]; |
| 63 |
| 64 // All subviews are as wide as the parent |
| 65 NSMutableArray* constraints = [NSMutableArray array]; |
| 66 for (UIView* view in self.subviews) { |
| 67 [view setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| 68 [constraints addObject:[view.leadingAnchor |
| 69 constraintEqualToAnchor:self.leadingAnchor]]; |
| 70 [constraints addObject:[view.trailingAnchor |
| 71 constraintEqualToAnchor:self.trailingAnchor]]; |
| 72 } |
| 73 |
| 74 [constraints addObjectsFromArray:@[ |
| 75 [[image_ topAnchor] constraintEqualToAnchor:self.topAnchor |
| 76 constant:topMargin], |
| 77 [[image_ bottomAnchor] constraintEqualToAnchor:self.bottomAnchor], |
| 78 [[toolbarHolder_ topAnchor] constraintEqualToAnchor:self.topAnchor |
| 79 constant:-StatusBarHeight()], |
| 80 [[toolbarHolder_ heightAnchor] |
| 81 constraintEqualToConstant:topMargin + StatusBarHeight()], |
| 82 [[shadowView_ topAnchor] constraintEqualToAnchor:self.topAnchor |
| 83 constant:topMargin], |
| 84 [[shadowView_ heightAnchor] |
| 85 constraintEqualToConstant:kNewTabPageShadowHeight] |
| 86 ]]; |
| 87 |
| 88 [NSLayoutConstraint activateConstraints:constraints]; |
| 89 } |
| 90 return self; |
| 91 } |
| 92 |
| 93 - (void)layoutSubviews { |
| 94 [super layoutSubviews]; |
| 95 [self updateImageBoundsAndZoom]; |
| 96 } |
| 97 |
| 98 - (void)updateImageBoundsAndZoom { |
| 99 UIImage* image = [image_ image]; |
| 100 if (image) { |
| 101 CGSize imageSize = image.size; |
| 102 CGSize viewSize = [image_ frame].size; |
| 103 CGFloat zoomRatio = std::max(viewSize.height / imageSize.height, |
| 104 viewSize.width / imageSize.width); |
| 105 [image_ layer].contentsRect = |
| 106 CGRectMake(0.0, 0.0, viewSize.width / (zoomRatio * imageSize.width), |
| 107 viewSize.height / (zoomRatio * imageSize.height)); |
| 108 } |
| 109 } |
| 110 |
| 111 - (void)setImage:(UIImage*)image { |
| 112 [image_ setImage:image]; |
| 113 [self updateImageBoundsAndZoom]; |
| 114 } |
| 115 |
| 116 - (void)setToolbarImage:(UIImage*)image isNewTabPage:(BOOL)isNewTabPage { |
| 117 [toolbarHolder_ setImage:image]; |
| 118 [shadowView_ setHidden:isNewTabPage]; |
| 119 } |
| 120 |
| 121 @end |
| 122 |
| 123 @interface CardSideSwipeView () |
| 124 // Pan touches ended or were cancelled. |
| 125 - (void)finishPan; |
| 126 // Is the current card is an edge card based on swipe direction. |
| 127 - (BOOL)isEdgeSwipe; |
| 128 // Initialize card based on model_ index. |
| 129 - (void)setupCard:(SwipeView*)card |
| 130 withIndex:(NSInteger)index |
| 131 withToolbar:(WebToolbarController*)toolbarController; |
| 132 // Build a |kResizeFactor| sized greyscaled version of |image|. |
| 133 - (UIImage*)smallGreyImage:(UIImage*)image; |
| 134 @end |
| 135 |
| 136 @implementation CardSideSwipeView |
| 137 |
| 138 @synthesize delegate = delegate_; |
| 139 @synthesize topMargin = topMargin_; |
| 140 |
| 141 - (id)initWithFrame:(CGRect)frame |
| 142 topMargin:(CGFloat)topMargin |
| 143 model:(TabModel*)model { |
| 144 self = [super initWithFrame:frame]; |
| 145 if (self) { |
| 146 model_ = model; |
| 147 currentPoint_ = CGPointZero; |
| 148 topMargin_ = topMargin; |
| 149 |
| 150 base::scoped_nsobject<UIView> background( |
| 151 [[UIView alloc] initWithFrame:CGRectZero]); |
| 152 [self addSubview:background]; |
| 153 |
| 154 [background setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| 155 [NSLayoutConstraint activateConstraints:@[ |
| 156 [[background rightAnchor] constraintEqualToAnchor:self.rightAnchor], |
| 157 [[background leftAnchor] constraintEqualToAnchor:self.leftAnchor], |
| 158 [[background topAnchor] constraintEqualToAnchor:self.topAnchor |
| 159 constant:-StatusBarHeight()], |
| 160 [[background bottomAnchor] constraintEqualToAnchor:self.bottomAnchor] |
| 161 ]]; |
| 162 |
| 163 InstallBackgroundInView(background); |
| 164 |
| 165 rightCard_.reset( |
| 166 [[SwipeView alloc] initWithFrame:CGRectZero topMargin:topMargin]); |
| 167 leftCard_.reset( |
| 168 [[SwipeView alloc] initWithFrame:CGRectZero topMargin:topMargin]); |
| 169 [rightCard_ setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| 170 [leftCard_ setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| 171 [self addSubview:rightCard_]; |
| 172 [self addSubview:leftCard_]; |
| 173 AddSameSizeConstraint(rightCard_, self); |
| 174 AddSameSizeConstraint(leftCard_, self); |
| 175 } |
| 176 return self; |
| 177 } |
| 178 |
| 179 - (CGRect)cardFrame { |
| 180 return self.bounds; |
| 181 } |
| 182 |
| 183 // Set up left and right card views depending on current tab and swipe |
| 184 // direction. |
| 185 - (void)updateViewsForDirection:(UISwipeGestureRecognizerDirection)direction |
| 186 withToolbar:(WebToolbarController*)tbc { |
| 187 direction_ = direction; |
| 188 CGRect cardFrame = [self cardFrame]; |
| 189 NSUInteger currentIndex = [model_ indexOfTab:model_.currentTab]; |
| 190 CGFloat offset = UseRTLLayout() ? -1 : 1; |
| 191 if (direction_ == UISwipeGestureRecognizerDirectionRight) { |
| 192 [self setupCard:rightCard_ withIndex:currentIndex withToolbar:tbc]; |
| 193 [rightCard_ setFrame:cardFrame]; |
| 194 [self setupCard:leftCard_ withIndex:currentIndex - offset withToolbar:tbc]; |
| 195 cardFrame.origin.x -= cardFrame.size.width + kCardHorizontalSpacing; |
| 196 [leftCard_ setFrame:cardFrame]; |
| 197 } else { |
| 198 [self setupCard:leftCard_ withIndex:currentIndex withToolbar:tbc]; |
| 199 [leftCard_ setFrame:cardFrame]; |
| 200 [self setupCard:rightCard_ withIndex:currentIndex + offset withToolbar:tbc]; |
| 201 cardFrame.origin.x += cardFrame.size.width + kCardHorizontalSpacing; |
| 202 [rightCard_ setFrame:cardFrame]; |
| 203 } |
| 204 [tbc resetToolbarAfterSideSwipeSnapshot]; |
| 205 } |
| 206 |
| 207 - (UIImage*)smallGreyImage:(UIImage*)image { |
| 208 CGRect smallSize = CGRectMake(0, 0, image.size.width / kResizeFactor, |
| 209 image.size.height / kResizeFactor); |
| 210 // Using CIFilter here on iOS 5+ might be faster, but it doesn't easily |
| 211 // allow for resizing. At the max size, it's still too slow for side swipe. |
| 212 UIGraphicsBeginImageContextWithOptions(smallSize.size, YES, 0); |
| 213 [image drawInRect:smallSize blendMode:kCGBlendModeLuminosity alpha:1.0]; |
| 214 UIImage* greyImage = UIGraphicsGetImageFromCurrentImageContext(); |
| 215 UIGraphicsEndImageContext(); |
| 216 return greyImage; |
| 217 } |
| 218 |
| 219 // Create card view based on TabModel index. |
| 220 - (void)setupCard:(SwipeView*)card |
| 221 withIndex:(NSInteger)index |
| 222 withToolbar:(WebToolbarController*)toolbarController { |
| 223 if (index < 0 || index >= (NSInteger)[model_ count]) { |
| 224 [card setHidden:YES]; |
| 225 return; |
| 226 } |
| 227 [card setHidden:NO]; |
| 228 |
| 229 Tab* tab = [model_ tabAtIndex:index]; |
| 230 BOOL isNTP = tab.url.host() == kChromeUINewTabHost; |
| 231 [toolbarController updateToolbarForSideSwipeSnapshot:tab]; |
| 232 UIImage* toolbarView = CaptureViewWithOption([toolbarController view], |
| 233 [[UIScreen mainScreen] scale], |
| 234 kClientSideRendering); |
| 235 [card setToolbarImage:toolbarView isNewTabPage:isNTP]; |
| 236 |
| 237 // Converting snapshotted images to grey takes too much time for single core |
| 238 // devices. Instead, show the colored image for single core devices and the |
| 239 // grey image for multi core devices. |
| 240 dispatch_queue_t priorityQueue = |
| 241 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul); |
| 242 [tab retrieveSnapshot:^(UIImage* image) { |
| 243 if (tab.webController.usePlaceholderOverlay && |
| 244 !ios::device_util::IsSingleCoreDevice()) { |
| 245 [card setImage:[CRWWebController defaultSnapshotImage]]; |
| 246 dispatch_async(priorityQueue, ^{ |
| 247 UIImage* greyImage = [self smallGreyImage:image]; |
| 248 dispatch_async(dispatch_get_main_queue(), ^{ |
| 249 [card setImage:greyImage]; |
| 250 }); |
| 251 }); |
| 252 } else { |
| 253 [card setImage:image]; |
| 254 } |
| 255 }]; |
| 256 } |
| 257 |
| 258 // Place cards around |currentPoint_.x|, and lean towards each card near the |
| 259 // X edges of |bounds|. Shrink cards as they are dragged towards the middle of |
| 260 // the |bounds|, and edge cards only drag |kEdgeCardDragPercentage| of |bounds|. |
| 261 - (void)updateCardPositions { |
| 262 CGRect bounds = [self cardFrame]; |
| 263 [rightCard_ setFrame:bounds]; |
| 264 [leftCard_ setFrame:bounds]; |
| 265 |
| 266 CGFloat width = CGRectGetWidth([self cardFrame]); |
| 267 CGPoint center = CGPointMake(bounds.origin.x + bounds.size.width / 2, |
| 268 bounds.origin.y + bounds.size.height / 2); |
| 269 if ([self isEdgeSwipe]) { |
| 270 // If an edge card, don't allow the card to be dragged across the screen. |
| 271 // Instead, drag across |kEdgeCardDragPercentage| of the screen. |
| 272 center.x = currentPoint_.x - width / 2 - |
| 273 (currentPoint_.x - width) / width * |
| 274 (width * (1 - kEdgeCardDragPercentage)); |
| 275 [leftCard_ setCenter:center]; |
| 276 center.x = currentPoint_.x / width * (width * kEdgeCardDragPercentage) + |
| 277 bounds.size.width / 2; |
| 278 [rightCard_ setCenter:center]; |
| 279 } else { |
| 280 // Place cards around the finger as it is dragged across the screen. |
| 281 // Place the finger between the cards in the middle of the screen, on the |
| 282 // right card border when on the left side of the screen, and on the left |
| 283 // card border when on the right side of the screen. |
| 284 CGFloat rightXBuffer = kCardHorizontalSpacing * currentPoint_.x / width; |
| 285 CGFloat leftXBuffer = kCardHorizontalSpacing - rightXBuffer; |
| 286 |
| 287 center.x = currentPoint_.x - leftXBuffer - width / 2; |
| 288 [leftCard_ setCenter:center]; |
| 289 |
| 290 center.x = currentPoint_.x + rightXBuffer + width / 2; |
| 291 [rightCard_ setCenter:center]; |
| 292 } |
| 293 } |
| 294 |
| 295 // Update layout with new touch event. |
| 296 - (void)handleHorizontalPan:(SideSwipeGestureRecognizer*)gesture { |
| 297 currentPoint_ = [gesture locationInView:self]; |
| 298 currentPoint_.x -= gesture.swipeOffset; |
| 299 |
| 300 // Since it's difficult to touch the very edge of the screen (touches tend to |
| 301 // sit near x ~ 4), push the touch towards the edge. |
| 302 CGFloat width = CGRectGetWidth([self cardFrame]); |
| 303 CGFloat half = floor(width / 2); |
| 304 CGFloat padding = floor(std::abs(currentPoint_.x - half) / half); |
| 305 |
| 306 // Push towards the edges. |
| 307 if (currentPoint_.x > half) |
| 308 currentPoint_.x += padding; |
| 309 else |
| 310 currentPoint_.x -= padding; |
| 311 |
| 312 // But don't go past the edges. |
| 313 if (currentPoint_.x < 0) |
| 314 currentPoint_.x = 0; |
| 315 else if (currentPoint_.x > width) |
| 316 currentPoint_.x = width; |
| 317 |
| 318 [self updateCardPositions]; |
| 319 |
| 320 if (gesture.state == UIGestureRecognizerStateEnded || |
| 321 gesture.state == UIGestureRecognizerStateCancelled || |
| 322 gesture.state == UIGestureRecognizerStateFailed) { |
| 323 [self finishPan]; |
| 324 } |
| 325 } |
| 326 |
| 327 - (BOOL)isEdgeSwipe { |
| 328 NSUInteger currentIndex = [model_ indexOfTab:model_.currentTab]; |
| 329 return (IsSwipingBack(direction_) && currentIndex == 0) || |
| 330 (IsSwipingForward(direction_) && currentIndex == [model_ count] - 1); |
| 331 } |
| 332 |
| 333 // Update the current tab and animate the proper card view if the |
| 334 // |currentPoint_| is past the center of |bounds|. |
| 335 - (void)finishPan { |
| 336 NSUInteger currentIndex = [model_ indexOfTab:model_.currentTab]; |
| 337 // Something happened and now currentTab is gone. End card side swipe and let |
| 338 // BVC show no tabs UI. |
| 339 if (currentIndex == NSNotFound) |
| 340 return [delegate_ sideSwipeViewDismissAnimationDidEnd:self]; |
| 341 |
| 342 CGRect finalSize = [self cardFrame]; |
| 343 CGFloat width = CGRectGetWidth([self cardFrame]); |
| 344 CGRect leftFrame, rightFrame; |
| 345 SwipeView* dominantCard; |
| 346 Tab* destinationTab = model_.currentTab; |
| 347 CGFloat offset = UseRTLLayout() ? -1 : 1; |
| 348 if (direction_ == UISwipeGestureRecognizerDirectionRight) { |
| 349 // If swipe is right and |currentPoint_.x| is over the first 1/3, move left. |
| 350 if (currentPoint_.x > width / 3.0 && ![self isEdgeSwipe]) { |
| 351 destinationTab = [model_ tabAtIndex:currentIndex - offset]; |
| 352 dominantCard = leftCard_; |
| 353 rightFrame = leftFrame = finalSize; |
| 354 rightFrame.origin.x += rightFrame.size.width + kCardHorizontalSpacing; |
| 355 base::RecordAction(UserMetricsAction("MobileStackSwipeCompleted")); |
| 356 } else { |
| 357 dominantCard = rightCard_; |
| 358 leftFrame = rightFrame = finalSize; |
| 359 leftFrame.origin.x -= rightFrame.size.width + kCardHorizontalSpacing; |
| 360 base::RecordAction(UserMetricsAction("MobileStackSwipeCancelled")); |
| 361 } |
| 362 } else { |
| 363 // If swipe is left and |currentPoint_.x| is over the first 1/3, move right. |
| 364 if (currentPoint_.x < (width / 3.0) * 2.0 && ![self isEdgeSwipe]) { |
| 365 destinationTab = [model_ tabAtIndex:currentIndex + offset]; |
| 366 dominantCard = rightCard_; |
| 367 leftFrame = rightFrame = finalSize; |
| 368 leftFrame.origin.x -= rightFrame.size.width + kCardHorizontalSpacing; |
| 369 base::RecordAction(UserMetricsAction("MobileStackSwipeCompleted")); |
| 370 } else { |
| 371 dominantCard = leftCard_; |
| 372 rightFrame = leftFrame = finalSize; |
| 373 rightFrame.origin.x += rightFrame.size.width + kCardHorizontalSpacing; |
| 374 base::RecordAction(UserMetricsAction("MobileStackSwipeCancelled")); |
| 375 } |
| 376 } |
| 377 |
| 378 // Changing the model even when the tab is the same at the end of the |
| 379 // animation allows the UI to recover. |
| 380 [model_ setCurrentTab:destinationTab]; |
| 381 |
| 382 // Make sure the dominant card animates on top. |
| 383 [dominantCard.superview bringSubviewToFront:dominantCard]; |
| 384 |
| 385 [UIView animateWithDuration:kAnimationDuration |
| 386 animations:^{ |
| 387 [leftCard_ setTransform:CGAffineTransformIdentity]; |
| 388 [rightCard_ setTransform:CGAffineTransformIdentity]; |
| 389 [leftCard_ setFrame:leftFrame]; |
| 390 [rightCard_ setFrame:rightFrame]; |
| 391 } |
| 392 completion:^(BOOL finished) { |
| 393 [leftCard_ setImage:nil]; |
| 394 [rightCard_ setImage:nil]; |
| 395 [leftCard_ setToolbarImage:nil isNewTabPage:NO]; |
| 396 [rightCard_ setToolbarImage:nil isNewTabPage:NO]; |
| 397 [delegate_ sideSwipeViewDismissAnimationDidEnd:self]; |
| 398 }]; |
| 399 } |
| 400 |
| 401 @end |
OLD | NEW |