blob: a7fac3656760e5f1ab1ba8228fc410fff7988796 [file] [log] [blame]
// Copyright 2018-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 "MDCCardCollectionCell.h"
#import "MaterialMath.h"
#import "MaterialIcons+ic_check_circle.h"
#import "MaterialShapes.h"
static NSString *const MDCCardCellBackgroundColorsKey = @"MDCCardCellBackgroundColorsKey";
static NSString *const MDCCardCellBorderWidthsKey = @"MDCCardCellBorderWidthsKey";
static NSString *const MDCCardCellBorderColorsKey = @"MDCCardCellBorderColorsKey";
static NSString *const MDCCardCellCornerRadiusKey = @"MDCCardCellCornerRadiusKey";
static NSString *const MDCCardCellHorizontalImageAlignmentsKey =
@"MDCCardCellHorizontalImageAlignmentsKey";
static NSString *const MDCCardCellImageTintColorsKey = @"MDCCardCellImageTintColorsKey";
static NSString *const MDCCardCellImagesKey = @"MDCCardCellImagesKey";
static NSString *const MDCCardCellInkViewKey = @"MDCCardCellInkViewKey";
static NSString *const MDCCardCellSelectableKey = @"MDCCardCellSelectableKey";
static NSString *const MDCCardCellSelectedImageViewKey = @"MDCCardCellSelectedImageViewKey";
static NSString *const MDCCardCellShadowElevationsKey = @"MDCCardCellShadowElevationsKey";
static NSString *const MDCCardCellShadowColorsKey = @"MDCCardCellShadowColorsKey";
static NSString *const MDCCardCellStateKey = @"MDCCardCellStateKey";
static NSString *const MDCCardCellVerticalImageAlignmentsKey =
@"MDCCardCellVerticalImageAlignmentsKey";
static NSString *const MDCCardCellIsInteractableKey = @"MDCCardCellIsInteractableKey";
static const CGFloat MDCCardCellCornerRadiusDefault = 4;
static const CGFloat MDCCardCellSelectedImagePadding = 8;
static const CGFloat MDCCardCellShadowElevationHighlighted = 8;
static const CGFloat MDCCardCellShadowElevationNormal = 1;
static const CGFloat MDCCardCellShadowElevationSelected = 8;
static const BOOL MDCCardCellIsInteractableDefault = YES;
@interface MDCCardCollectionCell ()
@property(nonatomic, strong, nullable) UIImageView *selectedImageView;
@property(nonatomic, readonly, strong) MDCShapedShadowLayer *layer;
@end
@implementation MDCCardCollectionCell {
NSMutableDictionary<NSNumber *, NSNumber *> *_shadowElevations;
NSMutableDictionary<NSNumber *, UIColor *> *_shadowColors;
NSMutableDictionary<NSNumber *, NSNumber *> *_borderWidths;
NSMutableDictionary<NSNumber *, UIColor *> *_borderColors;
NSMutableDictionary<NSNumber *, UIImage *> *_images;
NSMutableDictionary<NSNumber *, NSNumber *> *_horizontalImageAlignments;
NSMutableDictionary<NSNumber *, NSNumber *> *_verticalImageAlignments;
NSMutableDictionary<NSNumber *, UIColor *> *_imageTintColors;
UIColor *_backgroundColor;
CGPoint _lastTouch;
}
@dynamic layer;
+ (Class)layerClass {
return [MDCShapedShadowLayer class];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
_shadowElevations = [coder decodeObjectOfClass:[NSMutableDictionary class]
forKey:MDCCardCellShadowElevationsKey];
_shadowColors = [coder decodeObjectOfClass:[NSMutableDictionary class]
forKey:MDCCardCellShadowColorsKey];
_borderWidths = [coder decodeObjectOfClass:[NSMutableDictionary class]
forKey:MDCCardCellBorderWidthsKey];
_borderColors = [coder decodeObjectOfClass:[NSMutableDictionary class]
forKey:MDCCardCellBorderColorsKey];
_inkView = [coder decodeObjectOfClass:[MDCInkView class] forKey:MDCCardCellInkViewKey];
_selectedImageView = [coder decodeObjectOfClass:[UIImageView class]
forKey:MDCCardCellSelectedImageViewKey];
_state = [coder decodeIntegerForKey:MDCCardCellStateKey];
_selectable = [coder decodeBoolForKey:MDCCardCellSelectableKey];
_images = [coder decodeObjectOfClass:[NSMutableDictionary class]
forKey:MDCCardCellImagesKey];
_horizontalImageAlignments =
[coder decodeObjectOfClass:[NSMutableDictionary class]
forKey:MDCCardCellHorizontalImageAlignmentsKey];
_verticalImageAlignments = [coder decodeObjectOfClass:[NSMutableDictionary class]
forKey:MDCCardCellVerticalImageAlignmentsKey];
_imageTintColors = [coder decodeObjectOfClass:[NSMutableDictionary class]
forKey:MDCCardCellImageTintColorsKey];
if ([coder containsValueForKey:MDCCardCellCornerRadiusKey]) {
self.layer.cornerRadius = (CGFloat)[coder decodeDoubleForKey:MDCCardCellCornerRadiusKey];
} else {
self.layer.cornerRadius = MDCCardCellCornerRadiusDefault;
}
if ([coder containsValueForKey:MDCCardCellBackgroundColorsKey]) {
[self.layer setShapedBackgroundColor:
[coder decodeObjectOfClass:[UIColor class]
forKey:MDCCardCellBackgroundColorsKey]];
}
if ([coder containsValueForKey:MDCCardCellIsInteractableKey]) {
_interactable = [coder decodeBoolForKey:MDCCardCellIsInteractableKey];
} else {
_interactable = MDCCardCellIsInteractableDefault;
}
[self commonMDCCardCollectionCellInit];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.layer.cornerRadius = MDCCardCellCornerRadiusDefault;
_interactable = MDCCardCellIsInteractableDefault;
[self commonMDCCardCollectionCellInit];
}
return self;
}
- (void)commonMDCCardCollectionCellInit {
if (_inkView == nil) {
_inkView = [[MDCInkView alloc] initWithFrame:self.bounds];
_inkView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
_inkView.usesLegacyInkRipple = NO;
_inkView.layer.zPosition = FLT_MAX;
[self addSubview:_inkView];
}
if (_selectedImageView == nil) {
_selectedImageView = [[UIImageView alloc] init];
_selectedImageView.layer.zPosition = _inkView.layer.zPosition - 1;
_selectedImageView.autoresizingMask =
(UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin |
UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleBottomMargin);
[self.contentView addSubview:_selectedImageView];
_selectedImageView.hidden = YES;
}
if (_shadowElevations == nil) {
_shadowElevations = [NSMutableDictionary dictionary];
_shadowElevations[@(MDCCardCellStateNormal)] = @(MDCCardCellShadowElevationNormal);
_shadowElevations[@(MDCCardCellStateHighlighted)] = @(MDCCardCellShadowElevationHighlighted);
_shadowElevations[@(MDCCardCellStateSelected)] = @(MDCCardCellShadowElevationSelected);
}
if (_shadowColors == nil) {
_shadowColors = [NSMutableDictionary dictionary];
_shadowColors[@(MDCCardCellStateNormal)] = UIColor.blackColor;
}
if (_borderColors == nil) {
_borderColors = [NSMutableDictionary dictionary];
}
if (_borderWidths == nil) {
_borderWidths = [NSMutableDictionary dictionary];
}
if (_images == nil) {
_images = [NSMutableDictionary dictionary];
UIImage *circledCheck = [MDCIcons imageFor_ic_check_circle];
circledCheck = [circledCheck imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
_images[@(MDCCardCellStateSelected)] = circledCheck;
}
if (_horizontalImageAlignments == nil) {
_horizontalImageAlignments = [NSMutableDictionary dictionary];
_horizontalImageAlignments[@(MDCCardCellStateNormal)] =
@(MDCCardCellHorizontalImageAlignmentRight);
}
if (_verticalImageAlignments == nil) {
_verticalImageAlignments = [NSMutableDictionary dictionary];
_verticalImageAlignments[@(MDCCardCellStateNormal)] = @(MDCCardCellVerticalImageAlignmentTop);
}
if (_imageTintColors == nil) {
_imageTintColors = [NSMutableDictionary dictionary];
}
if (_backgroundColor == nil) {
_backgroundColor = UIColor.whiteColor;
}
[self updateShadowElevation];
[self updateBorderColor];
[self updateBorderWidth];
[self updateShadowColor];
[self updateImage];
[self updateImageTintColor];
[self updateBackgroundColor];
}
- (void)encodeWithCoder:(NSCoder *)coder {
[super encodeWithCoder:coder];
[coder encodeObject:_shadowElevations forKey:MDCCardCellShadowElevationsKey];
[coder encodeObject:_shadowColors forKey:MDCCardCellShadowColorsKey];
[coder encodeObject:_borderWidths forKey:MDCCardCellBorderWidthsKey];
[coder encodeObject:_borderColors forKey:MDCCardCellBorderColorsKey];
[coder encodeObject:_inkView forKey:MDCCardCellInkViewKey];
[coder encodeObject:_selectedImageView forKey:MDCCardCellSelectedImageViewKey];
[coder encodeInteger:_state forKey:MDCCardCellStateKey];
[coder encodeBool:_selectable forKey:MDCCardCellSelectableKey];
[coder encodeDouble:self.layer.cornerRadius forKey:MDCCardCellCornerRadiusKey];
[coder encodeObject:_images forKey:MDCCardCellImagesKey];
[coder encodeObject:_horizontalImageAlignments forKey:MDCCardCellHorizontalImageAlignmentsKey];
[coder encodeObject:_verticalImageAlignments forKey:MDCCardCellVerticalImageAlignmentsKey];
[coder encodeObject:_imageTintColors forKey:MDCCardCellImageTintColorsKey];
[coder encodeObject:self.layer.shapedBackgroundColor forKey:MDCCardCellBackgroundColorsKey];
[coder encodeBool:_interactable forKey:MDCCardCellIsInteractableKey];
}
- (void)layoutSubviews {
[super layoutSubviews];
if (!self.layer.shapeGenerator) {
self.layer.shadowPath = [self boundingPath].CGPath;
}
[self updateImageAlignment];
}
- (void)setCornerRadius:(CGFloat)cornerRadius {
self.layer.cornerRadius = cornerRadius;
[self setNeedsLayout];
}
- (CGFloat)cornerRadius {
return self.layer.cornerRadius;
}
- (void)setState:(MDCCardCellState)state animated:(BOOL)animated {
switch (state) {
case MDCCardCellStateSelected: {
if (_state != MDCCardCellStateHighlighted) {
if (animated) {
[self.inkView startTouchBeganAnimationAtPoint:_lastTouch completion:nil];
} else {
[self.inkView cancelAllAnimationsAnimated:NO];
[self.inkView startTouchBeganAtPoint:self.center
animated:NO
withCompletion:nil];
}
}
break;
}
case MDCCardCellStateNormal: {
[self.inkView startTouchEndAtPoint:_lastTouch
animated:animated
withCompletion:nil];
break;
}
case MDCCardCellStateHighlighted: {
// Note: setHighlighted: can get getting more calls with YES than NO when clicking rapidly.
// To guard against ink never going away and darkening our card we call
// startTouchEndedAnimationAtPoint:completion:.
[self.inkView startTouchEndedAnimationAtPoint:_lastTouch completion:nil];
[self.inkView startTouchBeganAnimationAtPoint:_lastTouch completion:nil];
break;
}
}
_state = state;
[self updateShadowElevation];
[self updateBorderColor];
[self updateBorderWidth];
[self updateShadowColor];
[self updateImage];
[self updateImageAlignment];
[self updateImageTintColor];
}
- (void)setSelected:(BOOL)selected {
[super setSelected:selected];
if (self.selectable) {
if (selected) {
[self setState:MDCCardCellStateSelected animated:NO];
} else {
[self setState:MDCCardCellStateNormal animated:NO];
}
}
}
- (void)setSelectable:(BOOL)selectable {
_selectable = selectable;
self.selectedImageView.hidden = !selectable;
}
- (UIBezierPath *)boundingPath {
CGFloat cornerRadius = self.cornerRadius;
return [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:cornerRadius];
}
- (MDCShadowElevation)shadowElevationForState:(MDCCardCellState)state {
NSNumber *elevation = _shadowElevations[@(state)];
if (state != MDCCardCellStateNormal && elevation == nil) {
elevation = _shadowElevations[@(MDCCardCellStateNormal)];
}
if (elevation != nil) {
return (CGFloat)[elevation doubleValue];
}
return 0;
}
- (void)setShadowElevation:(MDCShadowElevation)shadowElevation forState:(MDCCardCellState)state {
_shadowElevations[@(state)] = @(shadowElevation);
[self updateShadowElevation];
}
- (void)updateShadowElevation {
CGFloat elevation = [self shadowElevationForState:self.state];
if (!MDCCGFloatEqual(((MDCShadowLayer *)self.layer).elevation, elevation)) {
if (!self.layer.shapeGenerator) {
self.layer.shadowPath = [self boundingPath].CGPath;
}
[(MDCShadowLayer *)self.layer setElevation:elevation];
}
}
- (void)setBorderWidth:(CGFloat)borderWidth forState:(MDCCardCellState)state {
_borderWidths[@(state)] = @(borderWidth);
[self updateBorderWidth];
}
- (void)updateBorderWidth {
CGFloat borderWidth = [self borderWidthForState:self.state];
self.layer.shapedBorderWidth = borderWidth;
}
- (CGFloat)borderWidthForState:(MDCCardCellState)state {
NSNumber *borderWidth = _borderWidths[@(state)];
if (state != MDCCardCellStateNormal && borderWidth == nil) {
borderWidth = _borderWidths[@(MDCCardCellStateNormal)];
}
if (borderWidth != nil) {
return (CGFloat)[borderWidth doubleValue];
}
return 0;
}
- (void)setBorderColor:(UIColor *)borderColor forState:(MDCCardCellState)state {
_borderColors[@(state)] = borderColor;
[self updateBorderColor];
}
- (void)updateBorderColor {
UIColor *borderColor = [self borderColorForState:self.state];
self.layer.shapedBorderColor = borderColor;
}
- (UIColor *)borderColorForState:(MDCCardCellState)state {
UIColor *borderColor = _borderColors[@(state)];
if (state != MDCCardCellStateNormal && borderColor == nil) {
borderColor = _borderColors[@(MDCCardCellStateNormal)];
}
return borderColor;
}
- (void)setShadowColor:(UIColor *)shadowColor forState:(MDCCardCellState)state {
_shadowColors[@(state)] = shadowColor;
[self updateShadowColor];
}
- (void)updateShadowColor {
CGColorRef shadowColor = [self shadowColorForState:self.state].CGColor;
self.layer.shadowColor = shadowColor;
}
- (UIColor *)shadowColorForState:(MDCCardCellState)state {
UIColor *shadowColor = _shadowColors[@(state)];
if (state != MDCCardCellStateNormal && shadowColor == nil) {
shadowColor = _shadowColors[@(MDCCardCellStateNormal)];
}
if (shadowColor != nil) {
return shadowColor;
}
return [UIColor blackColor];
}
- (void)setImage:(UIImage *)image forState:(MDCCardCellState)state {
_images[@(state)] = image;
[self updateImage];
}
- (void)updateImage {
UIImage *image = [self imageForState:self.state];
[self.selectedImageView setImage:image];
[self.selectedImageView sizeToFit];
}
- (UIImage *)imageForState:(MDCCardCellState)state {
UIImage *image = _images[@(state)];
if (state != MDCCardCellStateNormal && image == nil) {
image = _images[@(MDCCardCellStateNormal)];
}
return image;
}
- (void)setHorizontalImageAlignment:(MDCCardCellHorizontalImageAlignment)horizontalImageAlignment
forState:(MDCCardCellState)state {
_horizontalImageAlignments[@(state)] = @(horizontalImageAlignment);
[self updateImageAlignment];
}
- (MDCCardCellHorizontalImageAlignment)horizontalImageAlignmentForState:(MDCCardCellState)state {
NSNumber *horizontalImageAlignment = _horizontalImageAlignments[@(state)];
if (state != MDCCardCellStateNormal && horizontalImageAlignment == nil) {
horizontalImageAlignment = _horizontalImageAlignments[@(MDCCardCellStateNormal)];
}
if (horizontalImageAlignment != nil) {
return (MDCCardCellHorizontalImageAlignment)[horizontalImageAlignment integerValue];
}
return MDCCardCellHorizontalImageAlignmentRight;
}
- (void)setVerticalImageAlignment:(MDCCardCellVerticalImageAlignment)verticalImageAlignment
forState:(MDCCardCellState)state {
_verticalImageAlignments[@(state)] = @(verticalImageAlignment);
[self updateImageAlignment];
}
- (MDCCardCellVerticalImageAlignment)verticalImageAlignmentForState:(MDCCardCellState)state {
NSNumber *verticalImageAlignment = _verticalImageAlignments[@(state)];
if (state != MDCCardCellStateNormal && verticalImageAlignment == nil) {
verticalImageAlignment = _verticalImageAlignments[@(MDCCardCellStateNormal)];
}
if (verticalImageAlignment != nil) {
return (MDCCardCellVerticalImageAlignment)[verticalImageAlignment integerValue];
}
return MDCCardCellVerticalImageAlignmentTop;
}
- (void)updateImageAlignment {
MDCCardCellVerticalImageAlignment verticalImageAlignment =
[self verticalImageAlignmentForState:self.state];
MDCCardCellHorizontalImageAlignment horizontalImageAlignment =
[self horizontalImageAlignmentForState:self.state];
CGFloat yAlignment = 0;
CGFloat xAlignment = 0;
switch (verticalImageAlignment) {
case MDCCardCellVerticalImageAlignmentTop:
yAlignment =
MDCCardCellSelectedImagePadding + CGRectGetHeight(self.selectedImageView.frame)/2;
break;
case MDCCardCellVerticalImageAlignmentCenter:
yAlignment = CGRectGetHeight(self.bounds)/2;
break;
case MDCCardCellVerticalImageAlignmentBottom:
yAlignment = CGRectGetHeight(self.bounds) - MDCCardCellSelectedImagePadding -
CGRectGetHeight(self.selectedImageView.frame)/2;
break;
}
switch (horizontalImageAlignment) {
case MDCCardCellHorizontalImageAlignmentLeft:
xAlignment =
MDCCardCellSelectedImagePadding + CGRectGetWidth(self.selectedImageView.frame)/2;
break;
case MDCCardCellHorizontalImageAlignmentCenter:
xAlignment = CGRectGetWidth(self.bounds)/2;
break;
case MDCCardCellHorizontalImageAlignmentRight:
xAlignment = CGRectGetWidth(self.bounds) - MDCCardCellSelectedImagePadding -
CGRectGetWidth(self.selectedImageView.frame)/2;
break;
}
self.selectedImageView.center = CGPointMake(xAlignment,
yAlignment);
}
- (void)setImageTintColor:(UIColor *)imageTintColor forState:(MDCCardCellState)state {
_imageTintColors[@(state)] = imageTintColor;
[self updateImageTintColor];
}
- (void)updateImageTintColor {
UIColor *imageTintColor = [self imageTintColorForState:self.state];
[self.selectedImageView setTintColor:imageTintColor];
}
- (UIColor *)imageTintColorForState:(MDCCardCellState)state {
UIColor *imageTintColor = _imageTintColors[@(state)];
if (state != MDCCardCellStateNormal && imageTintColor == nil) {
imageTintColor = _imageTintColors[@(MDCCardCellStateNormal)];
}
return imageTintColor;
}
- (void)tintColorDidChange {
[super tintColorDidChange];
[self setImageTintColor:self.tintColor forState:MDCCardCellStateNormal];
}
- (void)setShapeGenerator:(id<MDCShapeGenerating>)shapeGenerator {
if (shapeGenerator) {
self.layer.shadowPath = nil;
} else {
self.layer.shadowPath = [self boundingPath].CGPath;
}
self.layer.shapeGenerator = shapeGenerator;
self.layer.shadowMaskEnabled = NO;
[self updateBackgroundColor];
[self updateInkForShape];
}
- (id)shapeGenerator {
return self.layer.shapeGenerator;
}
- (void)updateInkForShape {
CGRect boundingBox = CGPathGetBoundingBox(self.layer.shapeLayer.path);
self.inkView.maxRippleRadius =
(CGFloat)(MDCHypot(CGRectGetHeight(boundingBox), CGRectGetWidth(boundingBox)) / 2 + 10);
self.inkView.layer.masksToBounds = NO;
}
- (void)setBackgroundColor:(UIColor *)backgroundColor {
_backgroundColor = backgroundColor;
[self updateBackgroundColor];
}
- (UIColor *)backgroundColor {
return _backgroundColor;
}
- (void)updateBackgroundColor {
self.layer.shapedBackgroundColor = _backgroundColor;
}
#pragma mark - UIResponder
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *result = [super hitTest:point withEvent:event];
if (!_interactable && (result == self.contentView || result == self)) {
return nil;
}
return result;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:self];
_lastTouch = location;
if (!self.selected || !self.selectable) {
[self setState:MDCCardCellStateHighlighted animated:YES];
}
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesEnded:touches withEvent:event];
if (!self.selected || !self.selectable) {
[self setState:MDCCardCellStateNormal animated:YES];
}
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesCancelled:touches withEvent:event];
if (!self.selected || !self.selectable) {
[self setState:MDCCardCellStateNormal animated:YES];
}
}
@end