blob: a630319ebca882f2e0305619960f1a1943abf622 [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 "MDCTextInputControllerFilled.h"
#import <MDFInternationalization/MDFInternationalization.h>
#import "MDCMultilineTextField.h"
#import "MDCTextInput.h"
#import "MDCTextInputBorderView.h"
#import "MDCTextInputController.h"
#import "MDCTextInputControllerBase.h"
#import "MDCTextInputControllerFloatingPlaceholder.h"
#import "private/MDCTextInputArt.h"
#import "private/MDCTextInputControllerBase+Subclassing.h"
#import "MaterialMath.h"
/**
Note: Right now this is a subclass of MDCTextInputControllerBase since they share a vast
majority of code. If the designs diverge further, this would make a good candidate for its own
class.
*/
#pragma mark - Constants
static const CGFloat MDCTextInputControllerFilledClearButtonPaddingAddition = -2;
static const CGFloat MDCTextInputControllerFilledDefaultUnderlineActiveHeight = 2;
static const CGFloat MDCTextInputControllerFilledDefaultUnderlineNormalHeight = 1;
static const CGFloat MDCTextInputControllerFilledFullPadding = 16;
// The guidelines have 8 points of padding but since the fonts on iOS are slightly smaller, we need
// to add points to keep the versions at the same height.
static const CGFloat MDCTextInputControllerFilledHalfPadding = 8;
static const CGFloat MDCTextInputControllerFilledHalfPaddingAddition = 1;
static const CGFloat MDCTextInputControllerFilledNormalPlaceholderPadding = 20;
static const CGFloat MDCTextInputControllerFilledThreeQuartersPadding = 12;
static inline UIColor *MDCTextInputControllerFilledDefaultBorderFillColorDefault() {
return [UIColor colorWithWhite:0 alpha:(CGFloat)0.06];
}
#pragma mark - Class Properties
static UIColor *_borderFillColorDefault;
static UIRectCorner _roundedCornersDefault = UIRectCornerAllCorners;
static CGFloat _underlineHeightActiveDefault =
MDCTextInputControllerFilledDefaultUnderlineActiveHeight;
static CGFloat _underlineHeightNormalDefault =
MDCTextInputControllerFilledDefaultUnderlineNormalHeight;
@interface MDCTextInputControllerFilled ()
@property(nonatomic, strong) NSLayoutConstraint *clearButtonBottom;
@property(nonatomic, strong) NSLayoutConstraint *placeholderTop;
@property(nonatomic, strong) NSLayoutConstraint *underlineBottom;
@end
@implementation MDCTextInputControllerFilled
#pragma mark - Properties Implementations
+ (UIColor *)borderFillColorDefault {
if (!_borderFillColorDefault) {
_borderFillColorDefault = MDCTextInputControllerFilledDefaultBorderFillColorDefault();
}
return _borderFillColorDefault;
}
+ (UIRectCorner)roundedCornersDefault {
return _roundedCornersDefault;
}
+ (void)setRoundedCornersDefault:(UIRectCorner)roundedCornersDefault {
_roundedCornersDefault = roundedCornersDefault;
}
+ (CGFloat)underlineHeightActiveDefault {
return _underlineHeightActiveDefault;
}
+ (void)setUnderlineHeightActiveDefault:(CGFloat)underlineHeightActiveDefault {
_underlineHeightActiveDefault = underlineHeightActiveDefault;
}
+ (CGFloat)underlineHeightNormalDefault {
return _underlineHeightNormalDefault;
}
+ (void)setUnderlineHeightNormalDefault:(CGFloat)underlineHeightNormalDefault {
_underlineHeightNormalDefault = underlineHeightNormalDefault;
}
#pragma mark - MDCTextInputPositioningDelegate
- (CGRect)leadingViewRectForBounds:(CGRect)bounds defaultRect:(CGRect)defaultRect {
CGRect leadingViewRect = defaultRect;
CGFloat xOffset = (self.textInput.mdf_effectiveUserInterfaceLayoutDirection ==
UIUserInterfaceLayoutDirectionRightToLeft)
? -1 * MDCTextInputControllerFilledFullPadding
: MDCTextInputControllerFilledFullPadding;
leadingViewRect = CGRectOffset(leadingViewRect, xOffset, 0);
leadingViewRect.origin.y =
CGRectGetHeight(self.textInput.borderPath.bounds) / 2 - CGRectGetHeight(leadingViewRect) / 2;
return leadingViewRect;
}
- (CGFloat)leadingViewTrailingPaddingConstant {
return MDCTextInputControllerFilledFullPadding;
}
- (CGRect)trailingViewRectForBounds:(CGRect)bounds defaultRect:(CGRect)defaultRect {
CGRect trailingViewRect = defaultRect;
CGFloat xOffset = (self.textInput.mdf_effectiveUserInterfaceLayoutDirection ==
UIUserInterfaceLayoutDirectionRightToLeft)
? MDCTextInputControllerFilledThreeQuartersPadding
: -1 * MDCTextInputControllerFilledThreeQuartersPadding;
trailingViewRect = CGRectOffset(trailingViewRect, xOffset, 0);
trailingViewRect.origin.y =
CGRectGetHeight(self.textInput.borderPath.bounds) / 2 - CGRectGetHeight(trailingViewRect) / 2;
return trailingViewRect;
}
- (CGFloat)trailingViewTrailingPaddingConstant {
return MDCTextInputControllerFilledThreeQuartersPadding;
}
// 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 (floating), this form:
MDCTextInputControllerFilledHalfPadding + // Small padding
MDCTextInputControllerFilledHalfPaddingAddition // Additional point (iOS specific)
MDCRint(self.textInput.placeholderLabel.font.lineHeight * scale) // Placeholder when up
MDCTextInputControllerFilledHalfPadding + // Small padding
MDCTextInputControllerFilledHalfPaddingAddition // Additional point (iOS specific)
MDCCeil(MAX(self.textInput.font.lineHeight, // Text field or placeholder line height
self.textInput.placeholderLabel.font.lineHeight))
MDCTextInputControllerFilledHalfPadding + // Small padding
MDCTextInputControllerFilledHalfPaddingAddition // Additional point (iOS specific)
--Underline-- // Underline (height not counted)
underlineLabelsOffset // Depends on text insets mode. See the super class.
*/
// clang-format on
- (UIEdgeInsets)textInsets:(UIEdgeInsets)defaultInsets {
UIEdgeInsets textInsets = [super textInsets:defaultInsets];
if (self.isFloatingEnabled) {
textInsets.top =
MDCTextInputControllerFilledHalfPadding + MDCTextInputControllerFilledHalfPaddingAddition +
MDCRint(self.textInput.placeholderLabel.font.lineHeight *
(CGFloat)self.floatingPlaceholderScale.floatValue) +
MDCTextInputControllerFilledHalfPadding + MDCTextInputControllerFilledHalfPaddingAddition;
} else {
textInsets.top = MDCTextInputControllerFilledNormalPlaceholderPadding;
}
textInsets.bottom = [self beneathInputPadding] + [self underlineOffset];
textInsets.left = MDCTextInputControllerFilledFullPadding;
textInsets.right = MDCTextInputControllerFilledHalfPadding;
return textInsets;
}
- (void)updateLayout {
[super updateLayout];
if (!self.textInput) {
return;
}
CGFloat clearButtonConstant =
-1 * ([self beneathInputPadding] - MDCTextInputClearButtonImageBuiltInPadding +
MDCTextInputControllerFilledClearButtonPaddingAddition);
if (!self.clearButtonBottom) {
self.clearButtonBottom = [NSLayoutConstraint constraintWithItem:self.textInput.clearButton
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.textInput.underline
attribute:NSLayoutAttributeTop
multiplier:1
constant:clearButtonConstant];
self.clearButtonBottom.active = YES;
}
self.clearButtonBottom.constant = clearButtonConstant;
}
#pragma mark - Layout
- (void)updatePlaceholder {
[super updatePlaceholder];
if (!self.placeholderTop) {
self.placeholderTop = [NSLayoutConstraint
constraintWithItem:self.textInput.placeholderLabel
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.textInput
attribute:NSLayoutAttributeTop
multiplier:1
constant:MDCTextInputControllerFilledNormalPlaceholderPadding];
self.placeholderTop.priority = UILayoutPriorityDefaultHigh;
self.placeholderTop.active = YES;
}
UIEdgeInsets textInsets = [self textInsets:UIEdgeInsetsZero];
CGFloat underlineBottomConstant =
textInsets.top + [self estimatedTextHeight] + [self beneathInputPadding];
// When floating placeholders are turned off, the underline will drift up unless this is set. Even
// tho it is redundant when floating is on, we just keep it on always for simplicity.
// Note: This is an issue only on single-line text fields.
if (!self.underlineBottom) {
if ([self.textInput isKindOfClass:[MDCMultilineTextField class]]) {
self.underlineBottom =
[NSLayoutConstraint constraintWithItem:self.textInput.underline
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:((MDCMultilineTextField *)self.textInput).textView
attribute:NSLayoutAttributeBottom
multiplier:1
constant:[self beneathInputPadding]];
self.underlineBottom.active = YES;
} else {
self.underlineBottom = [NSLayoutConstraint constraintWithItem:self.textInput.underline
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.textInput
attribute:NSLayoutAttributeTop
multiplier:1
constant:underlineBottomConstant];
self.underlineBottom.active = YES;
}
}
if ([self.textInput isKindOfClass:[MDCMultilineTextField class]]) {
self.underlineBottom.constant = [self beneathInputPadding];
} else {
self.underlineBottom.constant = underlineBottomConstant;
}
}
// The measurement from bottom to underline bottom. Only used in non-floating case.
- (CGFloat)underlineOffset {
// The amount of space underneath the underline may depend on whether there is content in the
// underline labels.
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;
CGFloat underlineOffset = 0;
switch (self.textInput.textInsetsMode) {
case MDCTextInputTextInsetsModeAlways:
underlineOffset +=
MAX(leadingOffset, trailingOffset) + MDCTextInputControllerFilledHalfPadding;
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 + MDCTextInputControllerFilledHalfPadding;
}
} break;
case MDCTextInputTextInsetsModeNever:
break;
}
return underlineOffset;
}
- (CGFloat)estimatedTextHeight {
CGFloat scale = UIScreen.mainScreen.scale;
CGFloat estimatedTextHeight = MDCCeil(self.textInput.font.lineHeight * scale) / scale;
return estimatedTextHeight;
}
- (UIOffset)floatingPlaceholderOffset {
UIOffset offset = [super floatingPlaceholderOffset];
if ([self.textInput conformsToProtocol:@protocol(MDCLeadingViewTextInput)]) {
UIView<MDCLeadingViewTextInput> *input = (UIView<MDCLeadingViewTextInput> *)self.textInput;
if (input.leadingView.superview) {
offset.horizontal -=
CGRectGetWidth(input.leadingView.frame) + [self leadingViewTrailingPaddingConstant];
}
}
return offset;
}
// The space ABOVE the underline but under the text input area.
- (CGFloat)beneathInputPadding {
if (self.isFloatingEnabled) {
return MDCTextInputControllerFilledHalfPadding +
MDCTextInputControllerFilledHalfPaddingAddition;
} else {
return MDCTextInputControllerFilledNormalPlaceholderPadding;
}
}
@end