blob: 0ca24d4c40d7edd9d0520efe319fe27cd32f2a07 [file] [log] [blame]
// 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/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