| 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
|
|
|