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

Side by Side Diff: ios/chrome/browser/ui/stack_view/card_view.mm

Issue 2587023002: Upstream Chrome on iOS source code [8/11]. (Closed)
Patch Set: Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 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
OLDNEW
« no previous file with comments | « ios/chrome/browser/ui/stack_view/card_view.h ('k') | ios/chrome/browser/ui/stack_view/close_button.h » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698