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

Unified 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 side-by-side diff with in-line comments
Download patch
« 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 »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: ios/chrome/browser/ui/stack_view/card_view.mm
diff --git a/ios/chrome/browser/ui/stack_view/card_view.mm b/ios/chrome/browser/ui/stack_view/card_view.mm
new file mode 100644
index 0000000000000000000000000000000000000000..9c015b3a482f372fa664ff59c43559ff688d4122
--- /dev/null
+++ b/ios/chrome/browser/ui/stack_view/card_view.mm
@@ -0,0 +1,1036 @@
+// Copyright 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// For performance reasons, the composition of the card frame is broken up into
+// four pieces. The overall structure of the CardView is:
+// - CardView
+// - Snapshot (UIImageView)
+// - FrameTop (UIImageView)
+// - FrameLeft (UIImageView)
+// - FrameRight (UIImageView)
+// - FrameBottom (UIImageView)
+// - CardTabView (UIView::DrawRect)
+//
+// While it would be simpler to put the frame in one transparent UIImageView,
+// that would make the entire snapshot area needlessly color-blended. Instead
+// the frame is broken up into four pieces, top, left, bottom, right.
+//
+// The frame's tab gets its own view above everything else (CardTabView) so that
+// it can be animated out. It's also transparent since the tab has a curve and
+// a shadow.
+
+#import "ios/chrome/browser/ui/stack_view/card_view.h"
+
+#import <QuartzCore/QuartzCore.h>
+#include <algorithm>
+
+#import "base/mac/foundation_util.h"
+#import "base/mac/objc_property_releaser.h"
+#import "base/mac/scoped_nsobject.h"
+#include "components/strings/grit/components_strings.h"
+#import "ios/chrome/browser/ui/animation_util.h"
+#import "ios/chrome/browser/ui/reversed_animation.h"
+#import "ios/chrome/browser/ui/rtl_geometry.h"
+#import "ios/chrome/browser/ui/stack_view/close_button.h"
+#import "ios/chrome/browser/ui/stack_view/title_label.h"
+#import "ios/chrome/browser/ui/tabs/tab_util.h"
+#import "ios/chrome/browser/ui/ui_util.h"
+#import "ios/chrome/common/material_timing.h"
+#import "ios/third_party/material_components_ios/src/components/Typography/src/MaterialTypography.h"
+#include "ui/base/l10n/l10n_util.h"
+#include "ui/base/resource/resource_bundle.h"
+#include "ui/gfx/favicon_size.h"
+#include "ui/gfx/image/image.h"
+#import "ui/gfx/ios/uikit_util.h"
+
+using ios::material::TimingFunction;
+
+const UIEdgeInsets kCardImageInsets = {48.0, 4.0, 4.0, 4.0};
+const CGFloat kCardFrameInset = 1.0;
+const CGFloat kCardShadowThickness = 16.0;
+const CGFloat kCardFrameCornerRadius = 4.0;
+const CGFloat kCardTabTopInset = 4;
+const CGFloat kCardFrameBackgroundBrightness = 242.0 / 255.0;
+const CGFloat kCardFrameBackgroundBrightnessIncognito = 80.0 / 255.0;
+const CGFloat kCardFrameImageSnapshotOverlap = 1.0;
+NSString* const kCardShadowImageName = @"card_frame_shadow";
+const UIEdgeInsets kCardShadowLayoutOutsets = {-14.0, -22.0, -14.0, -22.0};
+
+namespace {
+
+// Chrome corners and edges.
+const CGFloat kCardTabTitleMargin = 4;
+const CGFloat kCardTabFavIconLeadingMargin = 12;
+const CGFloat kCardTabFavIconCloseButtonPadding = 8.0;
+const UIEdgeInsets kCloseButtonContentInsets = {12.0, 12.0, 12.0, 12.0};
+const UIEdgeInsets kShadowStretchInsets = {51.0, 47.0, 51.0, 47.0};
+
+// Animation key for explicit animations added by
+// |-animateFromBeginFrame:toEndFrame:tabAnimationStyle:|.
+NSString* const kCardViewAnimationKey = @"CardViewAnimation";
+
+// Returns the appropriate variant of the image for |image_name| based on
+// |is_incognito|.
+UIImage* ImageWithName(NSString* image_name, BOOL is_incognito) {
+ NSString* name = is_incognito
+ ? [image_name stringByAppendingString:@"_incognito"]
+ : image_name;
+ return [UIImage imageNamed:name];
+}
+
+} // namespace
+
+#pragma mark -
+
+@interface CardTabView : UIView
+
+@property(nonatomic, assign) CardCloseButtonSide closeButtonSide;
+@property(nonatomic, retain) UIImageView* favIconView;
+@property(nonatomic, retain) UIImage* favicon;
+@property(nonatomic, retain) CloseButton* closeButton;
+@property(nonatomic, retain) TitleLabel* titleLabel;
+@property(nonatomic, assign) BOOL isIncognito;
+
+// Layout helper selectors that calculate the frames for subviews given the
+// bounds of the card tab. Note that the frames returned by these selectors
+// will be different depending on the value of the |displaySide| property.
+- (LayoutRect)faviconLayoutForBounds:(CGRect)bounds;
+- (CGRect)faviconFrameForBounds:(CGRect)bounds;
+- (LayoutRect)titleLabelLayoutForBounds:(CGRect)bounds;
+- (CGRect)titleLabelFrameForBounds:(CGRect)bounds;
+- (LayoutRect)closeButtonLayoutForBounds:(CGRect)bounds;
+- (CGRect)closeButtonFrameForBounds:(CGRect)bounds;
+
+// Adds frame animations for the favicon, title, and close button. The subviews
+// will be faded in if |tabAnimationStyle| = CardTabAnimationStyleFadeIn and
+// faded out if |tabAnimationStyle| = CardTabAnimationStyleFadeOut.
+- (void)animateFromBeginFrame:(CGRect)beginFrame
+ toEndFrame:(CGRect)endFrame
+ tabAnimationStyle:(CardTabAnimationStyle)tabAnimationStyle;
+
+// Called by CardView's selector of the same name and reverses animations added
+// by |-animateFromBeginFrame:toEndFrame:tabAnimationStyle:|.
+- (void)reverseAnimations;
+
+// Called by CardView's selector of the same name and removes animations added
+// by |-animateFromBeginFrame:toEndFrame:tabAnimationStyle:|.
+- (void)cleanUpAnimations;
+
+// Initialize a CardTabView with |frame| and |isIncognito| state.
+- (instancetype)initWithFrame:(CGRect)frame
+ isIncognito:(BOOL)isIncognito NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)initWithCoder:(NSCoder*)aDecoder NS_UNAVAILABLE;
+
+@end
+
+@implementation CardTabView {
+ base::mac::ObjCPropertyReleaser _propertyReleaser_CardTabView;
+}
+
+#pragma mark - Property Implementation
+
+@synthesize closeButtonSide = _closeButtonSide;
+@synthesize favIconView = _faviconView;
+@synthesize favicon = _favicon;
+@synthesize closeButton = _closeButton;
+@synthesize titleLabel = _titleLabel;
+@synthesize isIncognito = _isIncognito;
+
+- (instancetype)initWithFrame:(CGRect)frame {
+ return [self initWithFrame:frame isIncognito:NO];
+}
+
+- (instancetype)initWithFrame:(CGRect)frame isIncognito:(BOOL)isIncognito {
+ self = [super initWithFrame:frame];
+ if (!self)
+ return self;
+
+ _propertyReleaser_CardTabView.Init(self, [CardTabView class]);
+ _isIncognito = isIncognito;
+
+ UIImage* image = ImageWithName(@"default_favicon", _isIncognito);
+ _faviconView = [[UIImageView alloc] initWithImage:image];
+ [self addSubview:_faviconView];
+
+ UIImage* normal = ImageWithName(@"card_close_button", _isIncognito);
+ UIImage* pressed = ImageWithName(@"card_close_button_pressed", _isIncognito);
+
+ _closeButton = [[CloseButton alloc] initWithFrame:CGRectZero];
+ [_closeButton setAccessibilityLabel:l10n_util::GetNSString(IDS_CLOSE)];
+ [_closeButton setImage:normal forState:UIControlStateNormal];
+ [_closeButton setImage:pressed forState:UIControlStateHighlighted];
+ [_closeButton setContentEdgeInsets:kCloseButtonContentInsets];
+ [_closeButton sizeToFit];
+ [self addSubview:_closeButton];
+
+ _titleLabel = [[TitleLabel alloc] initWithFrame:CGRectZero];
+ [_titleLabel setFont:[MDCTypography body1Font]];
+ [self addSubview:_titleLabel];
+
+ [self updateTitleColors];
+
+ return self;
+}
+
+- (instancetype)initWithCoder:(NSCoder*)aDecoder {
+ NOTREACHED();
+ return nil;
+}
+
+- (void)setCloseButtonSide:(CardCloseButtonSide)closeButtonSide {
+ if (_closeButtonSide != closeButtonSide) {
+ _closeButtonSide = closeButtonSide;
+ [self setNeedsLayout];
+ }
+}
+
+- (void)layoutSubviews {
+ [super layoutSubviews];
+ self.favIconView.frame = [self faviconFrameForBounds:self.bounds];
+ self.titleLabel.frame = [self titleLabelFrameForBounds:self.bounds];
+ self.closeButton.frame = [self closeButtonFrameForBounds:self.bounds];
+}
+
+- (LayoutRect)faviconLayoutForBounds:(CGRect)bounds {
+ LayoutRect layout;
+ layout.boundingWidth = CGRectGetWidth(bounds);
+ layout.size = CGSizeMake(gfx::kFaviconSize, gfx::kFaviconSize);
+ layout.position.originY = CGRectGetMidY(bounds) - 0.5 * layout.size.height;
+ if (self.closeButtonSide == CardCloseButtonSide::LEADING) {
+ // Layout |kCardTabFavIconCloseButtonPadding| from the close button's
+ // trailing edge.
+ layout.position.leading =
+ LayoutRectGetTrailingEdge([self closeButtonLayoutForBounds:bounds]) +
+ kCardTabFavIconCloseButtonPadding;
+ } else {
+ // Layout |kCardTabFavIconLeadingMargin| from the leading edge of the
+ // bounds.
+ layout.position.leading = kCardTabFavIconLeadingMargin;
+ }
+ return layout;
+}
+
+- (CGRect)faviconFrameForBounds:(CGRect)bounds {
+ return AlignRectOriginAndSizeToPixels(
+ LayoutRectGetRect([self faviconLayoutForBounds:bounds]));
+}
+
+- (LayoutRect)titleLabelLayoutForBounds:(CGRect)bounds {
+ LayoutRect layout;
+ layout.boundingWidth = CGRectGetWidth(bounds);
+ layout.size = [self.titleLabel sizeThatFits:bounds.size];
+ layout.position.originY = CGRectGetMidY(bounds) - 0.5 * layout.size.height;
+ const CGFloat titlePadding = 2.0 * kCardTabTitleMargin;
+ CGFloat faviconTrailingEdge =
+ LayoutRectGetTrailingEdge([self faviconLayoutForBounds:bounds]);
+ CGFloat maxWidth = CGFLOAT_MAX;
+ if (self.closeButtonSide == CardCloseButtonSide::LEADING) {
+ // Layout between the favicon and the trailing edge of the bounds.
+ maxWidth = CGRectGetMaxX(bounds) - faviconTrailingEdge - titlePadding;
+ } else {
+ // Lay out between the favicon and the close button.
+ maxWidth = [self closeButtonLayoutForBounds:bounds].position.leading -
+ faviconTrailingEdge - titlePadding;
+ }
+ layout.size.width = std::min(layout.size.width, maxWidth);
+ layout.position.leading = faviconTrailingEdge + kCardTabTitleMargin;
+ return layout;
+}
+
+- (CGRect)titleLabelFrameForBounds:(CGRect)bounds {
+ return AlignRectOriginAndSizeToPixels(
+ LayoutRectGetRect([self titleLabelLayoutForBounds:bounds]));
+}
+
+- (LayoutRect)closeButtonLayoutForBounds:(CGRect)bounds {
+ LayoutRect layout;
+ layout.boundingWidth = CGRectGetWidth(bounds);
+ layout.size = [self.closeButton sizeThatFits:bounds.size];
+ layout.position.originY = CGRectGetMidY(bounds) - 0.5 * layout.size.height;
+ layout.position.leading = self.closeButtonSide == CardCloseButtonSide::LEADING
+ ? 0.0
+ : layout.boundingWidth - layout.size.width;
+ return layout;
+}
+
+- (CGRect)closeButtonFrameForBounds:(CGRect)bounds {
+ return AlignRectOriginAndSizeToPixels(
+ LayoutRectGetRect([self closeButtonLayoutForBounds:bounds]));
+}
+
+- (CGRect)closeButtonRect {
+ return [_closeButton frame];
+}
+
+- (void)setTitle:(NSString*)title {
+ [_titleLabel setText:title];
+ [_closeButton setAccessibilityValue:title];
+
+ [self setNeedsUpdateConstraints];
+}
+
+- (void)setFavicon:(UIImage*)favicon {
+ if (favicon != _favicon) {
+ [favicon retain];
+ [_favicon release];
+ _favicon = favicon;
+ [self updateFaviconImage];
+ }
+}
+
+- (void)updateTitleColors {
+ UIColor* titleColor = [UIColor blackColor];
+ if (_isIncognito)
+ titleColor = [UIColor whiteColor];
+
+ [_titleLabel setTextColor:titleColor];
+}
+
+- (void)updateFaviconImage {
+ UIImage* favicon = _favicon;
+ if (!favicon)
+ favicon = ImageWithName(@"default_favicon", _isIncognito);
+
+ [_faviconView setImage:favicon];
+
+ [self setNeedsUpdateConstraints];
+}
+
+- (void)animateFromBeginFrame:(CGRect)beginFrame
+ toEndFrame:(CGRect)endFrame
+ tabAnimationStyle:(CardTabAnimationStyle)tabAnimationStyle {
+ // Animation values.
+ CAAnimation* frameAnimation = nil;
+ CFTimeInterval frameDuration = ios::material::kDuration1;
+ CAMediaTimingFunction* frameTiming =
+ TimingFunction(ios::material::CurveEaseInOut);
+ CGRect beginBounds = {CGPointZero, beginFrame.size};
+ CGRect endBounds = {CGPointZero, endFrame.size};
+ BOOL shouldAnimateFade = (tabAnimationStyle != CARD_TAB_ANIMATION_STYLE_NONE);
+ BOOL shouldFadeIn = (tabAnimationStyle == CARD_TAB_ANIMATION_STYLE_FADE_IN);
+ CAAnimation* opacityAnimation = nil;
+ CAMediaTimingFunction* opacityTiming = nil;
+ if (shouldAnimateFade) {
+ opacityTiming = TimingFunction(shouldFadeIn ? ios::material::CurveEaseOut
+ : ios::material::CurveEaseIn);
+ }
+ CGFloat beginOpacity = shouldFadeIn ? 0.0 : 1.0;
+ CGFloat endOpacity = shouldFadeIn ? 1.0 : 0.0;
+ CAAnimation* animation = nil;
+
+ // Update layer geometry.
+ frameAnimation = FrameAnimationMake(self.layer, beginFrame, endFrame);
+ frameAnimation.duration = frameDuration;
+ frameAnimation.timingFunction = frameTiming;
+ [self.layer addAnimation:frameAnimation forKey:kCardViewAnimationKey];
+
+ // Update favicon.
+ frameAnimation = FrameAnimationMake(_faviconView.layer,
+ [self faviconFrameForBounds:beginBounds],
+ [self faviconFrameForBounds:endBounds]);
+ frameAnimation.duration = frameDuration;
+ frameAnimation.timingFunction = frameTiming;
+ if (shouldAnimateFade) {
+ opacityAnimation = OpacityAnimationMake(beginOpacity, endOpacity);
+ opacityAnimation.duration = ios::material::kDuration8;
+ opacityAnimation.timingFunction = opacityTiming;
+ opacityAnimation.beginTime = shouldFadeIn ? ios::material::kDuration8 : 0.0;
+ animation = AnimationGroupMake(@[ frameAnimation, opacityAnimation ]);
+ } else {
+ animation = frameAnimation;
+ }
+ [_faviconView.layer addAnimation:animation forKey:kCardViewAnimationKey];
+
+ // Update close button.
+ frameAnimation = FrameAnimationMake(
+ _closeButton.layer, [self closeButtonFrameForBounds:beginBounds],
+ [self closeButtonFrameForBounds:endBounds]);
+ frameAnimation.duration = frameDuration;
+ frameAnimation.timingFunction = frameTiming;
+ if (shouldAnimateFade) {
+ opacityAnimation = OpacityAnimationMake(beginOpacity, endOpacity);
+ opacityAnimation.timingFunction = opacityTiming;
+ opacityAnimation.duration =
+ shouldFadeIn ? ios::material::kDuration1 : ios::material::kDuration8;
+ opacityAnimation.beginTime = shouldFadeIn ? ios::material::kDuration1 : 0.0;
+ animation = AnimationGroupMake(@[ frameAnimation, opacityAnimation ]);
+ } else {
+ animation = frameAnimation;
+ }
+ [_closeButton.layer addAnimation:animation forKey:kCardViewAnimationKey];
+
+ // Update title.
+ [_titleLabel animateFromBeginFrame:[self titleLabelFrameForBounds:beginBounds]
+ toEndFrame:[self titleLabelFrameForBounds:endBounds]
+ duration:frameDuration
+ timingFunction:frameTiming];
+ if (shouldAnimateFade) {
+ opacityAnimation = OpacityAnimationMake(beginOpacity, endOpacity);
+ opacityAnimation.timingFunction = opacityTiming;
+ opacityAnimation.duration =
+ shouldFadeIn ? ios::material::kDuration6 : ios::material::kDuration8;
+ CFTimeInterval delay = shouldFadeIn ? ios::material::kDuration2 : 0.0;
+ opacityAnimation = DelayedAnimationMake(opacityAnimation, delay);
+ [_titleLabel.layer addAnimation:opacityAnimation
+ forKey:kCardViewAnimationKey];
+ }
+}
+
+- (void)reverseAnimations {
+ [_titleLabel reverseAnimations];
+ ReverseAnimationsForKeyForLayers(kCardViewAnimationKey, @[
+ self.layer, _faviconView.layer, _closeButton.layer, _titleLabel.layer
+ ]);
+}
+
+- (void)cleanUpAnimations {
+ [_titleLabel cleanUpAnimations];
+ RemoveAnimationForKeyFromLayers(kCardViewAnimationKey, @[
+ self.layer, _faviconView.layer, _closeButton.layer, _titleLabel.layer
+ ]);
+}
+
+// Implementation of this protocol forces VoiceOver to read the titleLabel and
+// close button in the correct order (top left to bottom right, by row). Because
+// the top border the close button's focus area is higher than the label's, in
+// portrait mode VoiceOver automatically orders the close button first, although
+// it looks like the default behavior should read the elements from left to
+// right.
+#pragma mark - UIAccessibilityContainer methods
+
+// CardTabView accessibility elements: titleLabel and closeButton.
+- (NSInteger)accessibilityElementCount {
+ return 2;
+}
+
+// Returns the leftmost accessibility element if |index| = 0 and the rightmost
+// accessibility element if |index| = 1. The display side determines the
+// location of the close button relative to the title label.
+- (id)accessibilityElementAtIndex:(NSInteger)index {
+ BOOL closeButtonLeading =
+ self.closeButtonSide == CardCloseButtonSide::LEADING;
+ id element = nil;
+ if (index == 0)
+ element = closeButtonLeading ? _closeButton : _titleLabel;
+ else if (index == 1)
+ element = closeButtonLeading ? _titleLabel : _closeButton;
+ return element;
+}
+
+// Returns 0 if element is on the left (titleLabel in portrait, closeButton in
+// landscape), and 1 if the element is on the right. Otherwise returns
+// NSNotFound.
+- (NSInteger)indexOfAccessibilityElement:(id)element {
+ BOOL closeButtonLeading =
+ self.closeButtonSide == CardCloseButtonSide::LEADING;
+ NSInteger index = NSNotFound;
+ if (element == _closeButton)
+ index = closeButtonLeading ? 0 : 1;
+ else if (element == _titleLabel)
+ index = closeButtonLeading ? 1 : 0;
+ return index;
+}
+
+@end
+
+#pragma mark -
+
+@interface CardView () {
+ base::scoped_nsobject<UIImageView> _contents;
+ base::scoped_nsobject<CardTabView> _tab;
+ id _cardCloseTarget; // weak
+ SEL _cardCloseAction;
+ id _accessibilityTarget; // weak
+ SEL _accessibilityAction;
+
+ BOOL _isIncognito; // YES if the card should use the incognito styling.
+
+ // Pieces of the card frame, split into four UIViews.
+ base::scoped_nsobject<UIImageView> _frameLeft;
+ base::scoped_nsobject<UIImageView> _frameRight;
+ base::scoped_nsobject<UIImageView> _frameTop;
+ base::scoped_nsobject<UIImageView> _frameBottom;
+ base::scoped_nsobject<UIImageView> _frameShadowImageView;
+ base::scoped_nsobject<CALayer> _shadowMask;
+}
+
+// The LayoutRect for the CardTabView.
+- (LayoutRect)tabLayout;
+
+// Sends |_cardCloseAction| to |_cardCloseTarget| with |self| as the sender.
+- (void)closeButtonWasTapped:(id)sender;
+
+// Resizes/zooms the snapshot to avoid stretching given the card's current
+// bounds.
+- (void)updateImageBoundsAndZoom;
+
+// If the image is reset during an animation, this is called to update the
+// snapshot's contentsRect animation for the new image.
+- (void)updateSnapshotAnimations;
+
+// The frame to use for the shadow mask for a CardVeiw with |bounds|.
+- (CGRect)shadowMaskFrameForBounds:(CGRect)bounds;
+
+// Updates the mask used for the shadow.
+- (void)updateShadowMask;
+
+// Returns the contentsRect that will correctly zoom the image's layer for the
+// given card size.
+- (CGRect)imageContentsRectForCardSize:(CGSize)cardSize;
+
+// Animates the frame image such that it no longer overlaps the image snapshot.
+- (void)animateOutFrameImageOverlap;
+// Returns the frames used for the image views for a given bounds.
+- (CGRect)frameLeftFrameForBounds:(CGRect)bounds;
+- (CGRect)frameRightFrameForBounds:(CGRect)bounds;
+- (CGRect)frameTopFrameForBounds:(CGRect)bounds;
+- (CGRect)frameBottomFrameForBounds:(CGRect)bounds;
+
+@end
+
+@implementation CardView
+
+@synthesize isActiveTab = _isActiveTab;
+@synthesize shouldShowShadow = _shouldShowShadow;
+@synthesize shouldMaskShadow = _shouldMaskShadow;
+
+- (instancetype)initWithFrame:(CGRect)frame {
+ return [self initWithFrame:frame isIncognito:NO];
+}
+
+- (instancetype)initWithFrame:(CGRect)frame isIncognito:(BOOL)isIncognito {
+ self = [super initWithFrame:frame];
+ if (!self)
+ return self;
+
+ _isIncognito = isIncognito;
+ CGRect bounds = self.bounds;
+
+ self.opaque = NO;
+ self.contentMode = UIViewContentModeRedraw;
+
+ CGRect shadowFrame = UIEdgeInsetsInsetRect(bounds, kCardShadowLayoutOutsets);
+ _frameShadowImageView.reset([[UIImageView alloc] initWithFrame:shadowFrame]);
+ [_frameShadowImageView
+ setAutoresizingMask:(UIViewAutoresizingFlexibleWidth |
+ UIViewAutoresizingFlexibleHeight)];
+ [self addSubview:_frameShadowImageView];
+
+ // Calling properties for side-effects.
+ self.shouldShowShadow = YES;
+ self.shouldMaskShadow = YES;
+
+ UIImage* image = [UIImage imageNamed:kCardShadowImageName];
+ image = [image resizableImageWithCapInsets:kShadowStretchInsets];
+ [_frameShadowImageView setImage:image];
+
+ CGRect snapshotFrame = UIEdgeInsetsInsetRect(bounds, kCardImageInsets);
+ _contents.reset([[UIImageView alloc] initWithFrame:snapshotFrame]);
+ [_contents setClipsToBounds:YES];
+ [_contents setContentMode:UIViewContentModeScaleAspectFill];
+ [_contents setFrame:snapshotFrame];
+ [_contents setAutoresizingMask:UIViewAutoresizingFlexibleWidth |
+ UIViewAutoresizingFlexibleHeight];
+ [self addSubview:_contents];
+
+ image = [UIImage imageNamed:isIncognito ? @"border_frame_incognito_left"
+ : @"border_frame_left"];
+ UIEdgeInsets imageStretchInsets = UIEdgeInsetsMake(
+ 0.5 * image.size.height, 0.0, 0.5 * image.size.height, 0.0);
+ image = [image resizableImageWithCapInsets:imageStretchInsets];
+ _frameLeft.reset([[UIImageView alloc] initWithImage:image]);
+ [self addSubview:_frameLeft];
+
+ image = [UIImage imageNamed:isIncognito ? @"border_frame_incognito_right"
+ : @"border_frame_right"];
+ imageStretchInsets = UIEdgeInsetsMake(0.5 * image.size.height, 0.0,
+ 0.5 * image.size.height, 0.0);
+ image = [image resizableImageWithCapInsets:imageStretchInsets];
+ _frameRight.reset([[UIImageView alloc] initWithImage:image]);
+ [self addSubview:_frameRight];
+
+ image = [UIImage imageNamed:isIncognito ? @"border_frame_incognito_top"
+ : @"border_frame_top"];
+ imageStretchInsets = UIEdgeInsetsMake(0.0, 0.5 * image.size.width, 0.0,
+ 0.5 * image.size.width);
+ image = [image resizableImageWithCapInsets:imageStretchInsets];
+ _frameTop.reset([[UIImageView alloc] initWithImage:image]);
+ [self addSubview:_frameTop];
+
+ image = [UIImage imageNamed:isIncognito ? @"border_frame_incognito_bottom"
+ : @"border_frame_bottom"];
+ imageStretchInsets = UIEdgeInsetsMake(0.0, 0.5 * image.size.width, 0.0,
+ 0.5 * image.size.width);
+ image = [image resizableImageWithCapInsets:imageStretchInsets];
+ _frameBottom.reset([[UIImageView alloc] initWithImage:image]);
+ [self addSubview:_frameBottom];
+
+ _tab.reset([[CardTabView alloc]
+ initWithFrame:LayoutRectGetRect([self tabLayout])
+ isIncognito:_isIncognito]);
+ [_tab setCloseButtonSide:IsPortrait() ? CardCloseButtonSide::TRAILING
+ : CardCloseButtonSide::LEADING];
+ [[_tab closeButton] addTarget:self
+ action:@selector(closeButtonWasTapped:)
+ forControlEvents:UIControlEventTouchUpInside];
+ [[_tab closeButton]
+ addAccessibilityElementFocusedTarget:self
+ action:@selector(elementDidBecomeFocused:)];
+ [_tab closeButton].accessibilityIdentifier = [self closeButtonId];
+
+ [[_tab titleLabel]
+ addAccessibilityElementFocusedTarget:self
+ action:@selector(elementDidBecomeFocused:)];
+
+ [self addSubview:_tab];
+
+ self.accessibilityIdentifier = @"Tab";
+ self.isAccessibilityElement = NO;
+ self.accessibilityElementsHidden = NO;
+
+ return self;
+}
+
+- (instancetype)initWithCoder:(NSCoder*)aDecoder {
+ NOTREACHED();
+ return nil;
+}
+
+- (void)setImage:(UIImage*)image {
+ [_contents setImage:image];
+ [self updateImageBoundsAndZoom];
+ [self updateSnapshotAnimations];
+}
+
+- (UIImage*)image {
+ return [_contents image];
+}
+
+- (void)setShouldShowShadow:(BOOL)shouldShowShadow {
+ if (_shouldShowShadow != shouldShowShadow) {
+ _shouldShowShadow = shouldShowShadow;
+ [_frameShadowImageView setHidden:!_shouldShowShadow];
+ if (_shouldShowShadow)
+ [self updateShadowMask];
+ }
+}
+
+- (void)setShouldMaskShadow:(BOOL)shouldMaskShadow {
+ if (_shouldMaskShadow != shouldMaskShadow) {
+ _shouldMaskShadow = shouldMaskShadow;
+ if (self.shouldShowShadow)
+ [self updateShadowMask];
+ }
+}
+
+- (void)setCloseButtonSide:(CardCloseButtonSide)closeButtonSide {
+ if ([_tab closeButtonSide] == closeButtonSide)
+ return;
+ [_tab setCloseButtonSide:closeButtonSide];
+}
+
+- (CardCloseButtonSide)closeButtonSide {
+ return [_tab closeButtonSide];
+}
+
+- (void)setTitle:(NSString*)title {
+ [_tab setTitle:title];
+}
+
+- (TitleLabel*)titleLabel {
+ return [_tab titleLabel];
+}
+
+- (void)setFavicon:(UIImage*)favicon {
+ [_tab setFavicon:favicon];
+}
+
+- (void)closeButtonWasTapped:(id)sender {
+ [_cardCloseTarget performSelector:_cardCloseAction withObject:self];
+ // Disable the tab's close button to prevent touch handling from the button
+ // while it's animating closed.
+ [_tab closeButton].enabled = NO;
+}
+
+- (void)addCardCloseTarget:(id)target action:(SEL)action {
+ DCHECK(!target || [target respondsToSelector:action]);
+ _cardCloseTarget = target;
+ _cardCloseAction = action;
+}
+
+- (CGRect)closeButtonFrame {
+ CGRect frame = [_tab closeButtonRect];
+ return [self convertRect:frame fromView:_tab];
+}
+
+- (NSString*)closeButtonId {
+ return [NSString stringWithFormat:@"%p", [_tab closeButton]];
+}
+
+- (CGRect)imageContentsRectForCardSize:(CGSize)cardSize {
+ CGRect cardBounds = {CGPointZero, cardSize};
+ CGSize viewSize = UIEdgeInsetsInsetRect(cardBounds, kCardImageInsets).size;
+ CGSize imageSize = [_contents image].size;
+ CGFloat zoomRatio = std::max(viewSize.height / imageSize.height,
+ viewSize.width / imageSize.width);
+ return CGRectMake(0.0, 0.0, viewSize.width / (zoomRatio * imageSize.width),
+ viewSize.height / (zoomRatio * imageSize.height));
+}
+
+- (void)updateImageBoundsAndZoom {
+ UIImageView* imageView = _contents.get();
+ DCHECK(!CGRectEqualToRect(self.bounds, CGRectZero));
+
+ imageView.frame = UIEdgeInsetsInsetRect(self.bounds, kCardImageInsets);
+ if (imageView.image) {
+ // Zoom the image to fill the available space.
+ imageView.layer.contentsRect =
+ [self imageContentsRectForCardSize:self.bounds.size];
+ }
+}
+
+- (void)updateSnapshotAnimations {
+ CAAnimation* snapshotAnimation =
+ [[_contents layer] animationForKey:kCardViewAnimationKey];
+ if (!snapshotAnimation)
+ return;
+
+ // Create copy of animation (animations become immutable after they're added
+ // to the layer).
+ base::scoped_nsobject<CAAnimationGroup> updatedAnimation(
+ static_cast<CAAnimationGroup*>([snapshotAnimation copy]));
+ // Extract begin and end sizes of the card.
+ CAAnimation* cardAnimation =
+ [self.layer animationForKey:kCardViewAnimationKey];
+ CABasicAnimation* cardBoundsAnimation =
+ FindAnimationForKeyPath(@"bounds", cardAnimation);
+ CGSize beginCardSize = [cardBoundsAnimation.fromValue CGRectValue].size;
+ CGSize endCardSize = [cardBoundsAnimation.toValue CGRectValue].size;
+ // Update the contentsRect animation.
+ CABasicAnimation* contentsRectAnimation =
+ FindAnimationForKeyPath(@"contentsRect", updatedAnimation);
+ contentsRectAnimation.fromValue = [NSValue
+ valueWithCGRect:[self imageContentsRectForCardSize:beginCardSize]];
+ contentsRectAnimation.toValue =
+ [NSValue valueWithCGRect:[self imageContentsRectForCardSize:endCardSize]];
+ // Replace with updated animation.
+ [[_contents layer] removeAnimationForKey:kCardViewAnimationKey];
+ [[_contents layer] addAnimation:updatedAnimation
+ forKey:kCardViewAnimationKey];
+}
+
+- (CGRect)shadowMaskFrameForBounds:(CGRect)bounds {
+ CGRect shadowFrame = UIEdgeInsetsInsetRect(bounds, kCardShadowLayoutOutsets);
+ LayoutRect maskLayout = LayoutRectZero;
+ maskLayout.size = shadowFrame.size;
+ maskLayout.boundingWidth = maskLayout.size.width;
+ if (IsPortrait()) {
+ maskLayout.position.leading =
+ -UIEdgeInsetsGetLeading(kCardShadowLayoutOutsets);
+ maskLayout.size.width = CGRectGetWidth(bounds);
+ maskLayout.size.height =
+ kCardFrameCornerRadius + kCardFrameInset - kCardShadowLayoutOutsets.top;
+ } else {
+ maskLayout.position.originY = -kCardShadowLayoutOutsets.top;
+ maskLayout.size.width = kCardFrameCornerRadius + kCardFrameInset -
+ UIEdgeInsetsGetLeading(kCardShadowLayoutOutsets);
+ maskLayout.size.height = CGRectGetHeight(bounds);
+ }
+ return LayoutRectGetRect(maskLayout);
+}
+
+- (void)updateShadowMask {
+ if (!self.shouldShowShadow)
+ return;
+
+ if (self.shouldMaskShadow) {
+ if (!_shadowMask) {
+ _shadowMask.reset([[CALayer alloc] init]);
+ [_shadowMask setBackgroundColor:[UIColor blackColor].CGColor];
+ }
+ [_frameShadowImageView layer].mask = _shadowMask;
+ [_shadowMask setFrame:[self shadowMaskFrameForBounds:self.bounds]];
+ } else {
+ [_frameShadowImageView layer].mask = nil;
+ }
+}
+
+- (LayoutRect)tabLayout {
+ LayoutRect tabLayout;
+ tabLayout.position.leading = kCardFrameInset;
+ tabLayout.position.originY = kCardTabTopInset;
+ tabLayout.boundingWidth = CGRectGetWidth(self.bounds);
+ tabLayout.size.width = tabLayout.boundingWidth - 2.0 * kCardFrameInset;
+ tabLayout.size.height = kCardImageInsets.top - kCardTabTopInset;
+ return tabLayout;
+}
+
+- (void)layoutSubviews {
+ [_tab setFrame:LayoutRectGetRect([self tabLayout])];
+
+ [_tab setFrame:LayoutRectGetRect([self tabLayout])];
+ [_frameLeft setFrame:[self frameLeftFrameForBounds:self.bounds]];
+ [_frameRight setFrame:[self frameRightFrameForBounds:self.bounds]];
+ [_frameTop setFrame:[self frameTopFrameForBounds:self.bounds]];
+ [_frameBottom setFrame:[self frameBottomFrameForBounds:self.bounds]];
+
+ [self updateImageBoundsAndZoom];
+ [self updateShadowMask];
+}
+
+- (CGRect)frameLeftFrameForBounds:(CGRect)bounds {
+ return AlignRectToPixel(CGRectMake(
+ bounds.origin.x, bounds.origin.y + kCardImageInsets.top,
+ [_frameLeft image].size.width,
+ bounds.size.height - kCardImageInsets.top - kCardImageInsets.bottom));
+}
+
+- (CGRect)frameRightFrameForBounds:(CGRect)bounds {
+ CGSize size = ui::AlignSizeToUpperPixel(CGSizeMake(
+ [_frameRight image].size.width,
+ bounds.size.height - kCardImageInsets.top - kCardImageInsets.bottom));
+ CGFloat rightEdge = CGRectGetMaxX([self frameTopFrameForBounds:bounds]);
+ CGPoint origin = CGPointMake(rightEdge - size.width,
+ bounds.origin.y + kCardImageInsets.top);
+ return {origin, size};
+}
+
+- (CGRect)frameTopFrameForBounds:(CGRect)bounds {
+ return AlignRectToPixel(CGRectMake(bounds.origin.x, bounds.origin.y,
+ bounds.size.width,
+ [_frameTop image].size.height));
+}
+
+- (CGRect)frameBottomFrameForBounds:(CGRect)bounds {
+ CGFloat imageHeight = [_frameBottom image].size.height;
+ return AlignRectToPixel(CGRectMake(bounds.origin.x,
+ CGRectGetMaxY(bounds) - imageHeight,
+ bounds.size.width, imageHeight));
+}
+
+- (void)setTabOpacity:(CGFloat)opacity {
+ [_tab setAlpha:opacity];
+}
+
+- (NSString*)accessibilityValue {
+ return self.isActiveTab ? @"active" : @"inactive";
+}
+
+// Accounts for the fact that the tab's close button can extend beyond the
+// bounds of the card.
+- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event {
+ if ([super pointInside:point withEvent:event])
+ return YES;
+ CGPoint convertedPoint = [self convertPoint:point toView:_tab];
+ if ([_tab pointInside:convertedPoint withEvent:event])
+ return YES;
+ return NO;
+}
+
+- (void)installDummyToolbarBackgroundView:(UIView*)dummyToolbarBackgroundView {
+ [_tab insertSubview:dummyToolbarBackgroundView atIndex:0];
+}
+
+- (CGRect)dummyToolbarFrameForRect:(CGRect)rect inView:(UIView*)view {
+ return [_tab convertRect:rect fromView:view];
+}
+
+- (void)animateOutFrameImageOverlap {
+ // Calculate end frame for image. Applying the inverse of |kCardImageInsets|
+ // on the content snapshot's frame results in an overlap of
+ // |kCardFrameImageSnapshotOverlap|, so this needs to be included in the
+ // outsets.
+ CGRect contentFrame = [[_contents layer].presentationLayer frame];
+ UIEdgeInsets endFrameOutsets = UIEdgeInsetsMake(
+ -(kCardFrameImageSnapshotOverlap + kCardImageInsets.top),
+ -(kCardFrameImageSnapshotOverlap + kCardImageInsets.left),
+ -(kCardFrameImageSnapshotOverlap + kCardImageInsets.bottom),
+ -(kCardFrameImageSnapshotOverlap + kCardImageInsets.right));
+ CGRect endBounds = UIEdgeInsetsInsetRect(contentFrame, endFrameOutsets);
+
+ // Remove old frame animation and add new overlap animation.
+ CALayer* frameLayer = [_frameLeft layer];
+ CGRect beginFrame = [frameLayer.presentationLayer frame];
+ CGRect endFrame = [self frameLeftFrameForBounds:endBounds];
+ [frameLayer removeAnimationForKey:kCardViewAnimationKey];
+ CAAnimation* frameAnimation =
+ FrameAnimationMake(frameLayer, beginFrame, endFrame);
+ frameAnimation.duration = ios::material::kDuration2;
+ [frameLayer addAnimation:frameAnimation forKey:kCardViewAnimationKey];
+
+ frameLayer = [_frameRight layer];
+ beginFrame = [frameLayer.presentationLayer frame];
+ endFrame = [self frameRightFrameForBounds:endBounds];
+ [frameLayer removeAnimationForKey:kCardViewAnimationKey];
+ frameAnimation = FrameAnimationMake(frameLayer, beginFrame, endFrame);
+ frameAnimation.duration = ios::material::kDuration2;
+ [frameLayer addAnimation:frameAnimation forKey:kCardViewAnimationKey];
+
+ frameLayer = [_frameTop layer];
+ beginFrame = [frameLayer.presentationLayer frame];
+ endFrame = [self frameTopFrameForBounds:endBounds];
+ [frameLayer removeAnimationForKey:kCardViewAnimationKey];
+ frameAnimation = FrameAnimationMake(frameLayer, beginFrame, endFrame);
+ frameAnimation.duration = ios::material::kDuration2;
+ [frameLayer addAnimation:frameAnimation forKey:kCardViewAnimationKey];
+
+ frameLayer = [_frameBottom layer];
+ beginFrame = [frameLayer.presentationLayer frame];
+ endFrame = [self frameBottomFrameForBounds:endBounds];
+ [frameLayer removeAnimationForKey:kCardViewAnimationKey];
+ frameAnimation = FrameAnimationMake(frameLayer, beginFrame, endFrame);
+ frameAnimation.duration = ios::material::kDuration2;
+ [frameLayer addAnimation:frameAnimation forKey:kCardViewAnimationKey];
+}
+
+- (void)animateFromBeginFrame:(CGRect)beginFrame
+ toEndFrame:(CGRect)endFrame
+ tabAnimationStyle:(CardTabAnimationStyle)tabAnimationStyle {
+ // Animation values
+ CAAnimation* frameAnimation = nil;
+ CFTimeInterval frameDuration = ios::material::kDuration1;
+ CAMediaTimingFunction* frameTiming =
+ TimingFunction(ios::material::CurveEaseInOut);
+
+ // Update layer geometry
+ frameAnimation = FrameAnimationMake(self.layer, beginFrame, endFrame);
+ frameAnimation.duration = frameDuration;
+ frameAnimation.timingFunction = frameTiming;
+ [self.layer addAnimation:frameAnimation forKey:kCardViewAnimationKey];
+
+ // Update frame image. If the tab is being faded out, finish the frame
+ // image's animation by animating out the overlap.
+ BOOL shouldAnimateOverlap =
+ tabAnimationStyle == CARD_TAB_ANIMATION_STYLE_FADE_OUT;
+ if (shouldAnimateOverlap) {
+ [CATransaction begin];
+ [CATransaction setCompletionBlock:^(void) {
+ [self animateOutFrameImageOverlap];
+ }];
+ }
+ CGRect beginBounds = {CGPointZero, beginFrame.size};
+ CGRect endBounds = {CGPointZero, endFrame.size};
+ frameAnimation = FrameAnimationMake(
+ [_frameLeft layer], [self frameLeftFrameForBounds:beginBounds],
+ [self frameLeftFrameForBounds:endBounds]);
+ frameAnimation.duration = frameDuration;
+ frameAnimation.timingFunction = frameTiming;
+ [[_frameLeft layer] addAnimation:frameAnimation forKey:kCardViewAnimationKey];
+ frameAnimation = FrameAnimationMake(
+ [_frameRight layer], [self frameRightFrameForBounds:beginBounds],
+ [self frameRightFrameForBounds:endBounds]);
+ frameAnimation.duration = frameDuration;
+ frameAnimation.timingFunction = frameTiming;
+ [[_frameRight layer] addAnimation:frameAnimation
+ forKey:kCardViewAnimationKey];
+ frameAnimation = FrameAnimationMake([_frameTop layer],
+ [self frameTopFrameForBounds:beginBounds],
+ [self frameTopFrameForBounds:endBounds]);
+ frameAnimation.duration = frameDuration;
+ frameAnimation.timingFunction = frameTiming;
+ [[_frameTop layer] addAnimation:frameAnimation forKey:kCardViewAnimationKey];
+ frameAnimation = FrameAnimationMake(
+ [_frameBottom layer], [self frameBottomFrameForBounds:beginBounds],
+ [self frameBottomFrameForBounds:endBounds]);
+ frameAnimation.duration = frameDuration;
+ frameAnimation.timingFunction = frameTiming;
+ [[_frameBottom layer] addAnimation:frameAnimation
+ forKey:kCardViewAnimationKey];
+ if (shouldAnimateOverlap)
+ [CATransaction commit];
+
+ // Update frame shadow and its mask
+ if (self.shouldShowShadow) {
+ frameAnimation = FrameAnimationMake(
+ [_frameShadowImageView layer],
+ UIEdgeInsetsInsetRect(beginBounds, kCardShadowLayoutOutsets),
+ UIEdgeInsetsInsetRect(endBounds, kCardShadowLayoutOutsets));
+ frameAnimation.duration = frameDuration;
+ frameAnimation.timingFunction = frameTiming;
+ [[_frameShadowImageView layer] addAnimation:frameAnimation
+ forKey:kCardViewAnimationKey];
+ if (self.shouldMaskShadow) {
+ frameAnimation = FrameAnimationMake(
+ _shadowMask.get(), [self shadowMaskFrameForBounds:beginBounds],
+ [self shadowMaskFrameForBounds:endBounds]);
+ frameAnimation.duration = frameDuration;
+ frameAnimation.timingFunction = frameTiming;
+ [_shadowMask addAnimation:frameAnimation forKey:kCardViewAnimationKey];
+ }
+ }
+
+ // Update content snapshot
+ CGRect beginContentFrame =
+ UIEdgeInsetsInsetRect(beginBounds, kCardImageInsets);
+ CGRect endContentFrame = UIEdgeInsetsInsetRect(endBounds, kCardImageInsets);
+ frameAnimation =
+ FrameAnimationMake([_contents layer], beginContentFrame, endContentFrame);
+ frameAnimation.duration = frameDuration;
+ frameAnimation.timingFunction = frameTiming;
+ CABasicAnimation* contentsRectAnimation =
+ [CABasicAnimation animationWithKeyPath:@"contentsRect"];
+ CGRect beginContentsRect =
+ [self imageContentsRectForCardSize:beginBounds.size];
+ contentsRectAnimation.fromValue = [NSValue valueWithCGRect:beginContentsRect];
+ CGRect endContentsRect = [self imageContentsRectForCardSize:endBounds.size];
+ contentsRectAnimation.toValue = [NSValue valueWithCGRect:endContentsRect];
+ contentsRectAnimation.duration = frameDuration;
+ contentsRectAnimation.timingFunction = frameTiming;
+ CAAnimation* imageAnimation =
+ AnimationGroupMake(@[ frameAnimation, contentsRectAnimation ]);
+ [[_contents layer] addAnimation:imageAnimation forKey:kCardViewAnimationKey];
+
+ // Update tab view
+ CGPoint tabOrigin = CGPointMake(kCardFrameInset, kCardTabTopInset);
+ CGSize beginTabSize =
+ CGSizeMake(beginFrame.size.width - 2.0 * kCardFrameInset,
+ kCardImageInsets.top - kCardTabTopInset);
+ CGSize endTabSize = CGSizeMake(endFrame.size.width - 2.0 * kCardFrameInset,
+ kCardImageInsets.top - kCardTabTopInset);
+ [_tab animateFromBeginFrame:{ tabOrigin, beginTabSize }
+ toEndFrame:{ tabOrigin, endTabSize }
+ tabAnimationStyle:tabAnimationStyle];
+}
+
+- (void)removeFrameAnimation {
+ [self.layer removeAnimationForKey:kCardViewAnimationKey];
+}
+
+- (void)reverseAnimations {
+ [_tab reverseAnimations];
+ ReverseAnimationsForKeyForLayers(kCardViewAnimationKey, @[
+ self.layer, [_frameShadowImageView layer], _shadowMask, [_frameLeft layer],
+ [_frameRight layer], [_frameTop layer], [_frameBottom layer],
+ [_contents layer]
+ ]);
+}
+
+- (void)cleanUpAnimations {
+ [_tab cleanUpAnimations];
+ RemoveAnimationForKeyFromLayers(kCardViewAnimationKey, @[
+ self.layer, [_frameShadowImageView layer], _shadowMask, [_frameLeft layer],
+ [_frameRight layer], [_frameTop layer], [_frameBottom layer],
+ [_contents layer]
+ ]);
+}
+
+#pragma mark - Accessibility Methods
+
+- (void)postAccessibilityNotification {
+ UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification,
+ [_tab titleLabel]);
+}
+
+- (void)addAccessibilityTarget:(id)target action:(SEL)action {
+ DCHECK(!target || [target respondsToSelector:action]);
+ _accessibilityTarget = target;
+ _accessibilityAction = action;
+}
+
+- (void)elementDidBecomeFocused:(id)sender {
+ [_accessibilityTarget performSelector:_accessibilityAction withObject:sender];
+}
+
+@end
« 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