blob: 09a26c3997745201ffbbaad1b8aecec75387f608 [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 "MDCTextInputControllerBase.h"
#import "private/MDCTextInputControllerBase+Subclassing.h"
#import "MDCMultilineTextField.h"
#import "MDCTextField.h"
#import "MDCTextInput.h"
#import "MDCTextInputBorderView.h"
#import "MDCTextInputCharacterCounter.h"
#import "MDCTextInputController.h"
#import "MDCTextInputControllerFloatingPlaceholder.h"
#import "MDCTextInputUnderlineView.h"
#import "MaterialAnimationTiming.h"
#import "MaterialMath.h"
#import "MaterialPalettes.h"
#import "MaterialTypography.h"
#pragma mark - Constants
const CGFloat MDCTextInputControllerBaseDefaultBorderRadius = 4;
static const CGFloat MDCTextInputControllerBaseDefaultFloatingPlaceholderScaleDefault =
(CGFloat)0.75;
static const CGFloat MDCTextInputControllerBaseDefaultHintTextOpacity = (CGFloat)0.54;
static const CGFloat MDCTextInputControllerBaseDefaultPadding = 8;
static const NSTimeInterval
MDCTextInputControllerBaseDefaultFloatingPlaceholderDownAnimationDuration = (CGFloat)0.266666;
static const NSTimeInterval
MDCTextInputControllerBaseDefaultFloatingPlaceholderUpAnimationDuration = (CGFloat)0.3;
static inline UIColor *MDCTextInputControllerBaseDefaultInlinePlaceholderTextColorDefault() {
return [UIColor colorWithWhite:0 alpha:MDCTextInputControllerBaseDefaultHintTextOpacity];
}
static inline UIColor *MDCTextInputControllerBaseDefaultActiveColorDefault() {
return [MDCPalette bluePalette].accent700;
}
static inline UIColor *MDCTextInputControllerBaseDefaultNormalUnderlineColorDefault() {
return [UIColor lightGrayColor];
}
static inline UIColor *MDCTextInputControllerBaseDefaultTextErrorColorDefault() {
return [MDCPalette redPalette].accent400;
}
#pragma mark - Class Properties
static BOOL _floatingEnabledDefault = YES;
static BOOL _mdc_adjustsFontForContentSizeCategoryDefault = NO;
static CGFloat _floatingPlaceholderScaleDefault =
MDCTextInputControllerBaseDefaultFloatingPlaceholderScaleDefault;
static CGFloat _underlineHeightActiveDefault = 0;
static CGFloat _underlineHeightNormalDefault = 0;
static UIColor *_activeColorDefault;
static UIColor *_borderFillColorDefault;
static UIColor *_disabledColorDefault;
static UIColor *_errorColorDefault;
static UIColor *_floatingPlaceholderActiveColorDefault;
static UIColor *_floatingPlaceholderNormalColorDefault;
static UIColor *_inlinePlaceholderColorDefault;
static UIColor *_leadingUnderlineLabelTextColorDefault;
static UIColor *_normalColorDefault;
static UIColor *_trailingUnderlineLabelTextColorDefault;
static UIFont *_inlinePlaceholderFontDefault;
static UIFont *_leadingUnderlineLabelFontDefault;
static UIFont *_textInputFontDefault;
static UIColor *_textInputClearButtonTintColorDefault;
static UIFont *_trailingUnderlineLabelFontDefault;
static UIRectCorner _roundedCornersDefault = 0;
static UITextFieldViewMode _underlineViewModeDefault = UITextFieldViewModeWhileEditing;
@interface MDCTextInputControllerBase () {
BOOL _mdc_adjustsFontForContentSizeCategory;
MDCTextInputAllCharactersCounter *_characterCounter;
NSNumber *_floatingPlaceholderScale;
UIColor *_activeColor;
UIColor *_borderFillColor;
UIColor *_disabledColor;
UIColor *_errorColor;
UIColor *_floatingPlaceholderActiveColor;
UIColor *_floatingPlaceholderNormalColor;
UIColor *_inlinePlaceholderColor;
UIColor *_leadingUnderlineLabelTextColor;
UIColor *_normalColor;
UIColor *_trailingUnderlineLabelTextColor;
UIFont *_inlinePlaceholderFont;
UIFont *_leadingUnderlineLabelFont;
UIFont *_textInputFont;
UIColor *_textInputClearButtonTintColor;
UIFont *_trailingUnderlineLabelFont;
UIRectCorner _roundedCorners;
}
@property(nonatomic, strong) MDCTextInputAllCharactersCounter *internalCharacterCounter;
@property(nonatomic, copy) NSArray<NSLayoutConstraint *> *placeholderAnimationConstraints;
@property(nonatomic, strong) NSLayoutConstraint *placeholderAnimationConstraintLeading;
@property(nonatomic, strong) NSLayoutConstraint *placeholderAnimationConstraintTop;
@property(nonatomic, strong) NSLayoutConstraint *placeholderAnimationConstraintTrailing;
@property(nonatomic, copy) NSString *errorAccessibilityValue;
@property(nonatomic, copy, readwrite) NSString *errorText;
@property(nonatomic, copy) NSString *helperAccessibilityLabel;
@property(nonatomic, copy) NSString *previousLeadingText;
@end
@implementation MDCTextInputControllerBase
@synthesize characterCountMax = _characterCountMax;
@synthesize characterCountViewMode = _characterCountViewMode;
@synthesize floatingEnabled = _floatingEnabled;
@synthesize roundedCorners = _roundedCorners;
@synthesize textInput = _textInput;
@synthesize underlineHeightActive = _underlineHeightActive;
@synthesize underlineHeightNormal = _underlineHeightNormal;
@synthesize underlineViewMode = _underlineViewMode;
// TODO: (larche): Support in-line auto complete.
- (instancetype)init {
self = [super init];
if (self) {
if ([self isMemberOfClass:[MDCTextInputControllerBase class]]) {
NSLog(@"NOTE: MDCTextInputControllerBase is a base class meant to be subclassed.\nFor more"
"information, see the README.md file");
}
[self commonMDCTextInputControllerBaseInitialization];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
[self commonMDCTextInputControllerBaseInitialization];
// This should happen last because it relies on the state of a ton of properties.
[self setupInput];
}
return self;
}
- (instancetype)initWithTextInput:(UIView<MDCTextInput> *)textInput {
self = [self init];
if (self) {
_textInput = textInput;
// This should happen last because it relies on the state of a ton of properties.
[self setupInput];
}
return self;
}
- (instancetype)copyWithZone:(__unused NSZone *)zone {
MDCTextInputControllerBase *copy = [[[self class] alloc] init];
copy.activeColor = self.activeColor;
copy.borderFillColor = self.borderFillColor;
copy.characterCounter = self.characterCounter; // Just a pointer value copy
copy.characterCountViewMode = self.characterCountViewMode;
copy.characterCountMax = self.characterCountMax;
copy.roundedCorners = self.roundedCorners;
copy.disabledColor = self.disabledColor;
copy.errorAccessibilityValue = [self.errorAccessibilityValue copy];
copy.errorColor = self.errorColor;
copy.errorText = [self.errorText copy];
copy.expandsOnOverflow = self.expandsOnOverflow;
copy.floatingEnabled = self.isFloatingEnabled;
copy.floatingPlaceholderActiveColor = self.floatingPlaceholderActiveColor;
copy.floatingPlaceholderNormalColor = self.floatingPlaceholderNormalColor;
copy.floatingPlaceholderScale = self.floatingPlaceholderScale;
copy.helperText = [self.helperText copy];
copy.inlinePlaceholderColor = self.inlinePlaceholderColor;
copy.inlinePlaceholderFont = self.inlinePlaceholderFont;
copy.leadingUnderlineLabelFont = self.leadingUnderlineLabelFont;
copy.leadingUnderlineLabelTextColor = self.leadingUnderlineLabelTextColor;
copy.minimumLines = self.minimumLines;
copy.normalColor = self.normalColor;
copy.previousLeadingText = [self.previousLeadingText copy];
copy.textInput = self.textInput; // Just a pointer value copy
copy.trailingUnderlineLabelFont = self.trailingUnderlineLabelFont;
copy.trailingUnderlineLabelTextColor = self.trailingUnderlineLabelTextColor;
copy.underlineHeightActive = self.underlineHeightActive;
copy.underlineHeightNormal = self.underlineHeightNormal;
copy.underlineViewMode = self.underlineViewMode;
return copy;
}
- (void)dealloc {
[self unsubscribeFromNotifications];
}
- (void)commonMDCTextInputControllerBaseInitialization {
_roundedCorners = [self class].roundedCornersDefault;
_characterCountViewMode = UITextFieldViewModeAlways;
_disabledColor = [self class].disabledColorDefault;
_expandsOnOverflow = YES;
_floatingEnabled = [self class].isFloatingEnabledDefault;
_internalCharacterCounter = [[MDCTextInputAllCharactersCounter alloc] init];
_minimumLines = 1;
_underlineHeightActive = [self class].underlineHeightActiveDefault;
_underlineHeightNormal = [self class].underlineHeightNormalDefault;
_underlineViewMode = [self class].underlineViewModeDefault;
_textInput.hidesPlaceholderOnInput = NO;
[self forceUpdatePlaceholderY];
}
- (void)setupInput {
if (!_textInput) {
return;
}
// This controller will handle Dynamic Type and all fonts for the text input
_mdc_adjustsFontForContentSizeCategory =
_textInput.mdc_adjustsFontForContentSizeCategory ||
[self class].mdc_adjustsFontForContentSizeCategoryDefault;
_textInput.underline.disabledColor = self.disabledColor;
_textInput.mdc_adjustsFontForContentSizeCategory = NO;
_textInput.positioningDelegate = self;
_textInput.hidesPlaceholderOnInput = !self.isFloatingEnabled;
_textInput.textInsetsMode = [self textInsetModeDefault];
if ([_textInput conformsToProtocol:@protocol(MDCMultilineTextInput)] &&
[_textInput respondsToSelector:@selector(setMinimumLines:)]) {
((id<MDCMultilineTextInput>)_textInput).minimumLines = self.minimumLines;
}
if ([_textInput conformsToProtocol:@protocol(MDCMultilineTextInput)] &&
[_textInput respondsToSelector:@selector(setExpandsOnOverflow:)]) {
((id<MDCMultilineTextInput>)_textInput).expandsOnOverflow = self.expandsOnOverflow;
}
[self subscribeForNotifications];
_textInput.underline.color = [self class].normalColorDefault;
_textInput.clearButton.tintColor = self.textInputClearButtonTintColor;
[self forceUpdatePlaceholderY];
}
- (MDCTextInputTextInsetsMode)textInsetModeDefault {
return MDCTextInputTextInsetsModeAlways;
}
- (void)subscribeForNotifications {
if (!_textInput) {
return;
}
NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
[defaultCenter addObserver:self
selector:@selector(textInputDidToggleEnabled:)
name:MDCTextInputDidToggleEnabledNotification
object:_textInput];
if ([_textInput isKindOfClass:[UITextField class]]) {
[defaultCenter addObserver:self
selector:@selector(textInputDidBeginEditing:)
name:UITextFieldTextDidBeginEditingNotification
object:_textInput];
[defaultCenter addObserver:self
selector:@selector(textInputDidChange:)
name:UITextFieldTextDidChangeNotification
object:_textInput];
[defaultCenter addObserver:self
selector:@selector(textInputDidEndEditing:)
name:UITextFieldTextDidEndEditingNotification
object:_textInput];
}
if ([_textInput isKindOfClass:[MDCMultilineTextField class]]) {
MDCMultilineTextField *textField = (MDCMultilineTextField *)_textInput;
[defaultCenter addObserver:self
selector:@selector(textInputDidBeginEditing:)
name:UITextViewTextDidBeginEditingNotification
object:textField.textView];
[defaultCenter addObserver:self
selector:@selector(textInputDidChange:)
name:UITextViewTextDidChangeNotification
object:textField.textView];
[defaultCenter addObserver:self
selector:@selector(textInputDidEndEditing:)
name:UITextViewTextDidEndEditingNotification
object:textField.textView];
}
[defaultCenter addObserver:self
selector:@selector(textInputDidChange:)
name:MDCTextFieldTextDidSetTextNotification
object:_textInput];
}
- (void)unsubscribeFromNotifications {
NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
[defaultCenter removeObserver:self];
}
#pragma mark - Border Customization
- (void)updateBorder {
self.textInput.borderView.borderFillColor = self.borderFillColor;
self.textInput.borderPath = [self defaultBorderPath];
}
- (UIBezierPath *)defaultBorderPath {
CGRect borderBound = self.textInput.bounds;
borderBound.size.height = CGRectGetMaxY(self.textInput.underline.frame);
return [UIBezierPath
bezierPathWithRoundedRect:borderBound
byRoundingCorners:self.roundedCorners
cornerRadii:CGSizeMake(MDCTextInputControllerBaseDefaultBorderRadius,
MDCTextInputControllerBaseDefaultBorderRadius)];
}
#pragma mark - Character Max Implementation
- (NSUInteger)characterCount {
return [self.characterCounter characterCountForTextInput:self.textInput];
}
- (id<MDCTextInputCharacterCounter>)characterCounter {
if (!_characterCounter) {
_characterCounter = self.internalCharacterCounter;
}
return _characterCounter;
}
- (void)setCharacterCounter:(id<MDCTextInputCharacterCounter>)characterCounter {
if (_characterCounter != characterCounter) {
_characterCounter = characterCounter;
[self updateLayout];
}
}
- (void)setCharacterCountMax:(NSUInteger)characterCountMax {
if (_characterCountMax != characterCountMax) {
_characterCountMax = characterCountMax;
[self updateLayout];
}
}
#pragma mark - Cursor Customization
- (void)updateCursor {
self.textInput.cursorColor = (self.isDisplayingErrorText || self.isDisplayingCharacterCountError)
? self.errorColor
: self.activeColor;
}
#pragma mark - Leading Label Customization
- (void)updateLeadingUnderlineLabel {
UIFont *font = self.leadingUnderlineLabelFont;
if (self.mdc_adjustsFontForContentSizeCategory) {
// TODO: (#4331) This needs to be converted to the new text scheme.
font = [font mdc_fontSizedForMaterialTextStyle:MDCFontTextStyleCaption
scaledForDynamicType:_mdc_adjustsFontForContentSizeCategory];
}
self.textInput.leadingUnderlineLabel.font = font;
self.textInput.leadingUnderlineLabel.textColor =
(self.isDisplayingErrorText || self.isDisplayingCharacterCountError)
? self.errorColor
: self.leadingUnderlineLabelTextColor;
}
#pragma mark - TextInput Customization
- (void)updateTextInput {
UIFont *font = self.textInputFont;
if (self.mdc_adjustsFontForContentSizeCategory) {
font = [font mdc_fontSizedForMaterialTextStyle:MDCFontTextStyleBody1
scaledForDynamicType:_mdc_adjustsFontForContentSizeCategory];
// TODO: (#4331) This needs to be converted to the new text scheme.
}
self.textInput.font = font;
}
#pragma mark - Placeholder Customization
// This updates the placeholder's visual characteristics and not its layout. See the section
// "Placeholder Floating" for methods that update the layout.
- (void)updatePlaceholder {
UIFont *placeHolderFont = self.inlinePlaceholderFont;
if (self.mdc_adjustsFontForContentSizeCategory) {
// TODO: (#4331) This needs to be converted to the new text scheme.
placeHolderFont =
[placeHolderFont mdc_fontSizedForMaterialTextStyle:MDCFontTextStyleBody1
scaledForDynamicType:_mdc_adjustsFontForContentSizeCategory];
}
// Aside from not wanting to kick off extra work for setting a font that hasn't changed, we use
// this custom equality check to prevent an edge case that caused a 1 pixel change in width even
// when the important parts of the new font were the same as the existing font.
if (![self.textInput.placeholderLabel.font mdc_isSimplyEqual:placeHolderFont]){
self.textInput.placeholderLabel.font = placeHolderFont;
}
UIColor *placeholderColor;
if ([self isPlaceholderUp]) {
UIColor *nonErrorColor =
self.textInput.isEditing
? self.floatingPlaceholderActiveColor
: self.floatingPlaceholderNormalColor;
placeholderColor = (self.isDisplayingCharacterCountError || self.isDisplayingErrorText)
? self.errorColor
: nonErrorColor;
} else {
placeholderColor = self.inlinePlaceholderColor;
}
if (!self.textInput.isEnabled) {
placeholderColor = self.disabledColor;
}
self.textInput.placeholderLabel.textColor = placeholderColor;
}
#pragma mark - Placeholder Floating
// 'Up' in these methods always means the placeholder is floating; it's .y is smaller and it's not
// 'inline' with the text input. The placeholder also doesn't disappear when you type since it's
// functioning as a title label when 'up'.
- (void)movePlaceholderToUp:(BOOL)isToUp {
if ([self isPlaceholderUp] == isToUp) {
return;
}
CGFloat scaleFactor = (CGFloat)self.floatingPlaceholderScale.floatValue;
CGAffineTransform floatingPlaceholderScaleTransform =
CGAffineTransformMakeScale(scaleFactor, scaleFactor);
// The animation is accomplished pretty simply. A constraint for vertical and a constraint for
// horizontal offset, both with a required priority (1000), are acivated on the placeholderLabel.
// A simple scale transform is also applied. Then it's animated through the UIView animation API
// (layoutIfNeeded). If in reverse (isToUp == NO), these things are just removed / deactivated.
CGAffineTransform scaleTransform =
isToUp ? floatingPlaceholderScaleTransform : CGAffineTransformIdentity;
// We do this beforehand to flush the layout engine.
[self.textInput layoutIfNeeded];
[self updatePlaceholderAnimationConstraints:isToUp];
[UIView animateWithDuration:[CATransaction animationDuration]
animations:^{
self.textInput.placeholderLabel.transform = scaleTransform;
[self updatePlaceholder];
[self updateBorder];
[self.textInput layoutIfNeeded];
}
completion:^(__unused BOOL finished) {
if (!isToUp) {
[self cleanupPlaceholderAnimationConstraints];
}
}];
}
- (BOOL)isPlaceholderUp {
return self.placeholderAnimationConstraints.count > 0 &&
!CGAffineTransformEqualToTransform(self.textInput.placeholderLabel.transform,
CGAffineTransformIdentity);
}
// Sometimes we set up the animation constraints for floating up before we have a width for the text
// field. Since there is math that needs to compensate for the transform in relation to the width,
// we update the constants on the constraints.
//
// Note: this method is not called inside updateLayout.
- (void)updatePlaceholderAnimationConstraints:(BOOL)isToUp {
if (isToUp) {
UIOffset offset = [self floatingPlaceholderOffset];
// Remember, the insets are always in LTR. It's automatically flipped when used in RTL.
// See MDCTextInputController.h.
UIEdgeInsets insets = self.textInput.textInsets;
CGFloat leadingConstant =
[self floatingPlaceholderAnimationConstraintLeadingConstant:insets offset:offset];
if (!self.placeholderAnimationConstraintLeading) {
self.placeholderAnimationConstraintLeading =
[NSLayoutConstraint constraintWithItem:self.textInput.placeholderLabel
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self.textInput
attribute:NSLayoutAttributeLeading
multiplier:1
constant:leadingConstant];
}
self.placeholderAnimationConstraintLeading.constant = leadingConstant;
if (!self.placeholderAnimationConstraintTop) {
self.placeholderAnimationConstraintTop =
[NSLayoutConstraint constraintWithItem:self.textInput.placeholderLabel
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.textInput
attribute:NSLayoutAttributeTop
multiplier:1
constant:offset.vertical];
}
self.placeholderAnimationConstraintTop.constant = offset.vertical;
CGFloat trailingConstant =
[self floatingPlaceholderAnimationConstraintTrailingConstant:insets offset:offset];
if (!self.placeholderAnimationConstraintTrailing) {
self.placeholderAnimationConstraintTrailing =
[NSLayoutConstraint constraintWithItem:self.textInput.placeholderLabel
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:self.textInput
attribute:NSLayoutAttributeTrailing
multiplier:1
constant:trailingConstant];
self.placeholderAnimationConstraintTrailing.priority = UILayoutPriorityDefaultHigh - 1;
}
self.placeholderAnimationConstraintTrailing.constant = trailingConstant;
self.placeholderAnimationConstraints = @[
self.placeholderAnimationConstraintLeading, self.placeholderAnimationConstraintTop,
self.placeholderAnimationConstraintTrailing
];
[NSLayoutConstraint activateConstraints:self.placeholderAnimationConstraints];
} else {
[NSLayoutConstraint deactivateConstraints:self.placeholderAnimationConstraints];
}
}
// Sometimes we set up the animation constraints for floating up before we have a width for the text
// field. Since there is math that needs to compensate for the transform in relation to the width,
// we check to see if the constraints still have the correct constants.
- (BOOL)needsUpdatePlaceholderAnimationConstraintsToUp {
if (![self isPlaceholderUp]) {
return NO;
}
// Remember, the insets are always in LTR. It's automatically flipped when used in RTL.
// See MDCTextInputController.h.
UIEdgeInsets insets = self.textInput.textInsets;
UIOffset offset = [self floatingPlaceholderOffset];
CGFloat leadingConstant =
[self floatingPlaceholderAnimationConstraintLeadingConstant:insets offset:offset];
CGFloat trailingConstant =
[self floatingPlaceholderAnimationConstraintTrailingConstant:insets offset:offset];
return self.placeholderAnimationConstraintLeading.constant != leadingConstant &&
self.placeholderAnimationConstraintTrailing.constant != trailingConstant;
}
// When placeholder is not up, it just uses the layout code that comes for free from the text field
// itself. That means these constraints need go away. So, we can use the existence of them as a
// way to check if the placeholder is floating up. See isplaceholderUp.
- (void)cleanupPlaceholderAnimationConstraints {
self.placeholderAnimationConstraints = nil;
self.placeholderAnimationConstraintLeading = nil;
self.placeholderAnimationConstraintTop = nil;
self.placeholderAnimationConstraintTrailing = nil;
}
// Sometimes the text field is not showing the correct layout for its values (like when it's created
// with .text already entered) so we make sure it's in the right place always.
//
// Note that this calls updateLayout inside it so it is not included in updateLayout.
- (void)forceUpdatePlaceholderY {
BOOL isDirectionToUp = NO;
if (self.floatingEnabled) {
isDirectionToUp = self.textInput.text.length >= 1 || self.textInput.isEditing;
}
[CATransaction begin];
[CATransaction setDisableActions:YES];
[self movePlaceholderToUp:isDirectionToUp];
[self updateLayout];
[CATransaction commit];
self.textInput.hidesPlaceholderOnInput = !self.floatingEnabled;
[self.textInput layoutIfNeeded];
}
// The placeholder has a simple scale transform on it. The default is to reduce the size by 25%.
// Unfortunately, auto layout has no concept of compensating for transforms and it keeps the same
// alignmentRect as if it weren't transformed at all. That gives the impression the placeholder,
// when floating up, is too far to the trailing side. This offset is part of a compensation for
// that. It calculates the amount of space between the alignmentRect's sides and the actual shrunken
// placeholder label.
- (UIOffset)floatingPlaceholderOffset {
CGFloat vertical = MDCTextInputControllerBaseDefaultPadding;
// Offsets needed due to transform working on normal (0.5,0.5) anchor point.
// Why no anchor point of (0,0)? Because autolayout doesn't play well with anchor points.
vertical -= self.textInput.placeholderLabel.font.lineHeight *
(1 - (CGFloat)self.floatingPlaceholderScale.floatValue) * (CGFloat)0.5;
// Remember, the insets are always in LTR. It's automatically flipped when used in RTL.
// See MDCTextInputController.h.
UIEdgeInsets insets = self.textInput.textInsets;
CGFloat placeholderMaxWidth =
CGRectGetWidth(self.textInput.bounds) / self.floatingPlaceholderScale.floatValue -
insets.left - insets.right;
CGFloat placeholderWidth =
[self.textInput.placeholderLabel systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]
.width;
if (placeholderWidth > placeholderMaxWidth) {
placeholderWidth = placeholderMaxWidth;
}
CGFloat horizontal =
placeholderWidth * (1 - (CGFloat)self.floatingPlaceholderScale.floatValue) * (CGFloat)0.5;
return UIOffsetMake(horizontal, vertical);
}
// Remember, the insets are always in LTR. It's automatically flipped when used in RTL.
// See MDCTextInputController.h.
- (CGFloat)floatingPlaceholderAnimationConstraintLeadingConstant:(UIEdgeInsets)textInsets
offset:(UIOffset)offset {
CGFloat constant = textInsets.left - offset.horizontal;
return constant;
}
// Remember, the insets are always in LTR. It's automatically flipped when used in RTL.
// See MDCTextInputController.h.
- (CGFloat)floatingPlaceholderAnimationConstraintTrailingConstant:(UIEdgeInsets)textInsets
offset:(UIOffset)offset {
CGFloat constant = offset.horizontal - textInsets.right;
return constant;
}
#pragma mark - Trailing Label Customization
- (void)updateTrailingUnderlineLabel {
if (!self.characterCountMax) {
self.textInput.trailingUnderlineLabel.text = nil;
} else {
self.textInput.trailingUnderlineLabel.text = [self characterCountText];
UIFont *font = self.trailingUnderlineLabelFont;
if (self.mdc_adjustsFontForContentSizeCategory) {
// TODO: (#4331) This needs to be converted to the new text scheme.
font =
[font mdc_fontSizedForMaterialTextStyle:MDCFontTextStyleCaption
scaledForDynamicType:_mdc_adjustsFontForContentSizeCategory];
}
self.textInput.trailingUnderlineLabel.font = font;
}
UIColor *textColor = self.trailingUnderlineLabelTextColor;
if (self.isDisplayingCharacterCountError || self.isDisplayingErrorText) {
textColor = self.errorColor;
}
switch (self.characterCountViewMode) {
case UITextFieldViewModeAlways:
break;
case UITextFieldViewModeWhileEditing:
textColor = !self.textInput.isEditing ? [UIColor clearColor] : textColor;
break;
case UITextFieldViewModeUnlessEditing:
textColor = self.textInput.isEditing ? [UIColor clearColor] : textColor;
break;
case UITextFieldViewModeNever:
textColor = [UIColor clearColor];
break;
}
self.textInput.trailingUnderlineLabel.textColor = textColor;
}
- (NSString *)characterCountText {
// TODO: (larche) Localize
return [NSString stringWithFormat:@"%lu / %lu", (unsigned long)[self characterCount],
(unsigned long)self.characterCountMax];
}
#pragma mark - Underline Customization
- (void)updateUnderline {
UIColor *underlineColor;
CGFloat underlineHeight;
switch (self.underlineViewMode) {
case UITextFieldViewModeAlways:
underlineColor = self.activeColor;
underlineHeight = self.underlineHeightActive;
break;
case UITextFieldViewModeWhileEditing:
underlineColor = self.textInput.isEditing ? self.activeColor : self.normalColor;
underlineHeight =
self.textInput.isEditing ? self.underlineHeightActive : self.underlineHeightNormal;
break;
case UITextFieldViewModeUnlessEditing:
underlineColor = !self.textInput.isEditing ? self.activeColor : self.normalColor;
underlineHeight =
!self.textInput.isEditing ? self.underlineHeightActive : self.underlineHeightNormal;
break;
case UITextFieldViewModeNever:
default:
underlineColor = self.normalColor;
underlineHeight = self.underlineHeightNormal;
break;
}
self.textInput.underline.color =
(self.isDisplayingCharacterCountError || self.isDisplayingErrorText) ? self.errorColor
: underlineColor;
self.textInput.underline.lineHeight = underlineHeight;
self.textInput.underline.disabledColor = self.disabledColor;
}
#pragma mark - Underline Labels Fonts
+ (UIFont *)placeholderFont {
return [UIFont mdc_standardFontForMaterialTextStyle:MDCFontTextStyleBody1];
// TODO: (#4331) This needs to be converted to the new text scheme.
}
+ (UIFont *)underlineLabelsFont {
// TODO: (#4331) This needs to be converted to the new text scheme.
return [UIFont mdc_standardFontForMaterialTextStyle:MDCFontTextStyleCaption];
}
#pragma mark - Properties Implementation
- (UIColor *)activeColor {
return _activeColor ? _activeColor : [self class].activeColorDefault;
}
- (void)setActiveColor:(UIColor *)activeColor {
if (![_activeColor isEqual:activeColor]) {
_activeColor = activeColor;
[self updateLayout];
}
}
+ (UIColor *)activeColorDefault {
if (!_activeColorDefault) {
_activeColorDefault = MDCTextInputControllerBaseDefaultActiveColorDefault();
}
return _activeColorDefault;
}
+ (void)setActiveColorDefault:(UIColor *)activeColorDefault {
_activeColorDefault = activeColorDefault ? activeColorDefault
: MDCTextInputControllerBaseDefaultActiveColorDefault();
}
- (UIColor *)borderFillColor {
if (!_borderFillColor) {
_borderFillColor = [self class].borderFillColorDefault;
}
return _borderFillColor;
}
- (void)setBorderFillColor:(UIColor *)borderFillColor {
if (_borderFillColor != borderFillColor) {
_borderFillColor = borderFillColor ? borderFillColor : [self class].borderFillColorDefault;
[self updateBorder];
}
}
+ (UIColor *)borderFillColorDefault {
if (!_borderFillColorDefault) {
_borderFillColorDefault = [UIColor clearColor];
}
return _borderFillColorDefault;
}
+ (void)setBorderFillColorDefault:(UIColor *)borderFillColorDefault {
_borderFillColorDefault = borderFillColorDefault ? borderFillColorDefault : [UIColor clearColor];
}
- (void)setCharacterCountViewMode:(UITextFieldViewMode)characterCountViewMode {
if (_characterCountViewMode != characterCountViewMode) {
_characterCountViewMode = characterCountViewMode;
[self updateLayout];
}
}
- (UIColor *)disabledColor {
if (!_disabledColor) {
_disabledColor = [self class].disabledColorDefault;
}
return _disabledColor;
}
- (void)setDisabledColor:(UIColor *)disabledColor {
if (_disabledColor != disabledColor) {
_disabledColor = disabledColor ? disabledColor
: [self class].disabledColorDefault;
[self updateLayout];
}
}
+ (UIColor *)disabledColorDefault {
if (!_disabledColorDefault) {
_disabledColorDefault = MDCTextInputControllerBaseDefaultNormalUnderlineColorDefault();
}
return _disabledColorDefault;
}
+ (void)setDisabledColorDefault:(UIColor *)disabledColorDefault {
_disabledColorDefault = disabledColorDefault
? disabledColorDefault
: MDCTextInputControllerBaseDefaultNormalUnderlineColorDefault();
}
// This is when the character count text is red because the inputted text has more characters than
// the characterCountMax allows.
- (BOOL)isDisplayingCharacterCountError {
return self.characterCountMax && [self characterCount] > self.characterCountMax;
}
- (BOOL)isDisplayingErrorText {
return self.errorText != nil;
}
- (void)setErrorAccessibilityValue:(NSString *)errorAccessibilityValue {
_errorAccessibilityValue = [errorAccessibilityValue copy];
}
-(void)setHelperAccessibilityLabel:(NSString *)helperAccessibilityLabel {
_helperAccessibilityLabel = [helperAccessibilityLabel copy];
if ([self.textInput.leadingUnderlineLabel.text isEqualToString:self.helperText]) {
self.textInput.leadingUnderlineLabel.accessibilityLabel = _helperAccessibilityLabel;
}
}
- (UIColor *)errorColor {
if (!_errorColor) {
_errorColor = [self class].errorColorDefault;
}
return _errorColor;
}
- (void)setErrorColor:(UIColor *)errorColor {
if (![_errorColor isEqual:errorColor]) {
_errorColor = errorColor ? errorColor : [self class].errorColorDefault;
if (self.isDisplayingCharacterCountError || self.isDisplayingErrorText) {
[self updateLeadingUnderlineLabel];
[self updatePlaceholder];
[self updateTrailingUnderlineLabel];
[self updateUnderline];
}
}
}
+ (UIColor *)errorColorDefault {
if (!_errorColorDefault) {
_errorColorDefault = MDCTextInputControllerBaseDefaultTextErrorColorDefault();
}
return _errorColorDefault;
}
+ (void)setErrorColorDefault:(UIColor *)errorColorDefault {
_errorColorDefault = errorColorDefault ? errorColorDefault
: MDCTextInputControllerBaseDefaultTextErrorColorDefault();
}
- (void)setErrorText:(NSString *)errorText {
_errorText = [errorText copy];
}
- (void)setExpandsOnOverflow:(BOOL)expandsOnOverflow {
if (_expandsOnOverflow != expandsOnOverflow) {
_expandsOnOverflow = expandsOnOverflow;
if ([_textInput conformsToProtocol:@protocol(MDCMultilineTextInput)] &&
[_textInput respondsToSelector:@selector(setExpandsOnOverflow:)]) {
((id<MDCMultilineTextInput>)_textInput).expandsOnOverflow = expandsOnOverflow;
}
}
}
- (UIColor *)floatingPlaceholderActiveColor {
return _floatingPlaceholderActiveColor ? _floatingPlaceholderActiveColor
: [self class].floatingPlaceholderActiveColorDefault;
}
- (void)setFloatingPlaceholderActiveColor:(UIColor *)floatingPlaceholderActiveColor {
if (![_floatingPlaceholderActiveColor isEqual:floatingPlaceholderActiveColor]) {
_floatingPlaceholderActiveColor = floatingPlaceholderActiveColor;
[self updatePlaceholder];
}
}
+ (UIColor *)floatingPlaceholderActiveColorDefault {
if (!_floatingPlaceholderActiveColorDefault) {
_floatingPlaceholderActiveColorDefault = [self class].activeColorDefault;
}
return _floatingPlaceholderActiveColorDefault;
}
+ (void)setFloatingPlaceholderActiveColorDefault:(UIColor *)floatingPlaceholderActiveColorDefault {
_floatingPlaceholderActiveColorDefault = floatingPlaceholderActiveColorDefault
? floatingPlaceholderActiveColorDefault
: [self class].activeColorDefault;
}
- (UIColor *)floatingPlaceholderNormalColor {
return _floatingPlaceholderNormalColor ? _floatingPlaceholderNormalColor
: [self class].floatingPlaceholderNormalColorDefault;
}
- (void)setFloatingPlaceholderNormalColor:(UIColor *)floatingPlaceholderNormalColor {
if (![_floatingPlaceholderNormalColor isEqual:floatingPlaceholderNormalColor]) {
_floatingPlaceholderNormalColor = floatingPlaceholderNormalColor;
[self updatePlaceholder];
}
}
+ (UIColor *)floatingPlaceholderNormalColorDefault {
if (!_floatingPlaceholderNormalColorDefault) {
_floatingPlaceholderNormalColorDefault = [self class].inlinePlaceholderColorDefault;
}
return _floatingPlaceholderNormalColorDefault;
}
+ (void)setFloatingPlaceholderNormalColorDefault:(UIColor *)floatingPlaceholderNormalColorDefault {
_floatingPlaceholderNormalColorDefault = floatingPlaceholderNormalColorDefault
? floatingPlaceholderNormalColorDefault
: [self class].inlinePlaceholderColorDefault;
}
- (void)setFloatingEnabled:(BOOL)floatingEnabled {
if (_floatingEnabled != floatingEnabled) {
_floatingEnabled = floatingEnabled;
[self forceUpdatePlaceholderY];
}
}
+ (BOOL)isFloatingEnabledDefault {
return _floatingEnabledDefault;
}
+ (void)setFloatingEnabledDefault:(BOOL)floatingEnabledDefault {
_floatingEnabledDefault = floatingEnabledDefault;
}
- (NSNumber *)floatingPlaceholderScale {
if (_floatingPlaceholderScale == nil) {
_floatingPlaceholderScale =
[NSNumber numberWithFloat:(float)[self class].floatingPlaceholderScaleDefault];
}
return _floatingPlaceholderScale;
}
- (void)setFloatingPlaceholderScale:(NSNumber *)floatingPlaceholderScale {
if (![_floatingPlaceholderScale isEqualToNumber:floatingPlaceholderScale]) {
_floatingPlaceholderScale =
(floatingPlaceholderScale != nil)
? floatingPlaceholderScale
: [NSNumber numberWithFloat:(float)[self class].floatingPlaceholderScaleDefault];
[self updatePlaceholder];
[self.textInput setNeedsUpdateConstraints];
}
}
+ (CGFloat)floatingPlaceholderScaleDefault {
return _floatingPlaceholderScaleDefault;
}
+ (void)setFloatingPlaceholderScaleDefault:(CGFloat)floatingPlaceholderScaleDefault {
_floatingPlaceholderScaleDefault = floatingPlaceholderScaleDefault;
if (floatingPlaceholderScaleDefault <= 0) {
_floatingPlaceholderScaleDefault =
MDCTextInputControllerBaseDefaultFloatingPlaceholderScaleDefault;
}
}
- (void)setHelperText:(NSString *)helperText {
if (self.isDisplayingErrorText) {
self.previousLeadingText = helperText;
} else {
if (![self.textInput.leadingUnderlineLabel.text isEqualToString:helperText]) {
self.textInput.leadingUnderlineLabel.text = helperText;
[self updateLayout];
}
}
}
- (NSString *)helperText {
if (self.isDisplayingErrorText) {
return self.previousLeadingText;
} else {
return self.textInput.leadingUnderlineLabel.text;
}
}
- (void)setInlinePlaceholderColor:(UIColor *)inlinePlaceholderColor {
if (![_inlinePlaceholderColor isEqual:inlinePlaceholderColor]) {
_inlinePlaceholderColor = inlinePlaceholderColor;
[self updatePlaceholder];
}
}
- (UIColor *)inlinePlaceholderColor {
return _inlinePlaceholderColor ? _inlinePlaceholderColor
: [self class].inlinePlaceholderColorDefault;
}
+ (UIColor *)inlinePlaceholderColorDefault {
if (!_inlinePlaceholderColorDefault) {
_inlinePlaceholderColorDefault =
MDCTextInputControllerBaseDefaultInlinePlaceholderTextColorDefault();
}
return _inlinePlaceholderColorDefault;
}
+ (void)setInlinePlaceholderColorDefault:(UIColor *)inlinePlaceholderColorDefault {
_inlinePlaceholderColorDefault =
inlinePlaceholderColorDefault
? inlinePlaceholderColorDefault
: MDCTextInputControllerBaseDefaultInlinePlaceholderTextColorDefault();
}
- (UIFont *)inlinePlaceholderFont {
return _inlinePlaceholderFont ?: [self class].inlinePlaceholderFontDefault;
}
- (void)setInlinePlaceholderFont:(UIFont *)inlinePlaceholderFont {
if (![_inlinePlaceholderFont isEqual:inlinePlaceholderFont]) {
_inlinePlaceholderFont = inlinePlaceholderFont;
[self updateLayout];
}
}
+ (UIFont *)inlinePlaceholderFontDefault {
return _inlinePlaceholderFontDefault ?: [[self class] placeholderFont];
}
+ (void)setInlinePlaceholderFontDefault:(UIFont *)inlinePlaceholderFontDefault {
_inlinePlaceholderFontDefault = inlinePlaceholderFontDefault;
}
- (UIFont *)leadingUnderlineLabelFont {
return _leadingUnderlineLabelFont ?: [self class].leadingUnderlineLabelFontDefault;
}
- (void)setLeadingUnderlineLabelFont:(UIFont *)leadingUnderlineLabelFont {
if (![_leadingUnderlineLabelFont isEqual:leadingUnderlineLabelFont]) {
_leadingUnderlineLabelFont = leadingUnderlineLabelFont;
[self updateLayout];
}
}
+ (UIFont *)leadingUnderlineLabelFontDefault {
return _leadingUnderlineLabelFontDefault ?: [[self class] underlineLabelsFont];
}
+ (void)setLeadingUnderlineLabelFontDefault:(UIFont *)leadingUnderlineLabelFontDefault {
_leadingUnderlineLabelFontDefault = leadingUnderlineLabelFontDefault;
}
- (UIColor *)leadingUnderlineLabelTextColor {
return _leadingUnderlineLabelTextColor ? _leadingUnderlineLabelTextColor
: [self class].leadingUnderlineLabelTextColorDefault;
}
- (void)setLeadingUnderlineLabelTextColor:(UIColor *)leadingUnderlineLabelTextColor {
if (_leadingUnderlineLabelTextColor != leadingUnderlineLabelTextColor) {
_leadingUnderlineLabelTextColor = leadingUnderlineLabelTextColor
? leadingUnderlineLabelTextColor
: [self class].leadingUnderlineLabelTextColorDefault;
[self updateLeadingUnderlineLabel];
}
}
+ (UIColor *)leadingUnderlineLabelTextColorDefault {
if (!_leadingUnderlineLabelTextColorDefault) {
_leadingUnderlineLabelTextColorDefault =
MDCTextInputControllerBaseDefaultInlinePlaceholderTextColorDefault();
}
return _leadingUnderlineLabelTextColorDefault;
}
+ (void)setLeadingUnderlineLabelTextColorDefault:(UIColor *)leadingUnderlineLabelTextColorDefault {
_leadingUnderlineLabelTextColorDefault =
leadingUnderlineLabelTextColorDefault
? leadingUnderlineLabelTextColorDefault
: MDCTextInputControllerBaseDefaultInlinePlaceholderTextColorDefault();
}
- (void)setMinimumLines:(NSUInteger)minimumLines {
if (_minimumLines != minimumLines) {
_minimumLines = minimumLines;
if ([_textInput conformsToProtocol:@protocol(MDCMultilineTextInput)] &&
[_textInput respondsToSelector:@selector(setMinimumLines:)]) {
((id<MDCMultilineTextInput>)_textInput).minimumLines = minimumLines;
}
}
}
- (UIColor *)normalColor {
return _normalColor ? _normalColor : [self class].normalColorDefault;
}
- (void)setNormalColor:(UIColor *)normalColor {
if (![_normalColor isEqual:normalColor]) {
_normalColor = normalColor;
[self updateLayout];
}
}
+ (UIColor *)normalColorDefault {
if (!_normalColorDefault) {
_normalColorDefault = MDCTextInputControllerBaseDefaultNormalUnderlineColorDefault();
}
return _normalColorDefault;
}
+ (void)setNormalColorDefault:(UIColor *)normalColorDefault {
_normalColorDefault = normalColorDefault
? normalColorDefault
: MDCTextInputControllerBaseDefaultNormalUnderlineColorDefault();
}
- (NSString *)placeholderText {
return _textInput.placeholder;
}
- (void)setPlaceholderText:(NSString *)placeholderText {
if ([_textInput.placeholder isEqualToString:placeholderText]) {
return;
}
_textInput.placeholder = [placeholderText copy];
if (self.isFloatingEnabled && _textInput.text.length > 0) {
[self updatePlaceholderAnimationConstraints:YES];
}
}
- (void)setPreviousLeadingText:(NSString *)previousLeadingText {
_previousLeadingText = [previousLeadingText copy];
}
- (void)setRoundedCorners:(UIRectCorner)roundedCorners {
if (_roundedCorners != roundedCorners) {
_roundedCorners = roundedCorners;
[self updateLayout];
}
}
+ (UIRectCorner)roundedCornersDefault {
return _roundedCornersDefault;
}
+ (void)setRoundedCornersDefault:(UIRectCorner)roundedCornersDefault {
_roundedCornersDefault = roundedCornersDefault;
}
- (void)setTextInput:(UIView<MDCTextInput> *)textInput {
if (_textInput != textInput) {
[self unsubscribeFromNotifications];
_textInput = textInput;
[self setupInput];
}
}
- (UIFont *)textInputFont {
if (_textInputFont) {
return _textInputFont;
}
return [self class].textInputFontDefault ?: self.textInput.font;
}
- (void)setTextInputFont:(UIFont *)textInputFont {
if (![_textInputFont isEqual:textInputFont]) {
_textInputFont = textInputFont;
[self updateLayout];
}
}
+ (UIFont *)textInputFontDefault {
return _textInputFontDefault;
}
+ (void)setTextInputFontDefault:(UIFont *)textInputFontDefault {
_textInputFontDefault = textInputFontDefault;
}
- (UIColor *)textInputClearButtonTintColor {
if (_textInputClearButtonTintColor) {
return _textInputClearButtonTintColor;
}
return [self class].textInputClearButtonTintColorDefault ?: self.textInput.clearButton.tintColor;
}
- (void)setTextInputClearButtonTintColor:(UIColor *)textInputClearButtonTintColor {
_textInputClearButtonTintColor = textInputClearButtonTintColor;
_textInput.clearButton.tintColor = _textInputClearButtonTintColor;
}
+ (UIColor *)textInputClearButtonTintColorDefault {
return _textInputClearButtonTintColorDefault;
}
+ (void)setTextInputClearButtonTintColorDefault:(UIColor *)textInputClearButtonTintColorDefault {
_textInputClearButtonTintColorDefault = textInputClearButtonTintColorDefault;
}
- (UIFont *)trailingUnderlineLabelFont {
return _trailingUnderlineLabelFont ?: [self class].trailingUnderlineLabelFontDefault;
}
- (void)setTrailingUnderlineLabelFont:(UIFont *)trailingUnderlineLabelFont {
if (![_trailingUnderlineLabelFont isEqual:trailingUnderlineLabelFont]) {
_trailingUnderlineLabelFont = trailingUnderlineLabelFont;
[self updateLayout];
}
}
+ (UIFont *)trailingUnderlineLabelFontDefault {
return _trailingUnderlineLabelFontDefault ?: [[self class] underlineLabelsFont];
}
+ (void)setTrailingUnderlineLabelFontDefault:(UIFont *)trailingUnderlineLabelFontDefault {
_trailingUnderlineLabelFontDefault = trailingUnderlineLabelFontDefault;
}
- (UIColor *)trailingUnderlineLabelTextColor {
return _trailingUnderlineLabelTextColor ? _trailingUnderlineLabelTextColor
: [self class].trailingUnderlineLabelTextColorDefault;
}
- (void)setTrailingUnderlineLabelTextColor:(UIColor *)trailingUnderlineLabelTextColor {
if (_trailingUnderlineLabelTextColor != trailingUnderlineLabelTextColor) {
_trailingUnderlineLabelTextColor = trailingUnderlineLabelTextColor
? trailingUnderlineLabelTextColor
: [self class].trailingUnderlineLabelTextColorDefault;
[self updateTrailingUnderlineLabel];
}
}
+ (UIColor *)trailingUnderlineLabelTextColorDefault {
if (!_trailingUnderlineLabelTextColorDefault) {
_trailingUnderlineLabelTextColorDefault =
MDCTextInputControllerBaseDefaultInlinePlaceholderTextColorDefault();
}
return _trailingUnderlineLabelTextColorDefault;
}
+ (void)setTrailingUnderlineLabelTextColorDefault:
(UIColor *)trailingUnderlineLabelTextColorDefault {
_trailingUnderlineLabelTextColorDefault =
trailingUnderlineLabelTextColorDefault
? trailingUnderlineLabelTextColorDefault
: MDCTextInputControllerBaseDefaultInlinePlaceholderTextColorDefault();
}
- (void)setUnderlineHeightActive:(CGFloat)underlineHeightActive {
if (_underlineHeightActive != underlineHeightActive) {
_underlineHeightActive = underlineHeightActive;
[self updateLayout];
}
}
+ (CGFloat)underlineHeightActiveDefault {
return _underlineHeightActiveDefault;
}
+ (void)setUnderlineHeightActiveDefault:(CGFloat)underlineHeightActiveDefault {
_underlineHeightActiveDefault = underlineHeightActiveDefault;
}
- (void)setUnderlineHeightNormal:(CGFloat)underlineHeightNormal {
if (_underlineHeightNormal != underlineHeightNormal) {
_underlineHeightNormal = underlineHeightNormal;
[self updateLayout];
}
}
+ (CGFloat)underlineHeightNormalDefault {
return _underlineHeightNormalDefault;
}
+ (void)setUnderlineHeightNormalDefault:(CGFloat)underlineHeightNormalDefault {
_underlineHeightNormalDefault = underlineHeightNormalDefault;
}
- (void)setUnderlineViewMode:(UITextFieldViewMode)underlineViewMode {
if (_underlineViewMode != underlineViewMode) {
_underlineViewMode = underlineViewMode;
[self updateLayout];
}
}
+ (UITextFieldViewMode)underlineViewModeDefault {
return _underlineViewModeDefault;
}
+ (void)setUnderlineViewModeDefault:(UITextFieldViewMode)underlineViewModeDefault {
_underlineViewModeDefault = underlineViewModeDefault;
}
#pragma mark - Layout
- (void)updateLayout {
if (!_textInput) {
return;
}
[self updateCursor];
[self updatePlaceholder];
[self updateLeadingUnderlineLabel];
[self updateTrailingUnderlineLabel];
[self updateTextInput];
[self updateUnderline];
[self updateBorder];
}
#pragma mark - MDCTextFieldPositioningDelegate
// clang-format off
/**
textInsets: is the source of truth for vertical layout. It's used to figure out the proper
height and also where to place the placeholder / text field.
NOTE: It's applied before the textRect is flipped for RTL. So all calculations are done here à la
LTR.
The vertical layout is, at most complex, this form:
MDCTextInputControllerBaseDefaultPadding // Top padding
MDCRint(self.textInput.placeholderLabel.font.lineHeight * scale) // Placeholder when up
MDCTextInputControllerBaseDefaultPadding // Padding
MDCCeil(MAX(self.textInput.font.lineHeight, // Text field or placeholder
self.textInput.placeholderLabel.font.lineHeight))
MDCTextInputControllerBaseDefaultPadding // Padding to underline
--Underline-- // Underline (height not counted)
underlineLabelsOffset // Depends on text insets mode
*/
// clang-format on
- (UIEdgeInsets)textInsets:(UIEdgeInsets)defaultInsets {
// NOTE: UITextFields have a centerY based layout. And you can change EITHER the height or the Y.
// Not both. Don't know why. So, we have to leave the text rect as big as the bounds and move it
// to a Y that works. In other words, no bottom inset will make a difference here for UITextFields
UIEdgeInsets textInsets = defaultInsets;
if (!self.isFloatingEnabled) {
return textInsets;
}
textInsets.top = MDCTextInputControllerBaseDefaultPadding +
MDCRint(self.textInput.placeholderLabel.font.lineHeight *
(CGFloat)self.floatingPlaceholderScale.floatValue) +
MDCTextInputControllerBaseDefaultPadding;
CGFloat scale = UIScreen.mainScreen.scale;
CGFloat leadingOffset =
MDCCeil(self.textInput.leadingUnderlineLabel.font.lineHeight * scale) / scale;
CGFloat trailingOffset =
MDCCeil(self.textInput.trailingUnderlineLabel.font.lineHeight * scale) / scale;
// The amount of space underneath the underline is variable. It could just be
// MDCTextInputControllerBaseDefaultPadding or the biggest estimated underlineLabel height +
// MDCTextInputControllerBaseDefaultPadding. It's also dependent on the .textInsetsMode.
CGFloat underlineOffset = 0;
switch (self.textInput.textInsetsMode) {
case MDCTextInputTextInsetsModeAlways:
underlineOffset +=
MAX(leadingOffset, trailingOffset) + MDCTextInputControllerBaseDefaultPadding;
break;
case MDCTextInputTextInsetsModeIfContent: {
// contentConditionalOffset will have the estimated text height for the largest underline
// label that also has text.
CGFloat contentConditionalOffset = 0;
if (self.textInput.leadingUnderlineLabel.text.length) {
contentConditionalOffset = leadingOffset;
}
if (self.textInput.trailingUnderlineLabel.text.length) {
contentConditionalOffset = MAX(contentConditionalOffset, trailingOffset);
}
if (!MDCCGFloatEqual(contentConditionalOffset, 0)) {
underlineOffset += contentConditionalOffset + MDCTextInputControllerBaseDefaultPadding;
}
} break;
case MDCTextInputTextInsetsModeNever:
break;
}
// .bottom = underlineOffset + MDCTextInputControllerBaseDefaultPadding
// Legacy default has an additional padding here but this version does not.
textInsets.bottom = underlineOffset + MDCTextInputControllerBaseDefaultPadding;
return textInsets;
}
- (void)textInputDidLayoutSubviews {
[self updateBorder];
if ([self needsUpdatePlaceholderAnimationConstraintsToUp]) {
[self updatePlaceholderAnimationConstraints:YES];
}
}
- (void)textInputDidUpdateConstraints {
if (self.isFloatingEnabled && _textInput.text.length > 0) {
[self updatePlaceholderAnimationConstraints:YES];
}
}
#pragma mark - UITextField & UITextView Notification Observation
- (void)textInputDidBeginEditing:(__unused NSNotification *)note {
[CATransaction begin];
[CATransaction
setAnimationDuration:MDCTextInputControllerBaseDefaultFloatingPlaceholderUpAnimationDuration];
[CATransaction
setAnimationTimingFunction:[CAMediaTimingFunction
mdc_functionWithType:MDCAnimationTimingFunctionEaseInOut]];
[self updateLayout];
if (self.isFloatingEnabled && self.textInput.text.length == 0) {
[self movePlaceholderToUp:YES];
}
[CATransaction commit];
if (self.characterCountMax > 0) {
NSString *announcementString;
if (!announcementString.length) {
announcementString = [NSString
stringWithFormat:@"%lu character limit.", (unsigned long)self.characterCountMax];
}
// Simply sending a layout change notification does not seem to
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcementString);
}
}
- (void)textInputDidToggleEnabled:(NSNotification *)notification {
if (notification.object != self.textInput) {
return;
}
[self updateLayout];
}
- (void)textInputDidChange:(NSNotification *)note {
if ([note.name isEqualToString:MDCTextFieldTextDidSetTextNotification]) {
[CATransaction begin];
[CATransaction setAnimationDuration:0];
[self updateLayout];
[self forceUpdatePlaceholderY];
[CATransaction commit];
} else {
[self updateLayout];
}
// Accessibility
if (self.textInput.isEditing && self.characterCountMax > 0) {
NSString *announcementString;
if (!announcementString.length) {
announcementString = [NSString
stringWithFormat:@"%lu characters remaining",
(unsigned long)(self.characterCountMax -
[self.characterCounter
characterCountForTextInput:self.textInput])];
}
// Simply sending a layout change notification does not seem to
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcementString);
}
}
- (void)textInputDidEndEditing:(__unused NSNotification *)note {
[CATransaction begin];
[CATransaction setAnimationDuration:
MDCTextInputControllerBaseDefaultFloatingPlaceholderDownAnimationDuration];
[CATransaction
setAnimationTimingFunction:[CAMediaTimingFunction
mdc_functionWithType:MDCAnimationTimingFunctionEaseInOut]];
[self updateLayout];
if (self.isFloatingEnabled && self.textInput.text.length == 0) {
[self movePlaceholderToUp:NO];
}
[CATransaction commit];
}
#pragma mark - Public API
- (void)setErrorText:(NSString *)errorText
errorAccessibilityValue:(NSString *)errorAccessibilityValue {
// Turn on error:
//
// Here the 'magic' logic happens for error text.
// When the user sets error text, we save the current state of their underline, leading text,
// trailing text, and placeholder text for both content and color.
if (errorText && !self.isDisplayingErrorText) {
// If we are not in error, but will be, we need to save the existing state.
self.previousLeadingText = self.textInput.leadingUnderlineLabel.text
? self.textInput.leadingUnderlineLabel.text.copy
: @"";
self.textInput.leadingUnderlineLabel.text = errorText;
[self updatePlaceholder];
}
// Change error:
if (errorText && self.isDisplayingErrorText) {
self.textInput.leadingUnderlineLabel.text = errorText;
}
// Turn off error:
//
// If error text is unset (nil) we reset to previous values.
if (!errorText) {
// If there is a saved state, use it.
if (self.previousLeadingText) {
self.textInput.leadingUnderlineLabel.text = self.previousLeadingText;
}
// Clear out saved state.
self.previousLeadingText = nil;
}
self.errorText = errorText;
self.errorAccessibilityValue = errorAccessibilityValue;
[self updateLayout];
// Accessibility
// TODO: (larche) Localize
if (errorText) {
NSString *announcementString = errorAccessibilityValue;
if (!announcementString.length) {
announcementString =
errorText.length > 0 ? [NSString stringWithFormat:@"Error: %@", errorText] : @"Error.";
}
NSString *valueString = @"";
if (self.textInput.text.length > 0) {
valueString = [self.textInput.text copy];
}
if (self.textInput.placeholder.length > 0) {
valueString = [NSString stringWithFormat:@"%@. %@", valueString, self.textInput.placeholder];
}
valueString = [valueString stringByAppendingString:@"."];
self.textInput.accessibilityValue = valueString;
self.textInput.leadingUnderlineLabel.accessibilityLabel = announcementString;
} else {
self.textInput.accessibilityValue = nil;
if ([self.textInput.leadingUnderlineLabel.text isEqualToString:self.helperText]) {
self.textInput.leadingUnderlineLabel.accessibilityLabel = self.helperAccessibilityLabel;
} else {
self.textInput.leadingUnderlineLabel.accessibilityLabel = nil;
}
}
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification,
self.textInput.leadingUnderlineLabel);
}
-(void)setHelperText:(NSString *)helperText
helperAccessibilityLabel:(NSString *)helperAccessibilityLabel {
self.helperText = helperText;
self.helperAccessibilityLabel = helperAccessibilityLabel;
}
#pragma mark - Accessibility
- (BOOL)mdc_adjustsFontForContentSizeCategory {
return _mdc_adjustsFontForContentSizeCategory;
}
- (void)mdc_setAdjustsFontForContentSizeCategory:(BOOL)adjusts {
_mdc_adjustsFontForContentSizeCategory = adjusts;
[self updateLayout];
if (_mdc_adjustsFontForContentSizeCategory) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(contentSizeCategoryDidChange:)
name:UIContentSizeCategoryDidChangeNotification
object:nil];
} else {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIContentSizeCategoryDidChangeNotification
object:nil];
}
}
+ (BOOL)mdc_adjustsFontForContentSizeCategoryDefault {
return _mdc_adjustsFontForContentSizeCategoryDefault;
}
+ (void)setMdc_adjustsFontForContentSizeCategoryDefault:
(BOOL)mdc_adjustsFontForContentSizeCategoryDefault {
_mdc_adjustsFontForContentSizeCategoryDefault = mdc_adjustsFontForContentSizeCategoryDefault;
}
- (void)contentSizeCategoryDidChange:(__unused NSNotification *)notification {
[self updateLayout];
}
@end