blob: 40ac868d32b2bb92a64cb709967670ffac660563 [file] [log] [blame]
// Copyright 2017-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 "MDCChipView.h"
#import "private/MDCChipView+Private.h"
#import "UIView+MaterialElevationResponding.h"
#import "MDCInkView.h"
#import "MDCRippleView.h"
#import "MDCShadow.h"
#import "MDCShadowsCollection.h"
#import "MDCShadowElevations.h"
#import "MDCRoundedCornerTreatment.h"
#import "MDCRectangleShapeGenerator.h"
#import "MDCShapeMediator.h"
#import "MDCShapedShadowLayer.h"
#import "MDCFontTextStyle.h"
#import "MDCTypography.h"
#import "UIFont+MaterialScalable.h"
#import "UIFont+MaterialTypography.h"
#import "MDCMath.h"
#import <MDFInternationalization/MDFInternationalization.h> // IWYU pragma: keep
#import <MDFInternationalization/MDFRTL.h>
static const MDCFontTextStyle kTitleTextStyle = MDCFontTextStyleBody2;
// KVO context
static char *const kKVOContextMDCChipView = "kKVOContextMDCChipView";
static const CGSize kMDCChipMinimumSizeDefault = (CGSize){(CGFloat)0, (CGFloat)32};
// Creates a UIColor from a 24-bit RGB color encoded as an integer.
static inline UIColor *MDCColorFromRGB(uint32_t rgbValue) {
return [UIColor colorWithRed:((CGFloat)((rgbValue & 0xFF0000) >> 16)) / 255
green:((CGFloat)((rgbValue & 0x00FF00) >> 8)) / 255
blue:((CGFloat)((rgbValue & 0x0000FF) >> 0)) / 255
alpha:1];
}
static inline UIColor *MDCColorDarken(UIColor *color, CGFloat percent) {
CGFloat hue;
CGFloat saturation;
CGFloat brightness;
CGFloat alpha;
[color getHue:&hue saturation:&saturation brightness:&brightness alpha:&alpha];
brightness = MIN(1, MAX(0, brightness - percent));
return [UIColor colorWithHue:hue saturation:saturation brightness:brightness alpha:alpha];
}
static inline UIColor *MDCColorLighten(UIColor *color, CGFloat percent) {
return MDCColorDarken(color, -percent);
}
// TODO(samnm): Pull background color from MDCPalette
static const uint32_t MDCChipBackgroundColor = 0xEBEBEB;
static const CGFloat MDCChipSelectedDarkenPercent = (CGFloat)0.16;
static const CGFloat MDCChipDisabledLightenPercent = (CGFloat)0.38;
static const CGFloat MDCChipTitleColorWhite = (CGFloat)0.13;
static const CGFloat MDCChipTitleColorDisabledLightenPercent = (CGFloat)0.38;
static const UIEdgeInsets MDCChipContentPadding = {4, 4, 4, 4};
static const UIEdgeInsets MDCChipImagePadding = {0, 0, 0, 0};
static const UIEdgeInsets MDCChipTitlePadding = {3, 8, 4, 8};
static const UIEdgeInsets MDCChipAccessoryPadding = {0, 0, 0, 0};
static CGRect CGRectVerticallyCentered(CGRect rect,
UIEdgeInsets padding,
CGFloat height,
CGFloat pixelScale) {
CGFloat viewHeight = CGRectGetHeight(rect) + padding.top + padding.bottom;
CGFloat yValue = (height - viewHeight) / 2;
yValue = round(yValue * pixelScale) / pixelScale;
return CGRectOffset(rect, 0, yValue);
}
static inline CGRect MDCChipBuildFrame(
UIEdgeInsets insets, CGSize size, CGPoint originPoint, CGFloat chipHeight, CGFloat pixelScale) {
CGRect frame =
CGRectMake(originPoint.x + insets.left, originPoint.y + insets.top, size.width, size.height);
return CGRectVerticallyCentered(frame, insets, chipHeight, pixelScale);
}
static inline CGFloat UIEdgeInsetsHorizontal(UIEdgeInsets insets) {
return insets.left + insets.right;
}
static inline CGFloat UIEdgeInsetsVertical(UIEdgeInsets insets) {
return insets.top + insets.bottom;
}
static inline CGSize CGSizeExpandWithInsets(CGSize size, UIEdgeInsets edgeInsets) {
return CGSizeMake(size.width + UIEdgeInsetsHorizontal(edgeInsets),
size.height + UIEdgeInsetsVertical(edgeInsets));
}
static inline CGSize CGSizeShrinkWithInsets(CGSize size, UIEdgeInsets edgeInsets) {
return CGSizeMake(size.width - UIEdgeInsetsHorizontal(edgeInsets),
size.height - UIEdgeInsetsVertical(edgeInsets));
}
@interface MDCChipView ()
@property(nonatomic, readonly) CGRect contentRect;
@property(nonatomic, readonly, strong) MDCShapedShadowLayer *layer;
@property(nonatomic, readonly) BOOL showImageView;
@property(nonatomic, readonly) BOOL showSelectedImageView;
@property(nonatomic, readonly) BOOL showAccessoryView;
@property(nonatomic, assign) BOOL shouldFullyRoundCorner;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
@property(nonatomic, strong) MDCInkView *inkView;
#pragma clang diagnostic pop
@property(nonatomic, strong) MDCRippleView *rippleView;
@property(nonatomic, strong, nonnull) NSMutableDictionary<NSNumber *, UIColor *> *rippleColors;
@property(nonatomic, readonly) CGFloat pixelScale;
@property(nonatomic, assign) BOOL enableRippleBehavior;
@property(nonatomic, assign) UIEdgeInsets currentVisibleAreaInsets;
@property(nonatomic, assign) CGFloat currentCornerRadius;
@end
@implementation MDCChipView {
// For each UIControlState.
NSMutableDictionary<NSNumber *, UIColor *> *_backgroundColors;
NSMutableDictionary<NSNumber *, UIColor *> *_borderColors;
NSMutableDictionary<NSNumber *, NSNumber *> *_borderWidths;
NSMutableDictionary<NSNumber *, NSNumber *> *_elevations;
NSMutableDictionary<NSNumber *, UIColor *> *_inkColors;
NSMutableDictionary<NSNumber *, UIColor *> *_shadowColors;
NSMutableDictionary<NSNumber *, UIColor *> *_tintColors;
NSMutableDictionary<NSNumber *, UIColor *> *_titleColors;
UIFont *_titleFont;
BOOL _mdc_adjustsFontForContentSizeCategory;
MDCShapeMediator *_shapedLayer;
CGFloat _currentElevation;
}
static BOOL gEnablePerformantShadow = NO;
@synthesize mdc_overrideBaseElevation = _mdc_overrideBaseElevation;
@synthesize mdc_elevationDidChangeBlock = _mdc_elevationDidChangeBlock;
@synthesize cornerRadius = _cornerRadius;
@synthesize shadowsCollection = _shadowsCollection;
@dynamic layer;
+ (Class)layerClass {
if (gEnablePerformantShadow) {
return [super layerClass];
} else {
return [MDCShapedShadowLayer class];
}
}
- (void)commonMDCChipViewInit {
_minimumSize = kMDCChipMinimumSizeDefault;
self.isAccessibilityElement = YES;
self.accessibilityTraits = UIAccessibilityTraitButton;
self.imageViewSize = CGSizeZero;
_mdc_overrideBaseElevation = -1;
_currentElevation = 0;
if (gEnablePerformantShadow) {
_shapedLayer = [[MDCShapeMediator alloc] initWithViewLayer:self.layer];
}
[self addObservers];
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
if (!_backgroundColors) {
// _backgroundColors may have already been initialized by setting the backgroundColor setter.
UIColor *normal = MDCColorFromRGB(MDCChipBackgroundColor);
UIColor *disabled = MDCColorLighten(normal, MDCChipDisabledLightenPercent);
UIColor *selected = MDCColorDarken(normal, MDCChipSelectedDarkenPercent);
UIColor *selectedDisabled = MDCColorFromRGB(MDCChipBackgroundColor);
_backgroundColors = [NSMutableDictionary dictionary];
_backgroundColors[@(UIControlStateNormal)] = normal;
_backgroundColors[@(UIControlStateDisabled)] = disabled;
_backgroundColors[@(UIControlStateSelected)] = selected;
_backgroundColors[@(UIControlStateSelected & UIControlStateDisabled)] = selectedDisabled;
}
_borderColors = [NSMutableDictionary dictionary];
_borderWidths = [NSMutableDictionary dictionary];
_elevations = [NSMutableDictionary dictionary];
_elevations[@(UIControlStateNormal)] = @(0);
_elevations[@(UIControlStateHighlighted)] = @(MDCShadowElevationRaisedButtonPressed);
_elevations[@(UIControlStateHighlighted | UIControlStateSelected)] =
@(MDCShadowElevationRaisedButtonPressed);
_inkColors = [NSMutableDictionary dictionary];
_tintColors = [NSMutableDictionary dictionary];
UIColor *titleColor = [UIColor colorWithWhite:MDCChipTitleColorWhite alpha:1];
_titleColors = [NSMutableDictionary dictionary];
_titleColors[@(UIControlStateNormal)] = titleColor;
_titleColors[@(UIControlStateDisabled)] =
MDCColorLighten(titleColor, MDCChipTitleColorDisabledLightenPercent);
_shadowColors = [NSMutableDictionary dictionary];
_shadowColors[@(UIControlStateNormal)] = [UIColor blackColor];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
_inkView = [[MDCInkView alloc] initWithFrame:self.bounds];
#pragma clang diagnostic pop
_inkView.usesLegacyInkRipple = NO;
_inkView.inkColor = [self inkColorForState:UIControlStateNormal];
[self addSubview:_inkView];
_rippleView = [[MDCRippleView alloc] initWithFrame:self.bounds];
_rippleView.rippleColor = [self rippleColorForState:UIControlStateNormal];
_rippleColors = [NSMutableDictionary dictionary];
_imageView = [[UIImageView alloc] init];
[self addSubview:_imageView];
_selectedImageView = [[UIImageView alloc] init];
[self addSubview:_selectedImageView];
_titleLabel = [[UILabel alloc] init];
// If we are using the default (system) font loader, retrieve the
// font from the UIFont standardFont API.
if ([MDCTypography.fontLoader isKindOfClass:[MDCSystemFontLoader class]]) {
_titleLabel.font = [UIFont mdc_standardFontForMaterialTextStyle:kTitleTextStyle];
} else {
// There is a custom font loader, retrieve the font from it.
_titleLabel.font = [MDCTypography buttonFont];
}
_titleLabel.textAlignment = NSTextAlignmentCenter;
[self addSubview:_titleLabel];
_contentPadding = MDCChipContentPadding;
_imagePadding = MDCChipImagePadding;
_titlePadding = MDCChipTitlePadding;
_accessoryPadding = MDCChipAccessoryPadding;
_currentVisibleAreaInsets = UIEdgeInsetsZero;
_currentCornerRadius = 0.0f;
_centerVisibleArea = NO;
if (!gEnablePerformantShadow) {
self.layer.elevation = [self elevationForState:UIControlStateNormal];
}
self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentFill;
self.shouldFullyRoundCorner = YES;
[self updateBackgroundColor];
[self commonMDCChipViewInit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super initWithCoder:aDecoder]) {
[self commonMDCChipViewInit];
}
return self;
}
- (void)dealloc {
[self removeObservers];
[self removeTarget:self action:NULL forControlEvents:UIControlEventAllEvents];
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
if (self.traitCollectionDidChangeBlock) {
self.traitCollectionDidChangeBlock(self, previousTraitCollection);
}
}
- (void)setShapeGenerator:(id<MDCShapeGenerating>)shapeGenerator {
if (!UIEdgeInsetsEqualToEdgeInsets(self.visibleAreaInsets, UIEdgeInsetsZero)) {
// When visibleAreaInsets is not UIEdgeInsetsZero, the custom shapeGenerater should not be set
// through setter.
return;
}
[self configureLayerWithShapeGenerator:shapeGenerator];
if (!shapeGenerator && !self.shouldFullyRoundCorner) {
[self configureLayerWithCornerRadius:self.cornerRadius];
}
[self setNeedsLayout];
}
- (void)configureLayerWithShapeGenerator:(id<MDCShapeGenerating>)shapeGenerator {
if (gEnablePerformantShadow) {
_shapedLayer.shapeGenerator = shapeGenerator;
} else {
self.layer.shapeGenerator = shapeGenerator;
}
if (shapeGenerator) {
self.layer.cornerRadius = 0;
self.layer.shadowPath = nil;
}
[self updateBackgroundColor];
}
- (id)shapeGenerator {
if (gEnablePerformantShadow) {
return _shapedLayer.shapeGenerator;
}
return self.layer.shapeGenerator;
}
- (void)configureLayerWithCornerRadius:(CGFloat)cornerRadius {
if (!self.shapeGenerator &&
UIEdgeInsetsEqualToEdgeInsets(self.visibleAreaInsets, UIEdgeInsetsZero)) {
self.layer.cornerRadius = cornerRadius;
if (gEnablePerformantShadow) {
[self updateShadow];
} else {
self.layer.shadowPath =
[UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:cornerRadius].CGPath;
}
} else if (!UIEdgeInsetsEqualToEdgeInsets(self.visibleAreaInsets, UIEdgeInsetsZero)) {
[self configureLayerWithVisibleAreaInsets:self.visibleAreaInsets cornerRadius:cornerRadius];
}
}
- (void)configureLayerWithVisibleAreaInsets:(UIEdgeInsets)visibleAreaInsets
cornerRadius:(CGFloat)cornerRadius {
if (UIEdgeInsetsEqualToEdgeInsets(visibleAreaInsets, self.currentVisibleAreaInsets) &&
MDCCGFloatEqual(self.currentCornerRadius, cornerRadius)) {
return;
}
self.currentVisibleAreaInsets = visibleAreaInsets;
self.currentCornerRadius = cornerRadius;
MDCRectangleShapeGenerator *shapeGenerator = [[MDCRectangleShapeGenerator alloc] init];
MDCCornerTreatment *cornerTreatment =
[[MDCRoundedCornerTreatment alloc] initWithRadius:cornerRadius];
[shapeGenerator setCorners:cornerTreatment];
shapeGenerator.topLeftCornerOffset = CGPointMake(visibleAreaInsets.left, visibleAreaInsets.top);
shapeGenerator.topRightCornerOffset =
CGPointMake(-visibleAreaInsets.right, visibleAreaInsets.top);
shapeGenerator.bottomLeftCornerOffset =
CGPointMake(visibleAreaInsets.left, -visibleAreaInsets.bottom);
shapeGenerator.bottomRightCornerOffset =
CGPointMake(-visibleAreaInsets.right, -visibleAreaInsets.bottom);
[self configureLayerWithShapeGenerator:shapeGenerator];
}
- (void)setCornerRadius:(CGFloat)cornerRadius {
_cornerRadius = cornerRadius;
// When cornerRadius is set to a custom value, corner is not forced to be fully rounded.
self.shouldFullyRoundCorner = NO;
[self configureLayerWithCornerRadius:cornerRadius];
}
- (CGFloat)cornerRadius {
if (!self.shouldFullyRoundCorner) {
return _cornerRadius;
}
return self.layer.cornerRadius;
}
- (void)setEnableRippleBehavior:(BOOL)enableRippleBehavior {
_enableRippleBehavior = enableRippleBehavior;
if (enableRippleBehavior) {
[self.inkView removeFromSuperview];
self.rippleView.frame = self.bounds;
[self insertSubview:self.rippleView belowSubview:self.imageView];
} else {
[self.rippleView removeFromSuperview];
[self insertSubview:self.inkView belowSubview:self.imageView];
}
}
- (void)setDisableInkAndRippleBehavior:(BOOL)disableInkAndRippleBehavior {
_disableInkAndRippleBehavior = disableInkAndRippleBehavior;
[self.inkView removeFromSuperview];
[self.rippleView removeFromSuperview];
}
#pragma mark - Dynamic Type Support
- (BOOL)mdc_adjustsFontForContentSizeCategory {
return _mdc_adjustsFontForContentSizeCategory;
}
- (void)mdc_setAdjustsFontForContentSizeCategory:(BOOL)adjusts {
_mdc_adjustsFontForContentSizeCategory = adjusts;
if (_mdc_adjustsFontForContentSizeCategory) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(contentSizeCategoryDidChange:)
name:UIContentSizeCategoryDidChangeNotification
object:nil];
} else {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIContentSizeCategoryDidChangeNotification
object:nil];
}
[self updateTitleFont];
}
- (void)contentSizeCategoryDidChange:(__unused NSNotification *)notification {
[self updateTitleFont];
}
#pragma mark - Property support
- (void)setAccessoryView:(UIView *)accessoryView {
[_accessoryView removeFromSuperview];
_accessoryView = accessoryView;
if (accessoryView) {
[self insertSubview:accessoryView aboveSubview:_titleLabel];
}
}
- (nullable UIColor *)backgroundColorForState:(UIControlState)state {
UIColor *backgroundColor = _backgroundColors[@(state)];
if (!backgroundColor && state != UIControlStateNormal) {
backgroundColor = _backgroundColors[@(UIControlStateNormal)];
}
return backgroundColor;
}
- (void)setBackgroundColor:(nullable UIColor *)backgroundColor forState:(UIControlState)state {
// Since setBackgroundColor can be called in the initializer we need to optionally build the dict.
if (!_backgroundColors) {
_backgroundColors = [NSMutableDictionary dictionary];
}
_backgroundColors[@(state)] = backgroundColor;
[self updateBackgroundColor];
}
- (UIColor *)backgroundColor {
return gEnablePerformantShadow ? _shapedLayer.shapedBackgroundColor
: self.layer.shapedBackgroundColor;
}
- (void)updateBackgroundColor {
if (gEnablePerformantShadow) {
_shapedLayer.shapedBackgroundColor = [self backgroundColorForState:self.state];
} else {
self.layer.shapedBackgroundColor = [self backgroundColorForState:self.state];
}
}
- (nullable UIColor *)borderColorForState:(UIControlState)state {
UIColor *borderColor = _borderColors[@(state)];
if (!borderColor && state != UIControlStateNormal) {
borderColor = _borderColors[@(UIControlStateNormal)];
}
return borderColor;
}
- (void)setBorderColor:(nullable UIColor *)borderColor forState:(UIControlState)state {
_borderColors[@(state)] = borderColor;
[self updateBorderColor];
}
- (void)updateBorderColor {
if (gEnablePerformantShadow) {
_shapedLayer.shapedBorderColor = [self borderColorForState:self.state];
} else {
self.layer.shapedBorderColor = [self borderColorForState:self.state];
}
}
- (CGFloat)borderWidthForState:(UIControlState)state {
NSNumber *borderWidth = _borderWidths[@(state)];
if (borderWidth == nil && state != UIControlStateNormal) {
borderWidth = _borderWidths[@(UIControlStateNormal)];
}
if (borderWidth != nil) {
return (CGFloat)borderWidth.doubleValue;
}
return 0;
}
- (void)setBorderWidth:(CGFloat)borderWidth forState:(UIControlState)state {
_borderWidths[@(state)] = @(borderWidth);
[self updateBorderWidth];
}
- (void)updateBorderWidth {
if (gEnablePerformantShadow) {
_shapedLayer.shapedBorderWidth = [self borderWidthForState:self.state];
} else {
self.layer.shapedBorderWidth = [self borderWidthForState:self.state];
}
}
- (CGFloat)mdc_currentElevation {
return _currentElevation;
}
- (MDCShadowsCollection *)shadowsCollection {
if (!_shadowsCollection) {
_shadowsCollection = MDCShadowsCollectionDefault();
}
return _shadowsCollection;
}
- (CGFloat)elevationForState:(UIControlState)state {
NSNumber *elevation = _elevations[@(state)];
if (elevation == nil && state != UIControlStateNormal) {
elevation = _elevations[@(UIControlStateNormal)];
}
if (elevation != nil) {
return (CGFloat)[elevation doubleValue];
}
return 0;
}
- (void)setElevation:(CGFloat)elevation forState:(UIControlState)state {
_elevations[@(state)] = @(elevation);
[self updateElevation];
}
- (void)updateElevation {
CGFloat newElevation = [self elevationForState:self.state];
if (!MDCCGFloatEqual(self.mdc_currentElevation, newElevation)) {
_currentElevation = newElevation;
if (gEnablePerformantShadow) {
[self updateShadow];
} else {
self.layer.elevation = newElevation;
}
[self mdc_elevationDidChange];
}
}
- (void)updateShadow {
if (_shapedLayer.shapeGenerator == nil) {
MDCShadow *shadow = [self.shadowsCollection shadowForElevation:self.mdc_currentElevation];
shadow = [[MDCShadowBuilder
builderWithColor:[self shadowColorForState:self.state] ?: MDCShadowColor()
opacity:shadow.opacity
radius:shadow.radius
offset:shadow.offset
spread:shadow.spread] build];
MDCConfigureShadowForView(self, shadow);
} else {
MDCShadow *shadow = [self.shadowsCollection shadowForElevation:self.mdc_currentElevation];
shadow = [[MDCShadowBuilder
builderWithColor:[self shadowColorForState:self.state] ?: MDCShadowColor()
opacity:shadow.opacity
radius:shadow.radius
offset:shadow.offset
spread:shadow.spread] build];
MDCConfigureShadowForViewWithPath(self, shadow, self.layer.shadowPath);
}
}
- (UIColor *)inkColorForState:(UIControlState)state {
UIColor *inkColor = _inkColors[@(state)];
if (!inkColor && state != UIControlStateNormal) {
inkColor = _inkColors[@(UIControlStateNormal)];
}
return inkColor;
}
- (void)setInkColor:(UIColor *)inkColor forState:(UIControlState)state {
_inkColors[@(state)] = inkColor;
[self updateInkColor];
// Set Ripple color as well when using the Ink API.
[self setRippleColor:inkColor forState:state];
}
- (void)updateInkColor {
UIColor *inkColor = [self inkColorForState:self.state];
self.inkView.inkColor = inkColor ?: self.inkView.defaultInkColor;
}
- (UIColor *)rippleColorForState:(UIControlState)state {
UIColor *rippleColor = self.rippleColors[@(state)];
if (!rippleColor && state != UIControlStateNormal) {
rippleColor = self.rippleColors[@(UIControlStateNormal)];
}
return rippleColor;
}
- (void)setRippleColor:(UIColor *)rippleColor forState:(UIControlState)state {
_rippleColors[@(state)] = rippleColor;
[self updateRippleColor];
}
- (void)updateRippleColor {
UIColor *rippleColor = [self rippleColorForState:self.state];
self.rippleView.rippleColor = rippleColor ?: self.inkView.defaultInkColor;
}
- (nullable UIColor *)shadowColorForState:(UIControlState)state {
UIColor *shadowColor = _shadowColors[@(state)];
if (!shadowColor && state != UIControlStateNormal) {
shadowColor = _shadowColors[@(UIControlStateNormal)];
}
return shadowColor;
}
- (void)setShadowColor:(nullable UIColor *)shadowColor forState:(UIControlState)state {
_shadowColors[@(state)] = shadowColor;
[self updateShadowColor];
}
- (void)updateShadowColor {
self.layer.shadowColor = [self shadowColorForState:self.state].CGColor;
}
- (nullable UIColor *)tintColorForState:(UIControlState)state {
UIColor *tintColor = _tintColors[@(state)];
return tintColor;
}
- (nullable UIFont *)titleFont {
return _titleFont;
}
- (void)setTitleFont:(nullable UIFont *)titleFont {
_titleFont = titleFont;
[self updateTitleFont];
}
- (nullable UIColor *)titleColorForState:(UIControlState)state {
UIColor *titleColor = _titleColors[@(state)];
if (!titleColor && state != UIControlStateNormal) {
titleColor = _titleColors[@(UIControlStateNormal)];
}
return titleColor;
}
- (void)setTitleColor:(nullable UIColor *)titleColor forState:(UIControlState)state {
_titleColors[@(state)] = titleColor;
[self updateTitleColor];
}
- (void)setTintColor:(nullable UIColor *)tintColor forState:(UIControlState)state {
_tintColors[@(state)] = tintColor;
[self updateTintColor];
}
- (void)setContentHorizontalAlignment:(UIControlContentHorizontalAlignment)alignment {
[super setContentHorizontalAlignment:alignment];
[self setNeedsLayout];
}
- (void)updateTitleFont {
// If we have a custom font apply it to the label.
// If not, fall back to the Material specified font.
UIFont *titleFont = _titleFont ?: [[self class] defaultTitleFont];
// If we are automatically adjusting for Dynamic Type resize the font based on the text style
if (self.mdc_adjustsFontForContentSizeCategory) {
if (titleFont.mdc_scalingCurve) {
titleFont = [titleFont mdc_scaledFontForTraitEnvironment:self];
} else {
titleFont =
[titleFont mdc_fontSizedForMaterialTextStyle:kTitleTextStyle
scaledForDynamicType:_mdc_adjustsFontForContentSizeCategory];
}
}
self.titleLabel.font = titleFont;
[self setNeedsLayout];
}
+ (UIFont *)defaultTitleFont {
// TODO(#2709): Migrate to a single source of truth for fonts
if ([MDCTypography.fontLoader isKindOfClass:[MDCSystemFontLoader class]]) {
return [UIFont mdc_standardFontForMaterialTextStyle:kTitleTextStyle];
}
return [MDCTypography buttonFont];
}
- (void)updateTintColor {
UIColor *tintColor = [self tintColorForState:self.state];
if (tintColor != nil) {
self.imageView.tintColor = tintColor;
self.selectedImageView.tintColor = tintColor;
self.accessoryView.tintColor = tintColor;
}
}
- (void)updateTitleColor {
self.titleLabel.textColor = [self titleColorForState:self.state];
}
- (void)updateAccessibility {
// Clearing and then adding the relevant traits based on current the state (while accommodating
// concurrent states).
self.accessibilityTraits &= ~(UIAccessibilityTraitSelected | UIAccessibilityTraitNotEnabled);
if ((self.state & UIControlStateSelected) == UIControlStateSelected) {
self.accessibilityTraits |= UIAccessibilityTraitSelected;
}
if ((self.state & UIControlStateDisabled) == UIControlStateDisabled) {
self.accessibilityTraits |= UIAccessibilityTraitNotEnabled;
}
}
- (NSString *)accessibilityLabel {
NSString *accessibilityLabel = [super accessibilityLabel];
if (accessibilityLabel.length > 0) {
return accessibilityLabel;
}
accessibilityLabel = self.titleLabel.accessibilityLabel;
if (accessibilityLabel.length > 0) {
return accessibilityLabel;
}
return self.titleLabel.text;
}
- (void)updateState {
[self updateBackgroundColor];
[self updateBorderColor];
[self updateBorderWidth];
[self updateElevation];
[self updateInkColor];
[self updateRippleColor];
[self updateShadowColor];
[self updateTitleFont];
[self updateTitleColor];
[self updateAccessibility];
[self updateTintColor];
}
#pragma mark - Key-value observing
- (void)addObservers {
for (NSString *keyPath in [self titleLabelKVOKeyPaths]) {
[self.titleLabel addObserver:self
forKeyPath:keyPath
options:NSKeyValueObservingOptionNew
context:kKVOContextMDCChipView];
}
[self.imageView addObserver:self
forKeyPath:NSStringFromSelector(@selector(image))
options:NSKeyValueObservingOptionNew
context:kKVOContextMDCChipView];
}
- (void)removeObservers {
for (NSString *keyPath in [self titleLabelKVOKeyPaths]) {
[self.titleLabel removeObserver:self forKeyPath:keyPath context:kKVOContextMDCChipView];
}
[self.imageView removeObserver:self
forKeyPath:NSStringFromSelector(@selector(image))
context:kKVOContextMDCChipView];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey, id> *)change
context:(void *)context {
if (context != kKVOContextMDCChipView) {
return;
}
if (object == self.titleLabel) {
NSArray<NSString *> *titleLabelKeyPaths = [self titleLabelKVOKeyPaths];
for (NSString *titleLabelKeyPath in titleLabelKeyPaths) {
if ([titleLabelKeyPath isEqualToString:keyPath]) {
[self invalidateIntrinsicContentSize];
[self setNeedsLayout];
}
}
} else if (object == self.imageView) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(image))]) {
[self invalidateIntrinsicContentSize];
[self setNeedsLayout];
}
}
}
- (NSArray<NSString *> *)titleLabelKVOKeyPaths {
return @[
NSStringFromSelector(@selector(text)),
NSStringFromSelector(@selector(font)),
];
}
#pragma mark - Custom touch handling
- (BOOL)pointInside:(CGPoint)point withEvent:(__unused UIEvent *)event {
CGRect hitAreaRect = UIEdgeInsetsInsetRect(CGRectStandardize(self.bounds), self.hitAreaInsets);
return CGRectContainsPoint(hitAreaRect, point);
}
#pragma mark - Visible area
- (UIEdgeInsets)visibleAreaInsets {
UIEdgeInsets visibleAreaInsets = UIEdgeInsetsZero;
if (self.centerVisibleArea) {
CGSize visibleAreaSize = [self sizeThatFits:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)];
CGFloat additionalRequiredHeight =
MAX(0, CGRectGetHeight(self.bounds) - visibleAreaSize.height);
CGFloat additionalRequiredWidth = MAX(0, CGRectGetWidth(self.bounds) - visibleAreaSize.width);
visibleAreaInsets.top = ceil(additionalRequiredHeight * 0.5f);
visibleAreaInsets.bottom = additionalRequiredHeight - visibleAreaInsets.top;
visibleAreaInsets.left = ceil(additionalRequiredWidth * 0.5f);
visibleAreaInsets.right = additionalRequiredWidth - visibleAreaInsets.left;
}
return visibleAreaInsets;
}
#pragma mark - Control
- (void)setEnabled:(BOOL)enabled {
[super setEnabled:enabled];
[self updateState];
}
- (void)setHighlighted:(BOOL)highlighted {
if (!self.isEnabled) {
return;
}
[super setHighlighted:highlighted];
[self updateState];
}
- (void)setSelected:(BOOL)selected {
if (!self.isEnabled) {
return;
}
[super setSelected:selected];
[self updateState];
[self setNeedsLayout];
}
#pragma mark - Layout
- (void)layoutSubviews {
[super layoutSubviews];
_inkView.frame = self.bounds;
_imageView.frame = [self imageViewFrame];
_selectedImageView.frame = [self selectedImageViewFrame];
_accessoryView.frame = [self accessoryViewFrame];
_titleLabel.frame = [self titleLabelFrame];
_selectedImageView.alpha = self.showSelectedImageView ? 1 : 0;
CGFloat cornerRadius = self.cornerRadius;
if (self.shouldFullyRoundCorner) {
CGRect visibleFrame = UIEdgeInsetsInsetRect(self.frame, self.visibleAreaInsets);
cornerRadius = MIN(CGRectGetHeight(visibleFrame), CGRectGetWidth(visibleFrame)) / 2;
}
[self configureLayerWithCornerRadius:cornerRadius];
// Handle RTL
if (self.effectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft) {
for (UIView *subview in self.subviews) {
CGRect flippedRect = MDFRectFlippedHorizontally(subview.frame, CGRectGetWidth(self.bounds));
subview.frame = flippedRect;
}
}
if (gEnablePerformantShadow && _shapedLayer.shapeGenerator) {
[_shapedLayer layoutShapedSublayers];
}
[self updateBackgroundColor];
[self updateBorderColor];
[self updateShadowColor];
}
- (CGRect)contentRect {
CGRect contentRect = UIEdgeInsetsInsetRect(self.bounds, self.visibleAreaInsets);
contentRect = UIEdgeInsetsInsetRect(contentRect, self.contentPadding);
UIControlContentHorizontalAlignment alignment = self.contentHorizontalAlignment;
if (alignment != UIControlContentHorizontalAlignmentCenter) {
return contentRect;
}
// Calculate the minimum width needed for all the content. If it's less than contentSize.width,
// then inset to center. If not, just return contentRect.
CGFloat neededContentWidth = 0;
CGSize maxContentSize = contentRect.size;
// If there's an imageView, add it and its padding.
if (self.showImageView || self.showSelectedImageView) {
CGFloat maxImageWidth = 0;
if (self.showImageView) {
maxImageWidth = [self sizeForImageView:self.imageView maxSize:maxContentSize].width;
}
if (self.showSelectedImageView) {
maxImageWidth =
MAX(maxImageWidth,
[self sizeForImageView:self.selectedImageView maxSize:maxContentSize].width);
}
neededContentWidth += maxImageWidth + UIEdgeInsetsHorizontal(self.imagePadding);
}
// Always add the title and its padding.
neededContentWidth += [_titleLabel sizeThatFits:maxContentSize].width;
neededContentWidth += UIEdgeInsetsHorizontal(_titlePadding);
// If there's an accessoryView, add it and its padding.
if (self.showAccessoryView) {
neededContentWidth += [self sizeForAccessoryViewWithMaxSize:maxContentSize].width;
neededContentWidth += UIEdgeInsetsHorizontal(self.accessoryPadding);
}
CGFloat difference = maxContentSize.width - neededContentWidth;
if (difference > 0) {
CGFloat padding = difference / 2;
contentRect.size.width -= difference;
contentRect.origin.x += padding;
}
return contentRect;
}
- (CGRect)imageViewFrame {
return [self frameForImageView:self.imageView visible:self.showImageView];
}
- (CGRect)selectedImageViewFrame {
return [self frameForImageView:self.selectedImageView visible:self.showSelectedImageView];
}
- (CGRect)frameForImageView:(UIImageView *)imageView visible:(BOOL)visible {
CGRect contentRect = self.contentRect;
CGRect frame = CGRectMake(CGRectGetMinX(contentRect), CGRectGetMidY(contentRect), 0, 0);
if (visible) {
CGSize selectedSize = CGSizeEqualToSize(self.imageViewSize, CGSizeZero)
? [self sizeForImageView:imageView maxSize:contentRect.size]
: self.imageViewSize;
frame = MDCChipBuildFrame(_imagePadding, selectedSize,
CGPointMake(CGRectGetMinX(contentRect), CGRectGetMinY(contentRect)),
CGRectGetHeight(contentRect), self.pixelScale);
}
return frame;
}
- (CGSize)sizeForImageView:(UIImageView *)imageView maxSize:(CGSize)maxSize {
CGSize availableSize = CGSizeShrinkWithInsets(maxSize, self.imagePadding);
return [imageView sizeThatFits:availableSize];
}
- (CGRect)accessoryViewFrame {
CGSize size = CGSizeZero;
CGRect contentRect = self.contentRect;
if (self.showAccessoryView) {
size = [self sizeForAccessoryViewWithMaxSize:contentRect.size];
}
CGFloat xOffset =
CGRectGetMaxX(self.contentRect) - size.width - UIEdgeInsetsHorizontal(_accessoryPadding);
CGPoint frameOrigin = CGPointMake(xOffset, CGRectGetMinY(contentRect));
return MDCChipBuildFrame(_accessoryPadding, size, frameOrigin, CGRectGetHeight(contentRect),
self.pixelScale);
}
- (CGSize)sizeForAccessoryViewWithMaxSize:(CGSize)maxSize {
CGSize availableSize = CGSizeShrinkWithInsets(maxSize, self.accessoryPadding);
return [_accessoryView sizeThatFits:availableSize];
}
- (CGRect)titleLabelFrame {
// Default to the unselected image, but account for the selected image if it's shown.
CGRect imageFrame = _imageView.frame;
if (self.showSelectedImageView) {
// Both images are present, take the union of their frames.
if (self.showImageView) {
imageFrame = CGRectUnion(_imageView.frame, _selectedImageView.frame);
} else {
imageFrame = _selectedImageView.frame;
}
}
CGRect contentRect = self.contentRect;
CGFloat maximumTitleWidth = CGRectGetWidth(contentRect) - CGRectGetWidth(imageFrame) -
UIEdgeInsetsHorizontal(_titlePadding);
if (self.showImageView || self.showSelectedImageView) {
maximumTitleWidth -= UIEdgeInsetsHorizontal(_imagePadding);
}
if (self.showAccessoryView) {
maximumTitleWidth -=
CGRectGetWidth(_accessoryView.frame) + UIEdgeInsetsHorizontal(_accessoryPadding);
}
CGFloat maximumTitleHeight = CGRectGetHeight(contentRect) - UIEdgeInsetsVertical(_titlePadding);
CGSize maximumSize = CGSizeMake(maximumTitleWidth, maximumTitleHeight);
CGSize titleSize = [_titleLabel sizeThatFits:maximumSize];
titleSize.width = MAX(0, maximumTitleWidth);
CGFloat imageRightEdge = CGRectGetMinX(contentRect);
if (self.showImageView || self.showSelectedImageView) {
imageRightEdge = CGRectGetMaxX(imageFrame) + _imagePadding.right;
}
CGPoint frameOrigin = CGPointMake(imageRightEdge, CGRectGetMinY(contentRect));
return MDCChipBuildFrame(_titlePadding, titleSize, frameOrigin, CGRectGetHeight(contentRect),
self.pixelScale);
}
- (CGSize)sizeThatFits:(CGSize)size {
CGSize contentPaddedSize = CGSizeShrinkWithInsets(size, self.contentPadding);
CGSize imagePaddedSize = CGSizeShrinkWithInsets(contentPaddedSize, self.imagePadding);
CGSize titlePaddedSize = CGSizeShrinkWithInsets(contentPaddedSize, self.titlePadding);
CGSize accessoryPaddedSize = CGSizeShrinkWithInsets(contentPaddedSize, self.accessoryPadding);
CGSize imageSize = CGSizeZero;
CGSize selectedSize = CGSizeZero;
if (self.showImageView) {
imageSize =
CGSizeExpandWithInsets([_imageView sizeThatFits:imagePaddedSize], self.imagePadding);
}
if (self.showSelectedImageView) {
selectedSize = CGSizeExpandWithInsets([_selectedImageView sizeThatFits:imagePaddedSize],
self.imagePadding);
}
imageSize.width = MAX(imageSize.width, selectedSize.width);
imageSize.height = MAX(imageSize.height, selectedSize.height);
CGSize originalTitleSize = [_titleLabel sizeThatFits:titlePaddedSize];
CGSize titleSize = CGSizeExpandWithInsets(originalTitleSize, self.titlePadding);
CGSize accessorySize = CGSizeZero;
if (_accessoryView) {
accessorySize = CGSizeExpandWithInsets([_accessoryView sizeThatFits:accessoryPaddedSize],
self.accessoryPadding);
}
CGSize contentSize =
CGSizeMake(imageSize.width + titleSize.width + accessorySize.width,
MAX(imageSize.height, MAX(titleSize.height, accessorySize.height)));
CGSize chipSize = CGSizeExpandWithInsets(contentSize, self.contentPadding);
if (self.minimumSize.width > 0) {
chipSize.width = MAX(self.minimumSize.width, chipSize.width);
}
if (self.minimumSize.height > 0) {
chipSize.height = MAX(self.minimumSize.height, chipSize.height);
}
return MDCSizeCeilWithScale(chipSize, self.pixelScale);
}
- (CGSize)intrinsicContentSize {
return [self sizeThatFits:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)];
}
- (void)willMoveToSuperview:(UIView *)newSuperview {
[super willMoveToSuperview:newSuperview];
[self.inkView cancelAllAnimationsAnimated:NO];
[self.rippleView cancelAllRipplesAnimated:NO completion:nil];
}
- (BOOL)showImageView {
return self.imageView.image != nil;
}
- (BOOL)showSelectedImageView {
return self.selected && self.selectedImageView.image != nil;
}
- (BOOL)showAccessoryView {
return self.accessoryView && !self.accessoryView.hidden;
}
- (CGFloat)pixelScale {
return self.window.screen ? self.window.screen.scale : UIScreen.mainScreen.scale;
}
#pragma mark - Ink Touches
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
[self startTouchBeganAnimationAtPoint:[self locationFromTouches:touches]];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesEnded:touches withEvent:event];
[self startTouchEndedAnimationAtPoint:[self locationFromTouches:touches]];
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesCancelled:touches withEvent:event];
[self startTouchEndedAnimationAtPoint:[self locationFromTouches:touches]];
}
- (CGPoint)locationFromTouches:(NSSet *)touches {
UITouch *touch = [touches anyObject];
return [touch locationInView:self];
}
@end
@implementation MDCChipView (Private)
- (void)startTouchBeganAnimationAtPoint:(CGPoint)point {
if (!self.enabled) {
return;
}
if (self.disableInkAndRippleBehavior) {
return;
}
CGSize size = [self sizeThatFits:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)];
CGFloat widthDiff = 24; // Difference between unselected and selected frame widths.
CGFloat maxRadius =
(CGFloat)(hypot(size.height, size.width + widthDiff) / 2 + 10 + widthDiff / 2);
if (self.enableRippleBehavior) {
_rippleView.maximumRadius = maxRadius;
[_rippleView beginRippleTouchDownAtPoint:point animated:YES completion:nil];
} else {
_inkView.maxRippleRadius = maxRadius;
[_inkView startTouchBeganAnimationAtPoint:point completion:nil];
}
}
- (void)startTouchEndedAnimationAtPoint:(CGPoint)point {
if (self.disableInkAndRippleBehavior) {
return;
}
if (self.enableRippleBehavior) {
[_rippleView beginRippleTouchUpAnimated:YES completion:nil];
} else {
[_inkView startTouchEndedAnimationAtPoint:point completion:nil];
}
}
- (BOOL)willChangeSizeWithSelectedValue:(BOOL)selected {
if (selected == self.isSelected) {
return NO;
}
BOOL hasImage = self.imageView.image != nil;
BOOL hasSelectedImage = self.selectedImageView.image != nil;
return !hasImage && hasSelectedImage;
}
#pragma mark - Performant Shadow Toggle
+ (void)setEnablePerformantShadow:(BOOL)enable {
gEnablePerformantShadow = enable;
}
+ (BOOL)enablePerformantShadow {
return gEnablePerformantShadow;
}
@end