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 // For performance reasons, the composition of the card frame is broken up into |
| 6 // four pieces. The overall structure of the CardView is: |
| 7 // - CardView |
| 8 // - Snapshot (UIImageView) |
| 9 // - FrameTop (UIImageView) |
| 10 // - FrameLeft (UIImageView) |
| 11 // - FrameRight (UIImageView) |
| 12 // - FrameBottom (UIImageView) |
| 13 // - CardTabView (UIView::DrawRect) |
| 14 // |
| 15 // While it would be simpler to put the frame in one transparent UIImageView, |
| 16 // that would make the entire snapshot area needlessly color-blended. Instead |
| 17 // the frame is broken up into four pieces, top, left, bottom, right. |
| 18 // |
| 19 // The frame's tab gets its own view above everything else (CardTabView) so that |
| 20 // it can be animated out. It's also transparent since the tab has a curve and |
| 21 // a shadow. |
| 22 |
| 23 #import "ios/chrome/browser/ui/stack_view/card_view.h" |
| 24 |
| 25 #import <QuartzCore/QuartzCore.h> |
| 26 #include <algorithm> |
| 27 |
| 28 #import "base/mac/foundation_util.h" |
| 29 #import "base/mac/objc_property_releaser.h" |
| 30 #import "base/mac/scoped_nsobject.h" |
| 31 #include "components/strings/grit/components_strings.h" |
| 32 #import "ios/chrome/browser/ui/animation_util.h" |
| 33 #import "ios/chrome/browser/ui/reversed_animation.h" |
| 34 #import "ios/chrome/browser/ui/rtl_geometry.h" |
| 35 #import "ios/chrome/browser/ui/stack_view/close_button.h" |
| 36 #import "ios/chrome/browser/ui/stack_view/title_label.h" |
| 37 #import "ios/chrome/browser/ui/tabs/tab_util.h" |
| 38 #import "ios/chrome/browser/ui/ui_util.h" |
| 39 #import "ios/chrome/common/material_timing.h" |
| 40 #import "ios/third_party/material_components_ios/src/components/Typography/src/M
aterialTypography.h" |
| 41 #include "ui/base/l10n/l10n_util.h" |
| 42 #include "ui/base/resource/resource_bundle.h" |
| 43 #include "ui/gfx/favicon_size.h" |
| 44 #include "ui/gfx/image/image.h" |
| 45 #import "ui/gfx/ios/uikit_util.h" |
| 46 |
| 47 using ios::material::TimingFunction; |
| 48 |
| 49 const UIEdgeInsets kCardImageInsets = {48.0, 4.0, 4.0, 4.0}; |
| 50 const CGFloat kCardFrameInset = 1.0; |
| 51 const CGFloat kCardShadowThickness = 16.0; |
| 52 const CGFloat kCardFrameCornerRadius = 4.0; |
| 53 const CGFloat kCardTabTopInset = 4; |
| 54 const CGFloat kCardFrameBackgroundBrightness = 242.0 / 255.0; |
| 55 const CGFloat kCardFrameBackgroundBrightnessIncognito = 80.0 / 255.0; |
| 56 const CGFloat kCardFrameImageSnapshotOverlap = 1.0; |
| 57 NSString* const kCardShadowImageName = @"card_frame_shadow"; |
| 58 const UIEdgeInsets kCardShadowLayoutOutsets = {-14.0, -22.0, -14.0, -22.0}; |
| 59 |
| 60 namespace { |
| 61 |
| 62 // Chrome corners and edges. |
| 63 const CGFloat kCardTabTitleMargin = 4; |
| 64 const CGFloat kCardTabFavIconLeadingMargin = 12; |
| 65 const CGFloat kCardTabFavIconCloseButtonPadding = 8.0; |
| 66 const UIEdgeInsets kCloseButtonContentInsets = {12.0, 12.0, 12.0, 12.0}; |
| 67 const UIEdgeInsets kShadowStretchInsets = {51.0, 47.0, 51.0, 47.0}; |
| 68 |
| 69 // Animation key for explicit animations added by |
| 70 // |-animateFromBeginFrame:toEndFrame:tabAnimationStyle:|. |
| 71 NSString* const kCardViewAnimationKey = @"CardViewAnimation"; |
| 72 |
| 73 // Returns the appropriate variant of the image for |image_name| based on |
| 74 // |is_incognito|. |
| 75 UIImage* ImageWithName(NSString* image_name, BOOL is_incognito) { |
| 76 NSString* name = is_incognito |
| 77 ? [image_name stringByAppendingString:@"_incognito"] |
| 78 : image_name; |
| 79 return [UIImage imageNamed:name]; |
| 80 } |
| 81 |
| 82 } // namespace |
| 83 |
| 84 #pragma mark - |
| 85 |
| 86 @interface CardTabView : UIView |
| 87 |
| 88 @property(nonatomic, assign) CardCloseButtonSide closeButtonSide; |
| 89 @property(nonatomic, retain) UIImageView* favIconView; |
| 90 @property(nonatomic, retain) UIImage* favicon; |
| 91 @property(nonatomic, retain) CloseButton* closeButton; |
| 92 @property(nonatomic, retain) TitleLabel* titleLabel; |
| 93 @property(nonatomic, assign) BOOL isIncognito; |
| 94 |
| 95 // Layout helper selectors that calculate the frames for subviews given the |
| 96 // bounds of the card tab. Note that the frames returned by these selectors |
| 97 // will be different depending on the value of the |displaySide| property. |
| 98 - (LayoutRect)faviconLayoutForBounds:(CGRect)bounds; |
| 99 - (CGRect)faviconFrameForBounds:(CGRect)bounds; |
| 100 - (LayoutRect)titleLabelLayoutForBounds:(CGRect)bounds; |
| 101 - (CGRect)titleLabelFrameForBounds:(CGRect)bounds; |
| 102 - (LayoutRect)closeButtonLayoutForBounds:(CGRect)bounds; |
| 103 - (CGRect)closeButtonFrameForBounds:(CGRect)bounds; |
| 104 |
| 105 // Adds frame animations for the favicon, title, and close button. The subviews |
| 106 // will be faded in if |tabAnimationStyle| = CardTabAnimationStyleFadeIn and |
| 107 // faded out if |tabAnimationStyle| = CardTabAnimationStyleFadeOut. |
| 108 - (void)animateFromBeginFrame:(CGRect)beginFrame |
| 109 toEndFrame:(CGRect)endFrame |
| 110 tabAnimationStyle:(CardTabAnimationStyle)tabAnimationStyle; |
| 111 |
| 112 // Called by CardView's selector of the same name and reverses animations added |
| 113 // by |-animateFromBeginFrame:toEndFrame:tabAnimationStyle:|. |
| 114 - (void)reverseAnimations; |
| 115 |
| 116 // Called by CardView's selector of the same name and removes animations added |
| 117 // by |-animateFromBeginFrame:toEndFrame:tabAnimationStyle:|. |
| 118 - (void)cleanUpAnimations; |
| 119 |
| 120 // Initialize a CardTabView with |frame| and |isIncognito| state. |
| 121 - (instancetype)initWithFrame:(CGRect)frame |
| 122 isIncognito:(BOOL)isIncognito NS_DESIGNATED_INITIALIZER; |
| 123 |
| 124 - (instancetype)initWithCoder:(NSCoder*)aDecoder NS_UNAVAILABLE; |
| 125 |
| 126 @end |
| 127 |
| 128 @implementation CardTabView { |
| 129 base::mac::ObjCPropertyReleaser _propertyReleaser_CardTabView; |
| 130 } |
| 131 |
| 132 #pragma mark - Property Implementation |
| 133 |
| 134 @synthesize closeButtonSide = _closeButtonSide; |
| 135 @synthesize favIconView = _faviconView; |
| 136 @synthesize favicon = _favicon; |
| 137 @synthesize closeButton = _closeButton; |
| 138 @synthesize titleLabel = _titleLabel; |
| 139 @synthesize isIncognito = _isIncognito; |
| 140 |
| 141 - (instancetype)initWithFrame:(CGRect)frame { |
| 142 return [self initWithFrame:frame isIncognito:NO]; |
| 143 } |
| 144 |
| 145 - (instancetype)initWithFrame:(CGRect)frame isIncognito:(BOOL)isIncognito { |
| 146 self = [super initWithFrame:frame]; |
| 147 if (!self) |
| 148 return self; |
| 149 |
| 150 _propertyReleaser_CardTabView.Init(self, [CardTabView class]); |
| 151 _isIncognito = isIncognito; |
| 152 |
| 153 UIImage* image = ImageWithName(@"default_favicon", _isIncognito); |
| 154 _faviconView = [[UIImageView alloc] initWithImage:image]; |
| 155 [self addSubview:_faviconView]; |
| 156 |
| 157 UIImage* normal = ImageWithName(@"card_close_button", _isIncognito); |
| 158 UIImage* pressed = ImageWithName(@"card_close_button_pressed", _isIncognito); |
| 159 |
| 160 _closeButton = [[CloseButton alloc] initWithFrame:CGRectZero]; |
| 161 [_closeButton setAccessibilityLabel:l10n_util::GetNSString(IDS_CLOSE)]; |
| 162 [_closeButton setImage:normal forState:UIControlStateNormal]; |
| 163 [_closeButton setImage:pressed forState:UIControlStateHighlighted]; |
| 164 [_closeButton setContentEdgeInsets:kCloseButtonContentInsets]; |
| 165 [_closeButton sizeToFit]; |
| 166 [self addSubview:_closeButton]; |
| 167 |
| 168 _titleLabel = [[TitleLabel alloc] initWithFrame:CGRectZero]; |
| 169 [_titleLabel setFont:[MDCTypography body1Font]]; |
| 170 [self addSubview:_titleLabel]; |
| 171 |
| 172 [self updateTitleColors]; |
| 173 |
| 174 return self; |
| 175 } |
| 176 |
| 177 - (instancetype)initWithCoder:(NSCoder*)aDecoder { |
| 178 NOTREACHED(); |
| 179 return nil; |
| 180 } |
| 181 |
| 182 - (void)setCloseButtonSide:(CardCloseButtonSide)closeButtonSide { |
| 183 if (_closeButtonSide != closeButtonSide) { |
| 184 _closeButtonSide = closeButtonSide; |
| 185 [self setNeedsLayout]; |
| 186 } |
| 187 } |
| 188 |
| 189 - (void)layoutSubviews { |
| 190 [super layoutSubviews]; |
| 191 self.favIconView.frame = [self faviconFrameForBounds:self.bounds]; |
| 192 self.titleLabel.frame = [self titleLabelFrameForBounds:self.bounds]; |
| 193 self.closeButton.frame = [self closeButtonFrameForBounds:self.bounds]; |
| 194 } |
| 195 |
| 196 - (LayoutRect)faviconLayoutForBounds:(CGRect)bounds { |
| 197 LayoutRect layout; |
| 198 layout.boundingWidth = CGRectGetWidth(bounds); |
| 199 layout.size = CGSizeMake(gfx::kFaviconSize, gfx::kFaviconSize); |
| 200 layout.position.originY = CGRectGetMidY(bounds) - 0.5 * layout.size.height; |
| 201 if (self.closeButtonSide == CardCloseButtonSide::LEADING) { |
| 202 // Layout |kCardTabFavIconCloseButtonPadding| from the close button's |
| 203 // trailing edge. |
| 204 layout.position.leading = |
| 205 LayoutRectGetTrailingEdge([self closeButtonLayoutForBounds:bounds]) + |
| 206 kCardTabFavIconCloseButtonPadding; |
| 207 } else { |
| 208 // Layout |kCardTabFavIconLeadingMargin| from the leading edge of the |
| 209 // bounds. |
| 210 layout.position.leading = kCardTabFavIconLeadingMargin; |
| 211 } |
| 212 return layout; |
| 213 } |
| 214 |
| 215 - (CGRect)faviconFrameForBounds:(CGRect)bounds { |
| 216 return AlignRectOriginAndSizeToPixels( |
| 217 LayoutRectGetRect([self faviconLayoutForBounds:bounds])); |
| 218 } |
| 219 |
| 220 - (LayoutRect)titleLabelLayoutForBounds:(CGRect)bounds { |
| 221 LayoutRect layout; |
| 222 layout.boundingWidth = CGRectGetWidth(bounds); |
| 223 layout.size = [self.titleLabel sizeThatFits:bounds.size]; |
| 224 layout.position.originY = CGRectGetMidY(bounds) - 0.5 * layout.size.height; |
| 225 const CGFloat titlePadding = 2.0 * kCardTabTitleMargin; |
| 226 CGFloat faviconTrailingEdge = |
| 227 LayoutRectGetTrailingEdge([self faviconLayoutForBounds:bounds]); |
| 228 CGFloat maxWidth = CGFLOAT_MAX; |
| 229 if (self.closeButtonSide == CardCloseButtonSide::LEADING) { |
| 230 // Layout between the favicon and the trailing edge of the bounds. |
| 231 maxWidth = CGRectGetMaxX(bounds) - faviconTrailingEdge - titlePadding; |
| 232 } else { |
| 233 // Lay out between the favicon and the close button. |
| 234 maxWidth = [self closeButtonLayoutForBounds:bounds].position.leading - |
| 235 faviconTrailingEdge - titlePadding; |
| 236 } |
| 237 layout.size.width = std::min(layout.size.width, maxWidth); |
| 238 layout.position.leading = faviconTrailingEdge + kCardTabTitleMargin; |
| 239 return layout; |
| 240 } |
| 241 |
| 242 - (CGRect)titleLabelFrameForBounds:(CGRect)bounds { |
| 243 return AlignRectOriginAndSizeToPixels( |
| 244 LayoutRectGetRect([self titleLabelLayoutForBounds:bounds])); |
| 245 } |
| 246 |
| 247 - (LayoutRect)closeButtonLayoutForBounds:(CGRect)bounds { |
| 248 LayoutRect layout; |
| 249 layout.boundingWidth = CGRectGetWidth(bounds); |
| 250 layout.size = [self.closeButton sizeThatFits:bounds.size]; |
| 251 layout.position.originY = CGRectGetMidY(bounds) - 0.5 * layout.size.height; |
| 252 layout.position.leading = self.closeButtonSide == CardCloseButtonSide::LEADING |
| 253 ? 0.0 |
| 254 : layout.boundingWidth - layout.size.width; |
| 255 return layout; |
| 256 } |
| 257 |
| 258 - (CGRect)closeButtonFrameForBounds:(CGRect)bounds { |
| 259 return AlignRectOriginAndSizeToPixels( |
| 260 LayoutRectGetRect([self closeButtonLayoutForBounds:bounds])); |
| 261 } |
| 262 |
| 263 - (CGRect)closeButtonRect { |
| 264 return [_closeButton frame]; |
| 265 } |
| 266 |
| 267 - (void)setTitle:(NSString*)title { |
| 268 [_titleLabel setText:title]; |
| 269 [_closeButton setAccessibilityValue:title]; |
| 270 |
| 271 [self setNeedsUpdateConstraints]; |
| 272 } |
| 273 |
| 274 - (void)setFavicon:(UIImage*)favicon { |
| 275 if (favicon != _favicon) { |
| 276 [favicon retain]; |
| 277 [_favicon release]; |
| 278 _favicon = favicon; |
| 279 [self updateFaviconImage]; |
| 280 } |
| 281 } |
| 282 |
| 283 - (void)updateTitleColors { |
| 284 UIColor* titleColor = [UIColor blackColor]; |
| 285 if (_isIncognito) |
| 286 titleColor = [UIColor whiteColor]; |
| 287 |
| 288 [_titleLabel setTextColor:titleColor]; |
| 289 } |
| 290 |
| 291 - (void)updateFaviconImage { |
| 292 UIImage* favicon = _favicon; |
| 293 if (!favicon) |
| 294 favicon = ImageWithName(@"default_favicon", _isIncognito); |
| 295 |
| 296 [_faviconView setImage:favicon]; |
| 297 |
| 298 [self setNeedsUpdateConstraints]; |
| 299 } |
| 300 |
| 301 - (void)animateFromBeginFrame:(CGRect)beginFrame |
| 302 toEndFrame:(CGRect)endFrame |
| 303 tabAnimationStyle:(CardTabAnimationStyle)tabAnimationStyle { |
| 304 // Animation values. |
| 305 CAAnimation* frameAnimation = nil; |
| 306 CFTimeInterval frameDuration = ios::material::kDuration1; |
| 307 CAMediaTimingFunction* frameTiming = |
| 308 TimingFunction(ios::material::CurveEaseInOut); |
| 309 CGRect beginBounds = {CGPointZero, beginFrame.size}; |
| 310 CGRect endBounds = {CGPointZero, endFrame.size}; |
| 311 BOOL shouldAnimateFade = (tabAnimationStyle != CARD_TAB_ANIMATION_STYLE_NONE); |
| 312 BOOL shouldFadeIn = (tabAnimationStyle == CARD_TAB_ANIMATION_STYLE_FADE_IN); |
| 313 CAAnimation* opacityAnimation = nil; |
| 314 CAMediaTimingFunction* opacityTiming = nil; |
| 315 if (shouldAnimateFade) { |
| 316 opacityTiming = TimingFunction(shouldFadeIn ? ios::material::CurveEaseOut |
| 317 : ios::material::CurveEaseIn); |
| 318 } |
| 319 CGFloat beginOpacity = shouldFadeIn ? 0.0 : 1.0; |
| 320 CGFloat endOpacity = shouldFadeIn ? 1.0 : 0.0; |
| 321 CAAnimation* animation = nil; |
| 322 |
| 323 // Update layer geometry. |
| 324 frameAnimation = FrameAnimationMake(self.layer, beginFrame, endFrame); |
| 325 frameAnimation.duration = frameDuration; |
| 326 frameAnimation.timingFunction = frameTiming; |
| 327 [self.layer addAnimation:frameAnimation forKey:kCardViewAnimationKey]; |
| 328 |
| 329 // Update favicon. |
| 330 frameAnimation = FrameAnimationMake(_faviconView.layer, |
| 331 [self faviconFrameForBounds:beginBounds], |
| 332 [self faviconFrameForBounds:endBounds]); |
| 333 frameAnimation.duration = frameDuration; |
| 334 frameAnimation.timingFunction = frameTiming; |
| 335 if (shouldAnimateFade) { |
| 336 opacityAnimation = OpacityAnimationMake(beginOpacity, endOpacity); |
| 337 opacityAnimation.duration = ios::material::kDuration8; |
| 338 opacityAnimation.timingFunction = opacityTiming; |
| 339 opacityAnimation.beginTime = shouldFadeIn ? ios::material::kDuration8 : 0.0; |
| 340 animation = AnimationGroupMake(@[ frameAnimation, opacityAnimation ]); |
| 341 } else { |
| 342 animation = frameAnimation; |
| 343 } |
| 344 [_faviconView.layer addAnimation:animation forKey:kCardViewAnimationKey]; |
| 345 |
| 346 // Update close button. |
| 347 frameAnimation = FrameAnimationMake( |
| 348 _closeButton.layer, [self closeButtonFrameForBounds:beginBounds], |
| 349 [self closeButtonFrameForBounds:endBounds]); |
| 350 frameAnimation.duration = frameDuration; |
| 351 frameAnimation.timingFunction = frameTiming; |
| 352 if (shouldAnimateFade) { |
| 353 opacityAnimation = OpacityAnimationMake(beginOpacity, endOpacity); |
| 354 opacityAnimation.timingFunction = opacityTiming; |
| 355 opacityAnimation.duration = |
| 356 shouldFadeIn ? ios::material::kDuration1 : ios::material::kDuration8; |
| 357 opacityAnimation.beginTime = shouldFadeIn ? ios::material::kDuration1 : 0.0; |
| 358 animation = AnimationGroupMake(@[ frameAnimation, opacityAnimation ]); |
| 359 } else { |
| 360 animation = frameAnimation; |
| 361 } |
| 362 [_closeButton.layer addAnimation:animation forKey:kCardViewAnimationKey]; |
| 363 |
| 364 // Update title. |
| 365 [_titleLabel animateFromBeginFrame:[self titleLabelFrameForBounds:beginBounds] |
| 366 toEndFrame:[self titleLabelFrameForBounds:endBounds] |
| 367 duration:frameDuration |
| 368 timingFunction:frameTiming]; |
| 369 if (shouldAnimateFade) { |
| 370 opacityAnimation = OpacityAnimationMake(beginOpacity, endOpacity); |
| 371 opacityAnimation.timingFunction = opacityTiming; |
| 372 opacityAnimation.duration = |
| 373 shouldFadeIn ? ios::material::kDuration6 : ios::material::kDuration8; |
| 374 CFTimeInterval delay = shouldFadeIn ? ios::material::kDuration2 : 0.0; |
| 375 opacityAnimation = DelayedAnimationMake(opacityAnimation, delay); |
| 376 [_titleLabel.layer addAnimation:opacityAnimation |
| 377 forKey:kCardViewAnimationKey]; |
| 378 } |
| 379 } |
| 380 |
| 381 - (void)reverseAnimations { |
| 382 [_titleLabel reverseAnimations]; |
| 383 ReverseAnimationsForKeyForLayers(kCardViewAnimationKey, @[ |
| 384 self.layer, _faviconView.layer, _closeButton.layer, _titleLabel.layer |
| 385 ]); |
| 386 } |
| 387 |
| 388 - (void)cleanUpAnimations { |
| 389 [_titleLabel cleanUpAnimations]; |
| 390 RemoveAnimationForKeyFromLayers(kCardViewAnimationKey, @[ |
| 391 self.layer, _faviconView.layer, _closeButton.layer, _titleLabel.layer |
| 392 ]); |
| 393 } |
| 394 |
| 395 // Implementation of this protocol forces VoiceOver to read the titleLabel and |
| 396 // close button in the correct order (top left to bottom right, by row). Because |
| 397 // the top border the close button's focus area is higher than the label's, in |
| 398 // portrait mode VoiceOver automatically orders the close button first, although |
| 399 // it looks like the default behavior should read the elements from left to |
| 400 // right. |
| 401 #pragma mark - UIAccessibilityContainer methods |
| 402 |
| 403 // CardTabView accessibility elements: titleLabel and closeButton. |
| 404 - (NSInteger)accessibilityElementCount { |
| 405 return 2; |
| 406 } |
| 407 |
| 408 // Returns the leftmost accessibility element if |index| = 0 and the rightmost |
| 409 // accessibility element if |index| = 1. The display side determines the |
| 410 // location of the close button relative to the title label. |
| 411 - (id)accessibilityElementAtIndex:(NSInteger)index { |
| 412 BOOL closeButtonLeading = |
| 413 self.closeButtonSide == CardCloseButtonSide::LEADING; |
| 414 id element = nil; |
| 415 if (index == 0) |
| 416 element = closeButtonLeading ? _closeButton : _titleLabel; |
| 417 else if (index == 1) |
| 418 element = closeButtonLeading ? _titleLabel : _closeButton; |
| 419 return element; |
| 420 } |
| 421 |
| 422 // Returns 0 if element is on the left (titleLabel in portrait, closeButton in |
| 423 // landscape), and 1 if the element is on the right. Otherwise returns |
| 424 // NSNotFound. |
| 425 - (NSInteger)indexOfAccessibilityElement:(id)element { |
| 426 BOOL closeButtonLeading = |
| 427 self.closeButtonSide == CardCloseButtonSide::LEADING; |
| 428 NSInteger index = NSNotFound; |
| 429 if (element == _closeButton) |
| 430 index = closeButtonLeading ? 0 : 1; |
| 431 else if (element == _titleLabel) |
| 432 index = closeButtonLeading ? 1 : 0; |
| 433 return index; |
| 434 } |
| 435 |
| 436 @end |
| 437 |
| 438 #pragma mark - |
| 439 |
| 440 @interface CardView () { |
| 441 base::scoped_nsobject<UIImageView> _contents; |
| 442 base::scoped_nsobject<CardTabView> _tab; |
| 443 id _cardCloseTarget; // weak |
| 444 SEL _cardCloseAction; |
| 445 id _accessibilityTarget; // weak |
| 446 SEL _accessibilityAction; |
| 447 |
| 448 BOOL _isIncognito; // YES if the card should use the incognito styling. |
| 449 |
| 450 // Pieces of the card frame, split into four UIViews. |
| 451 base::scoped_nsobject<UIImageView> _frameLeft; |
| 452 base::scoped_nsobject<UIImageView> _frameRight; |
| 453 base::scoped_nsobject<UIImageView> _frameTop; |
| 454 base::scoped_nsobject<UIImageView> _frameBottom; |
| 455 base::scoped_nsobject<UIImageView> _frameShadowImageView; |
| 456 base::scoped_nsobject<CALayer> _shadowMask; |
| 457 } |
| 458 |
| 459 // The LayoutRect for the CardTabView. |
| 460 - (LayoutRect)tabLayout; |
| 461 |
| 462 // Sends |_cardCloseAction| to |_cardCloseTarget| with |self| as the sender. |
| 463 - (void)closeButtonWasTapped:(id)sender; |
| 464 |
| 465 // Resizes/zooms the snapshot to avoid stretching given the card's current |
| 466 // bounds. |
| 467 - (void)updateImageBoundsAndZoom; |
| 468 |
| 469 // If the image is reset during an animation, this is called to update the |
| 470 // snapshot's contentsRect animation for the new image. |
| 471 - (void)updateSnapshotAnimations; |
| 472 |
| 473 // The frame to use for the shadow mask for a CardVeiw with |bounds|. |
| 474 - (CGRect)shadowMaskFrameForBounds:(CGRect)bounds; |
| 475 |
| 476 // Updates the mask used for the shadow. |
| 477 - (void)updateShadowMask; |
| 478 |
| 479 // Returns the contentsRect that will correctly zoom the image's layer for the |
| 480 // given card size. |
| 481 - (CGRect)imageContentsRectForCardSize:(CGSize)cardSize; |
| 482 |
| 483 // Animates the frame image such that it no longer overlaps the image snapshot. |
| 484 - (void)animateOutFrameImageOverlap; |
| 485 // Returns the frames used for the image views for a given bounds. |
| 486 - (CGRect)frameLeftFrameForBounds:(CGRect)bounds; |
| 487 - (CGRect)frameRightFrameForBounds:(CGRect)bounds; |
| 488 - (CGRect)frameTopFrameForBounds:(CGRect)bounds; |
| 489 - (CGRect)frameBottomFrameForBounds:(CGRect)bounds; |
| 490 |
| 491 @end |
| 492 |
| 493 @implementation CardView |
| 494 |
| 495 @synthesize isActiveTab = _isActiveTab; |
| 496 @synthesize shouldShowShadow = _shouldShowShadow; |
| 497 @synthesize shouldMaskShadow = _shouldMaskShadow; |
| 498 |
| 499 - (instancetype)initWithFrame:(CGRect)frame { |
| 500 return [self initWithFrame:frame isIncognito:NO]; |
| 501 } |
| 502 |
| 503 - (instancetype)initWithFrame:(CGRect)frame isIncognito:(BOOL)isIncognito { |
| 504 self = [super initWithFrame:frame]; |
| 505 if (!self) |
| 506 return self; |
| 507 |
| 508 _isIncognito = isIncognito; |
| 509 CGRect bounds = self.bounds; |
| 510 |
| 511 self.opaque = NO; |
| 512 self.contentMode = UIViewContentModeRedraw; |
| 513 |
| 514 CGRect shadowFrame = UIEdgeInsetsInsetRect(bounds, kCardShadowLayoutOutsets); |
| 515 _frameShadowImageView.reset([[UIImageView alloc] initWithFrame:shadowFrame]); |
| 516 [_frameShadowImageView |
| 517 setAutoresizingMask:(UIViewAutoresizingFlexibleWidth | |
| 518 UIViewAutoresizingFlexibleHeight)]; |
| 519 [self addSubview:_frameShadowImageView]; |
| 520 |
| 521 // Calling properties for side-effects. |
| 522 self.shouldShowShadow = YES; |
| 523 self.shouldMaskShadow = YES; |
| 524 |
| 525 UIImage* image = [UIImage imageNamed:kCardShadowImageName]; |
| 526 image = [image resizableImageWithCapInsets:kShadowStretchInsets]; |
| 527 [_frameShadowImageView setImage:image]; |
| 528 |
| 529 CGRect snapshotFrame = UIEdgeInsetsInsetRect(bounds, kCardImageInsets); |
| 530 _contents.reset([[UIImageView alloc] initWithFrame:snapshotFrame]); |
| 531 [_contents setClipsToBounds:YES]; |
| 532 [_contents setContentMode:UIViewContentModeScaleAspectFill]; |
| 533 [_contents setFrame:snapshotFrame]; |
| 534 [_contents setAutoresizingMask:UIViewAutoresizingFlexibleWidth | |
| 535 UIViewAutoresizingFlexibleHeight]; |
| 536 [self addSubview:_contents]; |
| 537 |
| 538 image = [UIImage imageNamed:isIncognito ? @"border_frame_incognito_left" |
| 539 : @"border_frame_left"]; |
| 540 UIEdgeInsets imageStretchInsets = UIEdgeInsetsMake( |
| 541 0.5 * image.size.height, 0.0, 0.5 * image.size.height, 0.0); |
| 542 image = [image resizableImageWithCapInsets:imageStretchInsets]; |
| 543 _frameLeft.reset([[UIImageView alloc] initWithImage:image]); |
| 544 [self addSubview:_frameLeft]; |
| 545 |
| 546 image = [UIImage imageNamed:isIncognito ? @"border_frame_incognito_right" |
| 547 : @"border_frame_right"]; |
| 548 imageStretchInsets = UIEdgeInsetsMake(0.5 * image.size.height, 0.0, |
| 549 0.5 * image.size.height, 0.0); |
| 550 image = [image resizableImageWithCapInsets:imageStretchInsets]; |
| 551 _frameRight.reset([[UIImageView alloc] initWithImage:image]); |
| 552 [self addSubview:_frameRight]; |
| 553 |
| 554 image = [UIImage imageNamed:isIncognito ? @"border_frame_incognito_top" |
| 555 : @"border_frame_top"]; |
| 556 imageStretchInsets = UIEdgeInsetsMake(0.0, 0.5 * image.size.width, 0.0, |
| 557 0.5 * image.size.width); |
| 558 image = [image resizableImageWithCapInsets:imageStretchInsets]; |
| 559 _frameTop.reset([[UIImageView alloc] initWithImage:image]); |
| 560 [self addSubview:_frameTop]; |
| 561 |
| 562 image = [UIImage imageNamed:isIncognito ? @"border_frame_incognito_bottom" |
| 563 : @"border_frame_bottom"]; |
| 564 imageStretchInsets = UIEdgeInsetsMake(0.0, 0.5 * image.size.width, 0.0, |
| 565 0.5 * image.size.width); |
| 566 image = [image resizableImageWithCapInsets:imageStretchInsets]; |
| 567 _frameBottom.reset([[UIImageView alloc] initWithImage:image]); |
| 568 [self addSubview:_frameBottom]; |
| 569 |
| 570 _tab.reset([[CardTabView alloc] |
| 571 initWithFrame:LayoutRectGetRect([self tabLayout]) |
| 572 isIncognito:_isIncognito]); |
| 573 [_tab setCloseButtonSide:IsPortrait() ? CardCloseButtonSide::TRAILING |
| 574 : CardCloseButtonSide::LEADING]; |
| 575 [[_tab closeButton] addTarget:self |
| 576 action:@selector(closeButtonWasTapped:) |
| 577 forControlEvents:UIControlEventTouchUpInside]; |
| 578 [[_tab closeButton] |
| 579 addAccessibilityElementFocusedTarget:self |
| 580 action:@selector(elementDidBecomeFocused:)]; |
| 581 [_tab closeButton].accessibilityIdentifier = [self closeButtonId]; |
| 582 |
| 583 [[_tab titleLabel] |
| 584 addAccessibilityElementFocusedTarget:self |
| 585 action:@selector(elementDidBecomeFocused:)]; |
| 586 |
| 587 [self addSubview:_tab]; |
| 588 |
| 589 self.accessibilityIdentifier = @"Tab"; |
| 590 self.isAccessibilityElement = NO; |
| 591 self.accessibilityElementsHidden = NO; |
| 592 |
| 593 return self; |
| 594 } |
| 595 |
| 596 - (instancetype)initWithCoder:(NSCoder*)aDecoder { |
| 597 NOTREACHED(); |
| 598 return nil; |
| 599 } |
| 600 |
| 601 - (void)setImage:(UIImage*)image { |
| 602 [_contents setImage:image]; |
| 603 [self updateImageBoundsAndZoom]; |
| 604 [self updateSnapshotAnimations]; |
| 605 } |
| 606 |
| 607 - (UIImage*)image { |
| 608 return [_contents image]; |
| 609 } |
| 610 |
| 611 - (void)setShouldShowShadow:(BOOL)shouldShowShadow { |
| 612 if (_shouldShowShadow != shouldShowShadow) { |
| 613 _shouldShowShadow = shouldShowShadow; |
| 614 [_frameShadowImageView setHidden:!_shouldShowShadow]; |
| 615 if (_shouldShowShadow) |
| 616 [self updateShadowMask]; |
| 617 } |
| 618 } |
| 619 |
| 620 - (void)setShouldMaskShadow:(BOOL)shouldMaskShadow { |
| 621 if (_shouldMaskShadow != shouldMaskShadow) { |
| 622 _shouldMaskShadow = shouldMaskShadow; |
| 623 if (self.shouldShowShadow) |
| 624 [self updateShadowMask]; |
| 625 } |
| 626 } |
| 627 |
| 628 - (void)setCloseButtonSide:(CardCloseButtonSide)closeButtonSide { |
| 629 if ([_tab closeButtonSide] == closeButtonSide) |
| 630 return; |
| 631 [_tab setCloseButtonSide:closeButtonSide]; |
| 632 } |
| 633 |
| 634 - (CardCloseButtonSide)closeButtonSide { |
| 635 return [_tab closeButtonSide]; |
| 636 } |
| 637 |
| 638 - (void)setTitle:(NSString*)title { |
| 639 [_tab setTitle:title]; |
| 640 } |
| 641 |
| 642 - (TitleLabel*)titleLabel { |
| 643 return [_tab titleLabel]; |
| 644 } |
| 645 |
| 646 - (void)setFavicon:(UIImage*)favicon { |
| 647 [_tab setFavicon:favicon]; |
| 648 } |
| 649 |
| 650 - (void)closeButtonWasTapped:(id)sender { |
| 651 [_cardCloseTarget performSelector:_cardCloseAction withObject:self]; |
| 652 // Disable the tab's close button to prevent touch handling from the button |
| 653 // while it's animating closed. |
| 654 [_tab closeButton].enabled = NO; |
| 655 } |
| 656 |
| 657 - (void)addCardCloseTarget:(id)target action:(SEL)action { |
| 658 DCHECK(!target || [target respondsToSelector:action]); |
| 659 _cardCloseTarget = target; |
| 660 _cardCloseAction = action; |
| 661 } |
| 662 |
| 663 - (CGRect)closeButtonFrame { |
| 664 CGRect frame = [_tab closeButtonRect]; |
| 665 return [self convertRect:frame fromView:_tab]; |
| 666 } |
| 667 |
| 668 - (NSString*)closeButtonId { |
| 669 return [NSString stringWithFormat:@"%p", [_tab closeButton]]; |
| 670 } |
| 671 |
| 672 - (CGRect)imageContentsRectForCardSize:(CGSize)cardSize { |
| 673 CGRect cardBounds = {CGPointZero, cardSize}; |
| 674 CGSize viewSize = UIEdgeInsetsInsetRect(cardBounds, kCardImageInsets).size; |
| 675 CGSize imageSize = [_contents image].size; |
| 676 CGFloat zoomRatio = std::max(viewSize.height / imageSize.height, |
| 677 viewSize.width / imageSize.width); |
| 678 return CGRectMake(0.0, 0.0, viewSize.width / (zoomRatio * imageSize.width), |
| 679 viewSize.height / (zoomRatio * imageSize.height)); |
| 680 } |
| 681 |
| 682 - (void)updateImageBoundsAndZoom { |
| 683 UIImageView* imageView = _contents.get(); |
| 684 DCHECK(!CGRectEqualToRect(self.bounds, CGRectZero)); |
| 685 |
| 686 imageView.frame = UIEdgeInsetsInsetRect(self.bounds, kCardImageInsets); |
| 687 if (imageView.image) { |
| 688 // Zoom the image to fill the available space. |
| 689 imageView.layer.contentsRect = |
| 690 [self imageContentsRectForCardSize:self.bounds.size]; |
| 691 } |
| 692 } |
| 693 |
| 694 - (void)updateSnapshotAnimations { |
| 695 CAAnimation* snapshotAnimation = |
| 696 [[_contents layer] animationForKey:kCardViewAnimationKey]; |
| 697 if (!snapshotAnimation) |
| 698 return; |
| 699 |
| 700 // Create copy of animation (animations become immutable after they're added |
| 701 // to the layer). |
| 702 base::scoped_nsobject<CAAnimationGroup> updatedAnimation( |
| 703 static_cast<CAAnimationGroup*>([snapshotAnimation copy])); |
| 704 // Extract begin and end sizes of the card. |
| 705 CAAnimation* cardAnimation = |
| 706 [self.layer animationForKey:kCardViewAnimationKey]; |
| 707 CABasicAnimation* cardBoundsAnimation = |
| 708 FindAnimationForKeyPath(@"bounds", cardAnimation); |
| 709 CGSize beginCardSize = [cardBoundsAnimation.fromValue CGRectValue].size; |
| 710 CGSize endCardSize = [cardBoundsAnimation.toValue CGRectValue].size; |
| 711 // Update the contentsRect animation. |
| 712 CABasicAnimation* contentsRectAnimation = |
| 713 FindAnimationForKeyPath(@"contentsRect", updatedAnimation); |
| 714 contentsRectAnimation.fromValue = [NSValue |
| 715 valueWithCGRect:[self imageContentsRectForCardSize:beginCardSize]]; |
| 716 contentsRectAnimation.toValue = |
| 717 [NSValue valueWithCGRect:[self imageContentsRectForCardSize:endCardSize]]; |
| 718 // Replace with updated animation. |
| 719 [[_contents layer] removeAnimationForKey:kCardViewAnimationKey]; |
| 720 [[_contents layer] addAnimation:updatedAnimation |
| 721 forKey:kCardViewAnimationKey]; |
| 722 } |
| 723 |
| 724 - (CGRect)shadowMaskFrameForBounds:(CGRect)bounds { |
| 725 CGRect shadowFrame = UIEdgeInsetsInsetRect(bounds, kCardShadowLayoutOutsets); |
| 726 LayoutRect maskLayout = LayoutRectZero; |
| 727 maskLayout.size = shadowFrame.size; |
| 728 maskLayout.boundingWidth = maskLayout.size.width; |
| 729 if (IsPortrait()) { |
| 730 maskLayout.position.leading = |
| 731 -UIEdgeInsetsGetLeading(kCardShadowLayoutOutsets); |
| 732 maskLayout.size.width = CGRectGetWidth(bounds); |
| 733 maskLayout.size.height = |
| 734 kCardFrameCornerRadius + kCardFrameInset - kCardShadowLayoutOutsets.top; |
| 735 } else { |
| 736 maskLayout.position.originY = -kCardShadowLayoutOutsets.top; |
| 737 maskLayout.size.width = kCardFrameCornerRadius + kCardFrameInset - |
| 738 UIEdgeInsetsGetLeading(kCardShadowLayoutOutsets); |
| 739 maskLayout.size.height = CGRectGetHeight(bounds); |
| 740 } |
| 741 return LayoutRectGetRect(maskLayout); |
| 742 } |
| 743 |
| 744 - (void)updateShadowMask { |
| 745 if (!self.shouldShowShadow) |
| 746 return; |
| 747 |
| 748 if (self.shouldMaskShadow) { |
| 749 if (!_shadowMask) { |
| 750 _shadowMask.reset([[CALayer alloc] init]); |
| 751 [_shadowMask setBackgroundColor:[UIColor blackColor].CGColor]; |
| 752 } |
| 753 [_frameShadowImageView layer].mask = _shadowMask; |
| 754 [_shadowMask setFrame:[self shadowMaskFrameForBounds:self.bounds]]; |
| 755 } else { |
| 756 [_frameShadowImageView layer].mask = nil; |
| 757 } |
| 758 } |
| 759 |
| 760 - (LayoutRect)tabLayout { |
| 761 LayoutRect tabLayout; |
| 762 tabLayout.position.leading = kCardFrameInset; |
| 763 tabLayout.position.originY = kCardTabTopInset; |
| 764 tabLayout.boundingWidth = CGRectGetWidth(self.bounds); |
| 765 tabLayout.size.width = tabLayout.boundingWidth - 2.0 * kCardFrameInset; |
| 766 tabLayout.size.height = kCardImageInsets.top - kCardTabTopInset; |
| 767 return tabLayout; |
| 768 } |
| 769 |
| 770 - (void)layoutSubviews { |
| 771 [_tab setFrame:LayoutRectGetRect([self tabLayout])]; |
| 772 |
| 773 [_tab setFrame:LayoutRectGetRect([self tabLayout])]; |
| 774 [_frameLeft setFrame:[self frameLeftFrameForBounds:self.bounds]]; |
| 775 [_frameRight setFrame:[self frameRightFrameForBounds:self.bounds]]; |
| 776 [_frameTop setFrame:[self frameTopFrameForBounds:self.bounds]]; |
| 777 [_frameBottom setFrame:[self frameBottomFrameForBounds:self.bounds]]; |
| 778 |
| 779 [self updateImageBoundsAndZoom]; |
| 780 [self updateShadowMask]; |
| 781 } |
| 782 |
| 783 - (CGRect)frameLeftFrameForBounds:(CGRect)bounds { |
| 784 return AlignRectToPixel(CGRectMake( |
| 785 bounds.origin.x, bounds.origin.y + kCardImageInsets.top, |
| 786 [_frameLeft image].size.width, |
| 787 bounds.size.height - kCardImageInsets.top - kCardImageInsets.bottom)); |
| 788 } |
| 789 |
| 790 - (CGRect)frameRightFrameForBounds:(CGRect)bounds { |
| 791 CGSize size = ui::AlignSizeToUpperPixel(CGSizeMake( |
| 792 [_frameRight image].size.width, |
| 793 bounds.size.height - kCardImageInsets.top - kCardImageInsets.bottom)); |
| 794 CGFloat rightEdge = CGRectGetMaxX([self frameTopFrameForBounds:bounds]); |
| 795 CGPoint origin = CGPointMake(rightEdge - size.width, |
| 796 bounds.origin.y + kCardImageInsets.top); |
| 797 return {origin, size}; |
| 798 } |
| 799 |
| 800 - (CGRect)frameTopFrameForBounds:(CGRect)bounds { |
| 801 return AlignRectToPixel(CGRectMake(bounds.origin.x, bounds.origin.y, |
| 802 bounds.size.width, |
| 803 [_frameTop image].size.height)); |
| 804 } |
| 805 |
| 806 - (CGRect)frameBottomFrameForBounds:(CGRect)bounds { |
| 807 CGFloat imageHeight = [_frameBottom image].size.height; |
| 808 return AlignRectToPixel(CGRectMake(bounds.origin.x, |
| 809 CGRectGetMaxY(bounds) - imageHeight, |
| 810 bounds.size.width, imageHeight)); |
| 811 } |
| 812 |
| 813 - (void)setTabOpacity:(CGFloat)opacity { |
| 814 [_tab setAlpha:opacity]; |
| 815 } |
| 816 |
| 817 - (NSString*)accessibilityValue { |
| 818 return self.isActiveTab ? @"active" : @"inactive"; |
| 819 } |
| 820 |
| 821 // Accounts for the fact that the tab's close button can extend beyond the |
| 822 // bounds of the card. |
| 823 - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { |
| 824 if ([super pointInside:point withEvent:event]) |
| 825 return YES; |
| 826 CGPoint convertedPoint = [self convertPoint:point toView:_tab]; |
| 827 if ([_tab pointInside:convertedPoint withEvent:event]) |
| 828 return YES; |
| 829 return NO; |
| 830 } |
| 831 |
| 832 - (void)installDummyToolbarBackgroundView:(UIView*)dummyToolbarBackgroundView { |
| 833 [_tab insertSubview:dummyToolbarBackgroundView atIndex:0]; |
| 834 } |
| 835 |
| 836 - (CGRect)dummyToolbarFrameForRect:(CGRect)rect inView:(UIView*)view { |
| 837 return [_tab convertRect:rect fromView:view]; |
| 838 } |
| 839 |
| 840 - (void)animateOutFrameImageOverlap { |
| 841 // Calculate end frame for image. Applying the inverse of |kCardImageInsets| |
| 842 // on the content snapshot's frame results in an overlap of |
| 843 // |kCardFrameImageSnapshotOverlap|, so this needs to be included in the |
| 844 // outsets. |
| 845 CGRect contentFrame = [[_contents layer].presentationLayer frame]; |
| 846 UIEdgeInsets endFrameOutsets = UIEdgeInsetsMake( |
| 847 -(kCardFrameImageSnapshotOverlap + kCardImageInsets.top), |
| 848 -(kCardFrameImageSnapshotOverlap + kCardImageInsets.left), |
| 849 -(kCardFrameImageSnapshotOverlap + kCardImageInsets.bottom), |
| 850 -(kCardFrameImageSnapshotOverlap + kCardImageInsets.right)); |
| 851 CGRect endBounds = UIEdgeInsetsInsetRect(contentFrame, endFrameOutsets); |
| 852 |
| 853 // Remove old frame animation and add new overlap animation. |
| 854 CALayer* frameLayer = [_frameLeft layer]; |
| 855 CGRect beginFrame = [frameLayer.presentationLayer frame]; |
| 856 CGRect endFrame = [self frameLeftFrameForBounds:endBounds]; |
| 857 [frameLayer removeAnimationForKey:kCardViewAnimationKey]; |
| 858 CAAnimation* frameAnimation = |
| 859 FrameAnimationMake(frameLayer, beginFrame, endFrame); |
| 860 frameAnimation.duration = ios::material::kDuration2; |
| 861 [frameLayer addAnimation:frameAnimation forKey:kCardViewAnimationKey]; |
| 862 |
| 863 frameLayer = [_frameRight layer]; |
| 864 beginFrame = [frameLayer.presentationLayer frame]; |
| 865 endFrame = [self frameRightFrameForBounds:endBounds]; |
| 866 [frameLayer removeAnimationForKey:kCardViewAnimationKey]; |
| 867 frameAnimation = FrameAnimationMake(frameLayer, beginFrame, endFrame); |
| 868 frameAnimation.duration = ios::material::kDuration2; |
| 869 [frameLayer addAnimation:frameAnimation forKey:kCardViewAnimationKey]; |
| 870 |
| 871 frameLayer = [_frameTop layer]; |
| 872 beginFrame = [frameLayer.presentationLayer frame]; |
| 873 endFrame = [self frameTopFrameForBounds:endBounds]; |
| 874 [frameLayer removeAnimationForKey:kCardViewAnimationKey]; |
| 875 frameAnimation = FrameAnimationMake(frameLayer, beginFrame, endFrame); |
| 876 frameAnimation.duration = ios::material::kDuration2; |
| 877 [frameLayer addAnimation:frameAnimation forKey:kCardViewAnimationKey]; |
| 878 |
| 879 frameLayer = [_frameBottom layer]; |
| 880 beginFrame = [frameLayer.presentationLayer frame]; |
| 881 endFrame = [self frameBottomFrameForBounds:endBounds]; |
| 882 [frameLayer removeAnimationForKey:kCardViewAnimationKey]; |
| 883 frameAnimation = FrameAnimationMake(frameLayer, beginFrame, endFrame); |
| 884 frameAnimation.duration = ios::material::kDuration2; |
| 885 [frameLayer addAnimation:frameAnimation forKey:kCardViewAnimationKey]; |
| 886 } |
| 887 |
| 888 - (void)animateFromBeginFrame:(CGRect)beginFrame |
| 889 toEndFrame:(CGRect)endFrame |
| 890 tabAnimationStyle:(CardTabAnimationStyle)tabAnimationStyle { |
| 891 // Animation values |
| 892 CAAnimation* frameAnimation = nil; |
| 893 CFTimeInterval frameDuration = ios::material::kDuration1; |
| 894 CAMediaTimingFunction* frameTiming = |
| 895 TimingFunction(ios::material::CurveEaseInOut); |
| 896 |
| 897 // Update layer geometry |
| 898 frameAnimation = FrameAnimationMake(self.layer, beginFrame, endFrame); |
| 899 frameAnimation.duration = frameDuration; |
| 900 frameAnimation.timingFunction = frameTiming; |
| 901 [self.layer addAnimation:frameAnimation forKey:kCardViewAnimationKey]; |
| 902 |
| 903 // Update frame image. If the tab is being faded out, finish the frame |
| 904 // image's animation by animating out the overlap. |
| 905 BOOL shouldAnimateOverlap = |
| 906 tabAnimationStyle == CARD_TAB_ANIMATION_STYLE_FADE_OUT; |
| 907 if (shouldAnimateOverlap) { |
| 908 [CATransaction begin]; |
| 909 [CATransaction setCompletionBlock:^(void) { |
| 910 [self animateOutFrameImageOverlap]; |
| 911 }]; |
| 912 } |
| 913 CGRect beginBounds = {CGPointZero, beginFrame.size}; |
| 914 CGRect endBounds = {CGPointZero, endFrame.size}; |
| 915 frameAnimation = FrameAnimationMake( |
| 916 [_frameLeft layer], [self frameLeftFrameForBounds:beginBounds], |
| 917 [self frameLeftFrameForBounds:endBounds]); |
| 918 frameAnimation.duration = frameDuration; |
| 919 frameAnimation.timingFunction = frameTiming; |
| 920 [[_frameLeft layer] addAnimation:frameAnimation forKey:kCardViewAnimationKey]; |
| 921 frameAnimation = FrameAnimationMake( |
| 922 [_frameRight layer], [self frameRightFrameForBounds:beginBounds], |
| 923 [self frameRightFrameForBounds:endBounds]); |
| 924 frameAnimation.duration = frameDuration; |
| 925 frameAnimation.timingFunction = frameTiming; |
| 926 [[_frameRight layer] addAnimation:frameAnimation |
| 927 forKey:kCardViewAnimationKey]; |
| 928 frameAnimation = FrameAnimationMake([_frameTop layer], |
| 929 [self frameTopFrameForBounds:beginBounds], |
| 930 [self frameTopFrameForBounds:endBounds]); |
| 931 frameAnimation.duration = frameDuration; |
| 932 frameAnimation.timingFunction = frameTiming; |
| 933 [[_frameTop layer] addAnimation:frameAnimation forKey:kCardViewAnimationKey]; |
| 934 frameAnimation = FrameAnimationMake( |
| 935 [_frameBottom layer], [self frameBottomFrameForBounds:beginBounds], |
| 936 [self frameBottomFrameForBounds:endBounds]); |
| 937 frameAnimation.duration = frameDuration; |
| 938 frameAnimation.timingFunction = frameTiming; |
| 939 [[_frameBottom layer] addAnimation:frameAnimation |
| 940 forKey:kCardViewAnimationKey]; |
| 941 if (shouldAnimateOverlap) |
| 942 [CATransaction commit]; |
| 943 |
| 944 // Update frame shadow and its mask |
| 945 if (self.shouldShowShadow) { |
| 946 frameAnimation = FrameAnimationMake( |
| 947 [_frameShadowImageView layer], |
| 948 UIEdgeInsetsInsetRect(beginBounds, kCardShadowLayoutOutsets), |
| 949 UIEdgeInsetsInsetRect(endBounds, kCardShadowLayoutOutsets)); |
| 950 frameAnimation.duration = frameDuration; |
| 951 frameAnimation.timingFunction = frameTiming; |
| 952 [[_frameShadowImageView layer] addAnimation:frameAnimation |
| 953 forKey:kCardViewAnimationKey]; |
| 954 if (self.shouldMaskShadow) { |
| 955 frameAnimation = FrameAnimationMake( |
| 956 _shadowMask.get(), [self shadowMaskFrameForBounds:beginBounds], |
| 957 [self shadowMaskFrameForBounds:endBounds]); |
| 958 frameAnimation.duration = frameDuration; |
| 959 frameAnimation.timingFunction = frameTiming; |
| 960 [_shadowMask addAnimation:frameAnimation forKey:kCardViewAnimationKey]; |
| 961 } |
| 962 } |
| 963 |
| 964 // Update content snapshot |
| 965 CGRect beginContentFrame = |
| 966 UIEdgeInsetsInsetRect(beginBounds, kCardImageInsets); |
| 967 CGRect endContentFrame = UIEdgeInsetsInsetRect(endBounds, kCardImageInsets); |
| 968 frameAnimation = |
| 969 FrameAnimationMake([_contents layer], beginContentFrame, endContentFrame); |
| 970 frameAnimation.duration = frameDuration; |
| 971 frameAnimation.timingFunction = frameTiming; |
| 972 CABasicAnimation* contentsRectAnimation = |
| 973 [CABasicAnimation animationWithKeyPath:@"contentsRect"]; |
| 974 CGRect beginContentsRect = |
| 975 [self imageContentsRectForCardSize:beginBounds.size]; |
| 976 contentsRectAnimation.fromValue = [NSValue valueWithCGRect:beginContentsRect]; |
| 977 CGRect endContentsRect = [self imageContentsRectForCardSize:endBounds.size]; |
| 978 contentsRectAnimation.toValue = [NSValue valueWithCGRect:endContentsRect]; |
| 979 contentsRectAnimation.duration = frameDuration; |
| 980 contentsRectAnimation.timingFunction = frameTiming; |
| 981 CAAnimation* imageAnimation = |
| 982 AnimationGroupMake(@[ frameAnimation, contentsRectAnimation ]); |
| 983 [[_contents layer] addAnimation:imageAnimation forKey:kCardViewAnimationKey]; |
| 984 |
| 985 // Update tab view |
| 986 CGPoint tabOrigin = CGPointMake(kCardFrameInset, kCardTabTopInset); |
| 987 CGSize beginTabSize = |
| 988 CGSizeMake(beginFrame.size.width - 2.0 * kCardFrameInset, |
| 989 kCardImageInsets.top - kCardTabTopInset); |
| 990 CGSize endTabSize = CGSizeMake(endFrame.size.width - 2.0 * kCardFrameInset, |
| 991 kCardImageInsets.top - kCardTabTopInset); |
| 992 [_tab animateFromBeginFrame:{ tabOrigin, beginTabSize } |
| 993 toEndFrame:{ tabOrigin, endTabSize } |
| 994 tabAnimationStyle:tabAnimationStyle]; |
| 995 } |
| 996 |
| 997 - (void)removeFrameAnimation { |
| 998 [self.layer removeAnimationForKey:kCardViewAnimationKey]; |
| 999 } |
| 1000 |
| 1001 - (void)reverseAnimations { |
| 1002 [_tab reverseAnimations]; |
| 1003 ReverseAnimationsForKeyForLayers(kCardViewAnimationKey, @[ |
| 1004 self.layer, [_frameShadowImageView layer], _shadowMask, [_frameLeft layer], |
| 1005 [_frameRight layer], [_frameTop layer], [_frameBottom layer], |
| 1006 [_contents layer] |
| 1007 ]); |
| 1008 } |
| 1009 |
| 1010 - (void)cleanUpAnimations { |
| 1011 [_tab cleanUpAnimations]; |
| 1012 RemoveAnimationForKeyFromLayers(kCardViewAnimationKey, @[ |
| 1013 self.layer, [_frameShadowImageView layer], _shadowMask, [_frameLeft layer], |
| 1014 [_frameRight layer], [_frameTop layer], [_frameBottom layer], |
| 1015 [_contents layer] |
| 1016 ]); |
| 1017 } |
| 1018 |
| 1019 #pragma mark - Accessibility Methods |
| 1020 |
| 1021 - (void)postAccessibilityNotification { |
| 1022 UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, |
| 1023 [_tab titleLabel]); |
| 1024 } |
| 1025 |
| 1026 - (void)addAccessibilityTarget:(id)target action:(SEL)action { |
| 1027 DCHECK(!target || [target respondsToSelector:action]); |
| 1028 _accessibilityTarget = target; |
| 1029 _accessibilityAction = action; |
| 1030 } |
| 1031 |
| 1032 - (void)elementDidBecomeFocused:(id)sender { |
| 1033 [_accessibilityTarget performSelector:_accessibilityAction withObject:sender]; |
| 1034 } |
| 1035 |
| 1036 @end |
OLD | NEW |