blob: 18d77bd3bcbb2cfccbd1a368671e8c8e791bccce [file] [log] [blame] [edit]
// 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 "private/MDCChipView+Private.h"
#import <MDFInternationalization/MDFInternationalization.h>
#import "MaterialInk.h"
#import "MaterialMath.h"
#import "MaterialRipple.h"
#import "MaterialShadowElevations.h"
#import "MaterialShadowLayer.h"
#import "MaterialShapes.h"
#import "MaterialTypography.h"
static const MDCFontTextStyle kTitleTextStyle = MDCFontTextStyleBody2;
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 CGFloat MDCChipViewRippleDefaultOpacity = (CGFloat)0.12;
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 = MDCRound(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, strong) MDCInkView *inkView;
@property(nonatomic, strong) MDCStatefulRippleView *rippleView;
@property(nonatomic, readonly) CGFloat pixelScale;
@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 *> *_titleColors;
UIFont *_titleFont;
BOOL _mdc_adjustsFontForContentSizeCategory;
}
@synthesize mdc_overrideBaseElevation = _mdc_overrideBaseElevation;
@synthesize mdc_elevationDidChangeBlock = _mdc_elevationDidChangeBlock;
@dynamic layer;
+ (Class)layerClass {
return [MDCShapedShadowLayer class];
}
- (void)commonMDCChipViewInit {
_minimumSize = kMDCChipMinimumSizeDefault;
self.rippleAllowsSelection = YES;
self.isAccessibilityElement = YES;
self.accessibilityTraits = UIAccessibilityTraitButton;
_mdc_overrideBaseElevation = -1;
_adjustsFontForContentSizeCategoryWhenScaledFontIsUnavailable = YES;
}
- (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);
_backgroundColors = [NSMutableDictionary dictionary];
_backgroundColors[@(UIControlStateNormal)] = normal;
_backgroundColors[@(UIControlStateDisabled)] = disabled;
_backgroundColors[@(UIControlStateSelected)] = selected;
}
_borderColors = [NSMutableDictionary dictionary];
_borderWidths = [NSMutableDictionary dictionary];
_elevations = [NSMutableDictionary dictionary];
_elevations[@(UIControlStateNormal)] = @(0);
_elevations[@(UIControlStateHighlighted)] = @(MDCShadowElevationRaisedButtonPressed);
_elevations[@(UIControlStateHighlighted | UIControlStateSelected)] =
@(MDCShadowElevationRaisedButtonPressed);
_inkColors = [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];
_inkView = [[MDCInkView alloc] initWithFrame:self.bounds];
_inkView.usesLegacyInkRipple = NO;
_inkView.inkColor = [self inkColorForState:UIControlStateNormal];
[self addSubview:_inkView];
_rippleView = [[MDCStatefulRippleView alloc] initWithFrame:self.bounds];
_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;
// UIControl has a drag enter/exit boundary that is outside of the frame of the button itself.
// Because this is not exposed externally, we can't use -touchesMoved: to calculate when to
// change ink state. So instead we fall back on adding target/actions for these specific events.
[self addTarget:self
action:@selector(touchDragEnter:forEvent:)
forControlEvents:UIControlEventTouchDragEnter];
[self addTarget:self
action:@selector(touchDragExit:forEvent:)
forControlEvents:UIControlEventTouchDragExit];
self.layer.elevation = [self elevationForState:UIControlStateNormal];
self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentFill;
[self updateBackgroundColor];
[self commonMDCChipViewInit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super initWithCoder:aDecoder]) {
[self commonMDCChipViewInit];
}
return self;
}
- (void)dealloc {
[self removeTarget:self action:NULL forControlEvents:UIControlEventAllEvents];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIContentSizeCategoryDidChangeNotification
object:nil];
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
if (self.traitCollectionDidChangeBlock) {
self.traitCollectionDidChangeBlock(self, previousTraitCollection);
}
}
- (void)setShapeGenerator:(id<MDCShapeGenerating>)shapeGenerator {
if (shapeGenerator) {
self.layer.cornerRadius = 0;
self.layer.shadowPath = nil;
} else {
CGFloat cornerRadius = MIN(CGRectGetHeight(self.frame), CGRectGetWidth(self.frame)) / 2;
self.layer.cornerRadius = cornerRadius;
self.layer.shadowPath =
[UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:cornerRadius].CGPath;
}
self.layer.shapeGenerator = shapeGenerator;
[self updateBackgroundColor];
}
- (id)shapeGenerator {
return self.layer.shapeGenerator;
}
- (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];
}
}
- (BOOL)rippleAllowsSelection {
return self.rippleView.allowsSelection;
}
- (void)setRippleAllowsSelection:(BOOL)allowsSelection {
self.rippleView.allowsSelection = allowsSelection;
}
#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 self.layer.shapedBackgroundColor;
}
- (void)updateBackgroundColor {
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 {
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 {
self.layer.shapedBorderWidth = [self borderWidthForState:self.state];
}
- (CGFloat)mdc_currentElevation {
return [self elevationForState:self.state];
}
- (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.layer.elevation, newElevation)) {
self.layer.elevation = newElevation;
[self mdc_elevationDidChange];
}
}
- (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;
NSNumber *rippleState = [self rippleStateForControlState:state];
if (rippleState) {
[self.rippleView setRippleColor:inkColor forState:rippleState.integerValue];
}
[self updateInkColor];
[self updateRippleColor];
}
- (void)updateInkColor {
UIColor *inkColor = [self inkColorForState:self.state];
self.inkView.inkColor = inkColor ?: self.inkView.defaultInkColor;
}
- (void)updateRippleColor {
UIColor *rippleColor = [self inkColorForState:self.state];
// MDCStatefulRippleView sets the ripple color internally when its state changes.
// If that specific state isn't supported by the stateful ripple, then we directly set the
// ripple view's color to the requested color.
if (![self rippleStateForControlState:self.state]) {
self.rippleView.rippleColor =
rippleColor ?: [UIColor colorWithWhite:1 alpha:MDCChipViewRippleDefaultOpacity];
}
}
- (NSNumber *)rippleStateForControlState:(UIControlState)state {
// We check to see if MDCRippleState conforms to a UIControlState and return it, otherwise
// we return nil for non-supported ripple states.
switch (state) {
case UIControlStateNormal:
return [NSNumber numberWithInteger:MDCRippleStateNormal];
case UIControlStateHighlighted:
return [NSNumber numberWithInteger:MDCRippleStateHighlighted];
case UIControlStateSelected:
return [NSNumber numberWithInteger:MDCRippleStateSelected];
case (UIControlStateHighlighted | UIControlStateSelected):
return [NSNumber numberWithInteger:(MDCRippleStateHighlighted | MDCRippleStateSelected)];
default:
return nil;
}
}
- (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 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)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 if (self.adjustsFontForContentSizeCategoryWhenScaledFontIsUnavailable) {
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)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];
}
#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 - Control
- (void)setEnabled:(BOOL)enabled {
[super setEnabled:enabled];
[self updateState];
}
- (void)setHighlighted:(BOOL)highlighted {
[super setHighlighted:highlighted];
self.rippleView.rippleHighlighted = highlighted;
[self updateState];
}
- (void)setSelected:(BOOL)selected {
[super setSelected:selected];
self.rippleView.selected = 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;
if (!self.layer.shapeGenerator) {
CGFloat cornerRadius = MIN(CGRectGetHeight(self.frame), CGRectGetWidth(self.frame)) / 2;
self.layer.cornerRadius = cornerRadius;
self.layer.shadowPath =
[UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:cornerRadius].CGPath;
}
// Handle RTL
if (self.mdf_effectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft) {
for (UIView *subview in self.subviews) {
CGRect flippedRect = MDFRectFlippedHorizontally(subview.frame, CGRectGetWidth(self.bounds));
subview.frame = flippedRect;
}
}
[self updateBackgroundColor];
[self updateBorderColor];
[self updateShadowColor];
}
- (CGRect)contentRect {
CGRect contentRect = UIEdgeInsetsInsetRect(self.bounds, 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 = [self sizeForImageView:imageView maxSize:contentRect.size];
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 {
if (self.enableRippleBehavior) {
// This method needs to be invoked before the super.
// Please see the `MDCStatefulRippleView` class header for more details.
[self rippleViewTouchesBegan:touches withEvent:event];
}
[super touchesBegan:touches withEvent:event];
if (!self.enableRippleBehavior) {
[self startTouchBeganAnimationAtPoint:[self locationFromTouches:touches]];
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if (self.enableRippleBehavior) {
// This method needs to be invoked before the super.
// Please see the `MDCStatefulRippleView` class header for more details.
[self rippleViewTouchesEnded:touches withEvent:event];
}
[super touchesEnded:touches withEvent:event];
if (!self.enableRippleBehavior) {
[self startTouchEndedAnimationAtPoint:[self locationFromTouches:touches]];
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
if (self.enableRippleBehavior) {
// This method needs to be invoked before the super.
// Please see the `MDCStatefulRippleView` class header for more details.
[self rippleViewTouchesCancelled:touches withEvent:event];
}
[super touchesCancelled:touches withEvent:event];
if (!self.enableRippleBehavior) {
[self startTouchEndedAnimationAtPoint:[self locationFromTouches:touches]];
}
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if (self.enableRippleBehavior) {
// This method needs to be invoked before the super.
// Please see the `MDCStatefulRippleView` class header for more details.
[self rippleViewTouchesMoved:touches withEvent:event];
}
[super touchesMoved:touches withEvent:event];
}
- (void)touchDragEnter:(__unused MDCChipView *)button forEvent:(UIEvent *)event {
if (!self.enableRippleBehavior) {
[self startTouchBeganAnimationAtPoint:[self locationFromTouches:event.allTouches]];
}
}
- (void)touchDragExit:(__unused MDCChipView *)button forEvent:(UIEvent *)event {
if (!self.enableRippleBehavior) {
[self startTouchEndedAnimationAtPoint:[self locationFromTouches:event.allTouches]];
}
}
- (CGPoint)locationFromTouches:(NSSet *)touches {
UITouch *touch = [touches anyObject];
return [touch locationInView:self];
}
@end
@implementation MDCChipView (Private)
- (void)startTouchBeganAnimationAtPoint:(CGPoint)point {
if (!self.enabled) {
return;
}
CGSize size = [self sizeThatFits:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)];
CGFloat widthDiff = 24; // Difference between unselected and selected frame widths.
_inkView.maxRippleRadius =
(CGFloat)(MDCHypot(size.height, size.width + widthDiff) / 2 + 10 + widthDiff / 2);
[_inkView startTouchBeganAnimationAtPoint:point completion:nil];
}
- (void)startTouchEndedAnimationAtPoint:(CGPoint)point {
if (!self.enabled) {
return;
}
[_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;
}
- (void)rippleViewTouchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.rippleView touchesBegan:touches withEvent:event];
}
- (void)rippleViewTouchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.rippleView touchesEnded:touches withEvent:event];
}
- (void)rippleViewTouchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.rippleView touchesMoved:touches withEvent:event];
}
- (void)rippleViewTouchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.rippleView touchesCancelled:touches withEvent:event];
}
@end