blob: fdf19816273dd83d4e07438916ef328f93135f80 [file] [log] [blame]
// Copyright 2016-present the Material Components for iOS authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import "MDCCollectionViewCell.h"
#import <MDFInternationalization/MDFInternationalization.h>
#import "MaterialCollectionLayoutAttributes.h"
#import "MaterialIcons+ic_check.h"
#import "MaterialIcons+ic_check_circle.h"
#import "MaterialIcons+ic_chevron_right.h"
#import "MaterialIcons+ic_info.h"
#import "MaterialIcons+ic_radio_button_unchecked.h"
#import "MaterialIcons+ic_reorder.h"
#import "MaterialPalettes.h"
static CGFloat kEditingControlAppearanceOffset = 16;
// Default accessory insets.
static const UIEdgeInsets kAccessoryInsetDefault = {0, 16, 0, 16};
// Default editing icon colors.
// Color is 0x626262
static inline UIColor *MDCCollectionViewCellGreyColor(void) {
return MDCPalette.greyPalette.tint700;
}
// Color is 0xF44336
static inline UIColor *MDCCollectionViewCellRedColor(void) {
return MDCPalette.redPalette.tint500;
}
// File name of the bundle (without the '.bundle' extension) containing resources.
static NSString *const kResourceBundleName = @"MaterialCollectionCells";
// String table name containing localized strings.
static NSString *const kStringTableName = @"MaterialCollectionCells";
NSString *const kSelectedCellAccessibilityHintKey =
@"MaterialCollectionCellsAccessibilitySelectedHint";
NSString *const kDeselectedCellAccessibilityHintKey =
@"MaterialCollectionCellsAccessibilityDeselectedHint";
// To be used as accessory view when an accessory type is set. It has no particular functionalities
// other than being a private class defined here, to check if an accessory view was set via an
// accessory type, or if the user of MDCCollectionViewCell set a custom accessory view.
@interface MDCAccessoryTypeImageView : UIImageView
@end
@implementation MDCAccessoryTypeImageView
@end
@implementation MDCCollectionViewCell {
MDCCollectionViewLayoutAttributes *_attr;
BOOL _usesCellSeparatorHiddenOverride;
BOOL _usesCellSeparatorInsetOverride;
BOOL _shouldAnimateEditingViews;
UIView *_separatorView;
UIImageView *_backgroundImageView;
UIImageView *_editingReorderImageView;
UIImageView *_editingSelectorImageView;
}
@synthesize inkView = _inkView;
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self commonMDCCollectionViewCellInit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self commonMDCCollectionViewCellInit];
}
return self;
}
- (void)commonMDCCollectionViewCellInit {
// Separator defaults.
_separatorView = [[UIImageView alloc] initWithFrame:CGRectZero];
[self addSubview:_separatorView];
// Accessory defaults.
_accessoryType = MDCCollectionViewCellAccessoryNone;
_accessoryInset = kAccessoryInsetDefault;
_editingSelectorColor = MDCCollectionViewCellRedColor();
}
#pragma mark - Layout
- (void)prepareForReuse {
[super prepareForReuse];
// Accessory defaults.
_accessoryType = MDCCollectionViewCellAccessoryNone;
_accessoryInset = kAccessoryInsetDefault;
[_accessoryView removeFromSuperview];
_accessoryView = nil;
// Reset properties.
_shouldAnimateEditingViews = NO;
_usesCellSeparatorHiddenOverride = NO;
_usesCellSeparatorInsetOverride = NO;
_separatorView.hidden = YES;
[self drawSeparatorIfNeeded];
[self updateInterfaceForEditing];
// Reset cells hidden during swipe deletion.
self.hidden = NO;
[self.inkView cancelAllAnimationsAnimated:NO];
}
- (void)layoutSubviews {
[super layoutSubviews];
// Layout the accessory view and the content view.
[self updateInterfaceForEditing];
[self layoutForegroundSubviews];
void (^editingViewLayout)(void) = ^() {
CGFloat txReorderTransform;
CGFloat txSelectorTransform;
switch (self.mdf_effectiveUserInterfaceLayoutDirection) {
case UIUserInterfaceLayoutDirectionLeftToRight:
txReorderTransform = kEditingControlAppearanceOffset;
txSelectorTransform = -kEditingControlAppearanceOffset;
break;
case UIUserInterfaceLayoutDirectionRightToLeft:
txReorderTransform = -kEditingControlAppearanceOffset;
txSelectorTransform = kEditingControlAppearanceOffset;
break;
}
self->_editingReorderImageView.alpha = self->_attr.shouldShowReorderStateMask ? 1 : 0;
self->_editingReorderImageView.transform = self->_attr.shouldShowReorderStateMask ?
CGAffineTransformMakeTranslation(txReorderTransform, 0) : CGAffineTransformIdentity;
self->_editingSelectorImageView.alpha = self->_attr.shouldShowSelectorStateMask ? 1 : 0;
self->_editingSelectorImageView.transform = self->_attr.shouldShowSelectorStateMask ?
CGAffineTransformMakeTranslation(txSelectorTransform, 0) : CGAffineTransformIdentity;
self.accessoryView.alpha = self->_attr.shouldShowSelectorStateMask ? 0 : 1;
self->_accessoryInset.right = self->_attr.shouldShowSelectorStateMask
? kAccessoryInsetDefault.right + kEditingControlAppearanceOffset
: kAccessoryInsetDefault.right;
};
// Animate editing controls.
if (_shouldAnimateEditingViews) {
[UIView animateWithDuration:0.3 animations:editingViewLayout];
_shouldAnimateEditingViews = NO;
} else {
[UIView performWithoutAnimation:editingViewLayout];
}
}
- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
[super applyLayoutAttributes:layoutAttributes];
if ([layoutAttributes isKindOfClass:[MDCCollectionViewLayoutAttributes class]]) {
_attr = (MDCCollectionViewLayoutAttributes *)layoutAttributes;
if (_attr.representedElementCategory == UICollectionElementCategoryCell) {
// Cells are often set to editing via layout attributes so we default to animating.
// This can be overridden by ensuring layoutSubviews is called inside a non-animation block.
[self setEditing:_attr.editing animated:YES];
}
// Create image view to hold cell background image with shadowing.
if (!_backgroundImageView) {
_backgroundImageView = [[UIImageView alloc] initWithFrame:self.bounds];
self.backgroundView = _backgroundImageView;
}
_backgroundImageView.image = _attr.backgroundImage;
// Draw separator if needed.
[self drawSeparatorIfNeeded];
// Layout the accessory view and the content view.
[self layoutForegroundSubviews];
// Animate cell on appearance settings.
[self updateAppearanceAnimation];
}
}
- (void)layoutForegroundSubviews {
// First lay out the accessory view.
_accessoryView.frame = [self accessoryFrame];
// Then lay out the content view, inset by the accessory view's width.
self.contentView.frame = [self contentViewFrame];
// If necessary flip subviews for RTL.
if (self.mdf_effectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft) {
_accessoryView.frame = MDFRectFlippedHorizontally(_accessoryView.frame,
CGRectGetWidth(self.bounds));
self.contentView.frame = MDFRectFlippedHorizontally(self.contentView.frame,
CGRectGetWidth(self.bounds));
}
}
#pragma mark - Accessory Views
- (void)setAccessoryType:(MDCCollectionViewCellAccessoryType)accessoryType {
_accessoryType = accessoryType;
UIImageView *accessoryImageView =
[_accessoryView isKindOfClass:[UIImageView class]] ? (UIImageView *)_accessoryView : nil;
if (!_accessoryView && accessoryType != MDCCollectionViewCellAccessoryNone) {
// Add accessory view.
accessoryImageView = [[MDCAccessoryTypeImageView alloc] initWithFrame:CGRectZero];
_accessoryView = accessoryImageView;
_accessoryView.userInteractionEnabled = NO;
[self addSubview:_accessoryView];
}
switch (_accessoryType) {
case MDCCollectionViewCellAccessoryDisclosureIndicator: {
UIImage *image = [MDCIcons imageFor_ic_chevron_right];
if (self.mdf_effectiveUserInterfaceLayoutDirection ==
UIUserInterfaceLayoutDirectionRightToLeft) {
image = [image mdf_imageWithHorizontallyFlippedOrientation];
}
accessoryImageView.image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
break;
}
case MDCCollectionViewCellAccessoryCheckmark: {
UIImage *image = [MDCIcons imageFor_ic_check];
accessoryImageView.image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
break;
}
case MDCCollectionViewCellAccessoryDetailButton: {
UIImage *image = [MDCIcons imageFor_ic_info];
accessoryImageView.image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
break;
}
case MDCCollectionViewCellAccessoryNone: {
[_accessoryView removeFromSuperview];
_accessoryView = nil;
break;
}
}
[_accessoryView sizeToFit];
}
- (void)setAccessoryView:(UIView *)accessoryView {
if (_accessoryView) {
[_accessoryView removeFromSuperview];
}
_accessoryView = accessoryView;
if (_accessoryView) {
[self addSubview:_accessoryView];
}
}
- (CGRect)accessoryFrame {
CGSize size = _accessoryView.frame.size;
CGFloat originX = CGRectGetWidth(self.bounds) - size.width - _accessoryInset.right;
CGFloat originY = (CGRectGetHeight(self.bounds) - size.height) / 2;
return CGRectMake(originX, originY, size.width, size.height);
}
- (MDCInkView *)inkView {
if (!_inkView) {
_inkView = [[MDCInkView alloc] initWithFrame:self.bounds];
_inkView.usesLegacyInkRipple = NO;
[self addSubview:_inkView];
}
return _inkView;
}
- (void)setInkView:(MDCInkView *)inkView {
if (inkView == _inkView) {
return;
}
if (_inkView) {
[_inkView removeFromSuperview];
}
if (inkView) {
[self addSubview:inkView];
}
_inkView = inkView;
}
#pragma mark - Separator
- (void)setShouldHideSeparator:(BOOL)shouldHideSeparator {
_usesCellSeparatorHiddenOverride = YES;
_shouldHideSeparator = shouldHideSeparator;
[self drawSeparatorIfNeeded];
}
- (void)setSeparatorInset:(UIEdgeInsets)separatorInset {
_usesCellSeparatorInsetOverride = YES;
_separatorInset = separatorInset;
[self drawSeparatorIfNeeded];
}
- (void)drawSeparatorIfNeeded {
// Determine separator spec from attributes and cell overrides. Don't draw separator for bottom
// cell or in grid layout cells. Separators are added here as cell subviews instead of decoration
// views registered with the layout to overcome inability to animate decoration views in
// coordination with cell animations.
BOOL isHidden =
_usesCellSeparatorHiddenOverride ? _shouldHideSeparator : _attr.shouldHideSeparators;
UIEdgeInsets separatorInset =
_usesCellSeparatorInsetOverride ? _separatorInset : _attr.separatorInset;
BOOL isBottom = _attr.sectionOrdinalPosition & MDCCollectionViewOrdinalPositionVerticalBottom;
BOOL isGrid = _attr.isGridLayout;
BOOL hideSeparator = isBottom || isHidden || isGrid;
if (hideSeparator != _separatorView.hidden) {
_separatorView.hidden = hideSeparator;
}
if (!hideSeparator) {
UIEdgeInsets insets = _attr.backgroundImageViewInsets;
// Compute the frame in LTR.
CGRect separatorFrame = CGRectMake(
insets.left, CGRectGetHeight(self.bounds) - _attr.separatorLineHeight,
CGRectGetWidth(self.bounds) - insets.left - insets.right, _attr.separatorLineHeight);
separatorFrame = UIEdgeInsetsInsetRect(separatorFrame, separatorInset);
if (self.mdf_effectiveUserInterfaceLayoutDirection ==
UIUserInterfaceLayoutDirectionRightToLeft) {
separatorFrame = MDFRectFlippedHorizontally(separatorFrame, CGRectGetWidth(self.bounds));
}
_separatorView.frame = separatorFrame;
_separatorView.backgroundColor = _attr.separatorColor;
}
}
#pragma mark - Editing
- (void)setEditing:(BOOL)editing {
[self setEditing:editing animated:NO];
}
- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
if (_editing == editing) {
return;
}
_shouldAnimateEditingViews = animated;
_editing = editing;
[self setNeedsLayout];
[self layoutIfNeeded];
}
- (void)updateInterfaceForEditing {
self.contentView.userInteractionEnabled = [self shouldEnableCellInteractions];
if (_editing) {
// Disable implicit animations when setting initial positioning of these subviews.
[CATransaction begin];
[CATransaction setDisableActions:YES];
// Create reorder editing controls.
if (_attr.shouldShowReorderStateMask) {
if (!_editingReorderImageView) {
UIImage *reorderImage = [MDCIcons imageFor_ic_reorder];
reorderImage = [reorderImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
_editingReorderImageView = [[UIImageView alloc] initWithImage:reorderImage];
_editingReorderImageView.tintColor = MDCCollectionViewCellGreyColor();
_editingReorderImageView.autoresizingMask =
MDFTrailingMarginAutoresizingMaskForLayoutDirection(
self.mdf_effectiveUserInterfaceLayoutDirection);
[self addSubview:_editingReorderImageView];
}
CGAffineTransform transform = _editingReorderImageView.transform;
_editingReorderImageView.transform = CGAffineTransformIdentity;
CGSize size = _editingReorderImageView.image.size;
CGRect frame =
CGRectMake(0, (CGRectGetHeight(self.bounds) - size.height) / 2, size.width, size.height);
if (self.mdf_effectiveUserInterfaceLayoutDirection ==
UIUserInterfaceLayoutDirectionRightToLeft) {
frame = MDFRectFlippedHorizontally(frame, CGRectGetWidth(self.bounds));
}
_editingReorderImageView.frame = frame;
_editingReorderImageView.transform = transform;
_editingReorderImageView.alpha = 1;
} else {
_editingReorderImageView.alpha = 0;
}
// Create selector editing controls.
if (_attr.shouldShowSelectorStateMask) {
if (!_editingSelectorImageView) {
UIImage *selectorImage = [MDCIcons imageFor_ic_radio_button_unchecked];
selectorImage = [selectorImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
_editingSelectorImageView = [[UIImageView alloc] initWithImage:selectorImage];
_editingSelectorImageView.tintColor = MDCCollectionViewCellGreyColor();
_editingSelectorImageView.autoresizingMask =
MDFLeadingMarginAutoresizingMaskForLayoutDirection(
self.mdf_effectiveUserInterfaceLayoutDirection);
[self addSubview:_editingSelectorImageView];
}
CGAffineTransform transform = _editingSelectorImageView.transform;
_editingSelectorImageView.transform = CGAffineTransformIdentity;
CGSize size = _editingSelectorImageView.image.size;
CGFloat originX = CGRectGetWidth(self.bounds) - size.width;
CGFloat originY = (CGRectGetHeight(self.bounds) - size.height) / 2;
CGRect frame = (CGRect){{originX, originY}, size};
if (self.mdf_effectiveUserInterfaceLayoutDirection ==
UIUserInterfaceLayoutDirectionRightToLeft) {
frame = MDFRectFlippedHorizontally(frame, CGRectGetWidth(self.bounds));
}
_editingSelectorImageView.frame = frame;
_editingSelectorImageView.transform = transform;
_editingSelectorImageView.alpha = 1;
} else {
_editingSelectorImageView.alpha = 0;
}
[CATransaction commit];
} else {
_editingReorderImageView.alpha = 0;
_editingSelectorImageView.alpha = 0;
}
// Update accessory view.
_accessoryView.alpha = _attr.shouldShowSelectorStateMask ? 0 : 1;
_accessoryInset.right = _attr.shouldShowSelectorStateMask
? kAccessoryInsetDefault.right + kEditingControlAppearanceOffset
: kAccessoryInsetDefault.right;
}
#pragma mark - Selecting
- (void)setSelected:(BOOL)selected {
BOOL previousSelectedState = self.selected;
[super setSelected:selected];
if (selected) {
if (_editingSelectorImageView && previousSelectedState != selected) {
_editingSelectorImageView.image = [MDCIcons imageFor_ic_check_circle];
_editingSelectorImageView.tintColor = self.editingSelectorColor;
_editingSelectorImageView.image = [_editingSelectorImageView.image
imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
}
self.accessibilityTraits |= UIAccessibilityTraitSelected;
} else {
if (_editingSelectorImageView && previousSelectedState != selected) {
_editingSelectorImageView.image = [MDCIcons imageFor_ic_radio_button_unchecked];
_editingSelectorImageView.tintColor = MDCCollectionViewCellGreyColor();
_editingSelectorImageView.image = [_editingSelectorImageView.image
imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
}
self.accessibilityTraits &= ~UIAccessibilityTraitSelected;
}
}
- (void)setEditingSelectorColor:(UIColor *)editingSelectorColor {
if (editingSelectorColor == nil) {
editingSelectorColor = MDCCollectionViewCellRedColor();
}
_editingSelectorColor = editingSelectorColor;
}
#pragma mark - Cell Appearance Animation
- (void)updateAppearanceAnimation {
if (_attr.animateCellsOnAppearanceDelay > 0) {
// Intially hide content view and separator.
self.contentView.alpha = 0;
_separatorView.alpha = 0;
// Animate fade-in after delay.
if (!_attr.willAnimateCellsOnAppearance) {
[UIView animateWithDuration:_attr.animateCellsOnAppearanceDuration
delay:_attr.animateCellsOnAppearanceDelay
options:UIViewAnimationOptionCurveEaseOut
animations:^{
self.contentView.alpha = 1;
self->_separatorView.alpha = 1;
}
completion:nil];
}
}
}
#pragma mark - RTL
// UISemanticContentAttribute was added in iOS SDK 9.0 but is available on devices running earlier
// version of iOS. We ignore the partial-availability warning that gets thrown on our use of this
// symbol.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wpartial-availability"
- (void)mdf_setSemanticContentAttribute:(UISemanticContentAttribute)mdf_semanticContentAttribute {
[super mdf_setSemanticContentAttribute:mdf_semanticContentAttribute];
// Reload the accessory type image if there is one.
if ([_accessoryView isKindOfClass:[MDCAccessoryTypeImageView class]]) {
self.accessoryType = self.accessoryType;
}
}
#pragma clang diagnostic pop
#pragma mark - Accessibility
- (UIAccessibilityTraits)accessibilityTraits {
UIAccessibilityTraits accessibilityTraits = [super accessibilityTraits];
if (self.accessoryType == MDCCollectionViewCellAccessoryCheckmark) {
accessibilityTraits |= UIAccessibilityTraitSelected;
}
return accessibilityTraits;
}
- (NSString *)accessibilityHint {
if (_attr.shouldShowSelectorStateMask) {
NSString *localizedHintKey =
self.selected ? kSelectedCellAccessibilityHintKey : kDeselectedCellAccessibilityHintKey;
return [[self class] localizedStringWithKey:localizedHintKey];
}
return nil;
}
#pragma mark - Private
+ (NSString *)localizedStringWithKey:(NSString *)key {
NSBundle *containingBundle = [NSBundle bundleForClass:self];
NSURL *resourceBundleURL =
[containingBundle URLForResource:kResourceBundleName withExtension:@"bundle"];
NSBundle *resourceBundle = [NSBundle bundleWithURL:resourceBundleURL];
return [resourceBundle localizedStringForKey:key value:nil table:kStringTableName];
}
- (CGRect)contentViewFrame {
CGFloat leadingPadding =
_attr.shouldShowReorderStateMask
? CGRectGetWidth(_editingReorderImageView.bounds) + kEditingControlAppearanceOffset
: 0;
CGFloat accessoryViewPadding =
_accessoryView ? CGRectGetWidth(self.bounds) - CGRectGetMinX(_accessoryView.frame) : 0;
CGFloat trailingPadding =
_attr.shouldShowSelectorStateMask
? CGRectGetWidth(_editingSelectorImageView.bounds) + kEditingControlAppearanceOffset
: accessoryViewPadding;
UIEdgeInsets insets = UIEdgeInsetsMake(0, leadingPadding, 0, trailingPadding);
return UIEdgeInsetsInsetRect(self.bounds, insets);
}
- (BOOL)shouldEnableCellInteractions {
return !_editing || _allowsCellInteractionsWhileEditing;
}
@end