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