blob: 577f4a89f374baf66d988fd5fa4330b1da0aa879 [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 "MDCTextInputControllerOutlined.h"
#import <MDFInternationalization/MDFInternationalization.h>
#import "MDCTextInput.h"
#import "MDCTextInputBorderView.h"
#import "MDCTextInputUnderlineView.h"
#import "private/MDCTextInputControllerBase+Subclassing.h"
#pragma mark - Class Properties
static const CGFloat MDCTextInputOutlinedTextFieldFloatingPlaceholderPadding = 8;
static const CGFloat MDCTextInputOutlinedTextFieldFullPadding = 16;
static const CGFloat MDCTextInputOutlinedTextFieldNormalPlaceholderPadding = 20;
static const CGFloat MDCTextInputOutlinedTextFieldThreeQuartersPadding = 12;
static UIRectCorner _roundedCornersDefault = UIRectCornerAllCorners;
@interface MDCTextInputControllerOutlined ()
@property(nonatomic, strong) NSLayoutConstraint *placeholderCenterY;
@property(nonatomic, strong) NSLayoutConstraint *placeholderLeading;
@end
@implementation MDCTextInputControllerOutlined
- (instancetype)initWithTextInput:(UIView<MDCTextInput> *)input {
NSAssert(![input conformsToProtocol:@protocol(MDCMultilineTextInput)],
@"This design is meant for single-line text fields only. For a complementary multi-line "
@"style, see MDCTextInputControllerOutlinedTextArea.");
self = [super initWithTextInput:input];
if (self) {
input.textInsetsMode = MDCTextInputTextInsetsModeAlways;
}
return self;
}
#pragma mark - Properties Implementations
- (BOOL)isFloatingEnabled {
return YES;
}
- (void)setFloatingEnabled:(__unused BOOL)floatingEnabled {
// Unused. Floating is always enabled.
}
- (UIOffset)floatingPlaceholderOffset {
UIOffset offset = [super floatingPlaceholderOffset];
CGFloat textVerticalOffset = 0;
offset.vertical = textVerticalOffset;
return offset;
}
+ (UIRectCorner)roundedCornersDefault {
return _roundedCornersDefault;
}
+ (void)setRoundedCornersDefault:(UIRectCorner)roundedCornersDefault {
_roundedCornersDefault = roundedCornersDefault;
}
#pragma mark - MDCTextInputPositioningDelegate
- (CGRect)leadingViewRectForBounds:(CGRect)bounds defaultRect:(CGRect)defaultRect {
CGRect leadingViewRect = defaultRect;
CGFloat xOffset = (self.textInput.mdf_effectiveUserInterfaceLayoutDirection ==
UIUserInterfaceLayoutDirectionRightToLeft)
? -1 * MDCTextInputOutlinedTextFieldFullPadding
: MDCTextInputOutlinedTextFieldFullPadding;
leadingViewRect = CGRectOffset(leadingViewRect, xOffset, 0);
CGRect borderRect = [self borderRect];
leadingViewRect.origin.y = CGRectGetMinY(borderRect) + CGRectGetHeight(borderRect) / 2 -
CGRectGetHeight(leadingViewRect) / 2;
return leadingViewRect;
}
- (CGFloat)leadingViewTrailingPaddingConstant {
return MDCTextInputOutlinedTextFieldFullPadding;
}
- (CGRect)trailingViewRectForBounds:(CGRect)bounds defaultRect:(CGRect)defaultRect {
CGRect trailingViewRect = defaultRect;
CGFloat xOffset = (self.textInput.mdf_effectiveUserInterfaceLayoutDirection ==
UIUserInterfaceLayoutDirectionRightToLeft)
? MDCTextInputOutlinedTextFieldThreeQuartersPadding
: -1 * MDCTextInputOutlinedTextFieldThreeQuartersPadding;
trailingViewRect = CGRectOffset(trailingViewRect, xOffset, 0);
CGRect borderRect = [self borderRect];
trailingViewRect.origin.y = CGRectGetMinY(borderRect) + CGRectGetHeight(borderRect) / 2 -
CGRectGetHeight(trailingViewRect) / 2;
return trailingViewRect;
}
- (CGFloat)trailingViewTrailingPaddingConstant {
return MDCTextInputOutlinedTextFieldThreeQuartersPadding;
}
// 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.
This one is a little different because the placeholder crosses the top bordered area when floating.
The vertical layout is, at most complex, this form:
placeholderEstimatedHeight // Height of placeholder
MDCTextInputOutlinedTextFieldFullPadding // Padding
ceil(MAX(self.textInput.font.lineHeight, // Text field or placeholder
self.textInput.placeholderLabel.font.lineHeight))
MDCTextInputControllerBaseDefaultPadding // Padding to bottom of border rect
underlineLabelsOffset // From super class.
*/
// clang-format on
- (UIEdgeInsets)textInsets:(UIEdgeInsets)defaultInsets
withSizeThatFitsWidthHint:(CGFloat)widthHint {
defaultInsets.left = MDCTextInputOutlinedTextFieldFullPadding;
defaultInsets.right = MDCTextInputOutlinedTextFieldFullPadding;
UIEdgeInsets textInsets = [super textInsets:defaultInsets withSizeThatFitsWidthHint:widthHint];
CGFloat textVerticalOffset = self.textInput.placeholderLabel.font.lineHeight * (CGFloat)0.5;
CGFloat scale = UIScreen.mainScreen.scale;
CGFloat placeholderEstimatedHeight =
ceil(self.textInput.placeholderLabel.font.lineHeight * scale) / scale;
textInsets.top = [self borderHeight] - MDCTextInputOutlinedTextFieldFullPadding -
placeholderEstimatedHeight + textVerticalOffset;
return textInsets;
}
#pragma mark - MDCTextInputControllerBase overrides
- (void)updateLayout {
[super updateLayout];
self.textInput.clipsToBounds = NO;
}
- (void)updateUnderline {
self.textInput.underline.hidden = YES;
}
- (void)updateBorder {
[super updateBorder];
UIBezierPath *path;
if ([self isPlaceholderUp]) {
CGFloat placeholderWidth = 0;
NSString *placeholderString = [self.textInput.placeholderLabel.text
stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
if (placeholderString.length > 0) {
placeholderWidth = [self.textInput.placeholderLabel
systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]
.width *
(CGFloat)self.floatingPlaceholderScale.floatValue;
placeholderWidth += MDCTextInputOutlinedTextFieldFloatingPlaceholderPadding;
}
path = [self roundedPathFromRect:[self borderRect]
withTextSpace:placeholderWidth
leadingOffset:MDCTextInputOutlinedTextFieldFullPadding -
MDCTextInputOutlinedTextFieldFloatingPlaceholderPadding / 2];
} else {
CGSize cornerRadius = CGSizeMake(self.borderRadius, self.borderRadius);
path = [UIBezierPath bezierPathWithRoundedRect:[self borderRect]
byRoundingCorners:self.roundedCorners
cornerRadii:cornerRadius];
}
self.textInput.borderPath = path;
UIColor *borderColor =
self.textInput.isEditing ? self.activeColor : (self.borderStrokeColor ?: self.normalColor);
if (!self.textInput.isEnabled) {
borderColor = self.disabledColor;
}
self.textInput.borderView.borderStrokeColor =
(self.isDisplayingCharacterCountError || self.isDisplayingErrorText) ? self.errorColor
: borderColor;
self.textInput.borderView.borderPath.lineWidth = self.textInput.isEditing ? 2 : 1;
[self.textInput.borderView setNeedsLayout];
[self updatePlaceholder];
}
- (CGRect)borderRect {
CGRect pathRect = self.textInput.bounds;
pathRect.origin.y =
pathRect.origin.y + self.textInput.placeholderLabel.font.lineHeight * (CGFloat)0.5;
pathRect.size.height = [self borderHeight];
return pathRect;
}
- (UIBezierPath *)roundedPathFromRect:(CGRect)f
withTextSpace:(CGFloat)textSpace
leadingOffset:(CGFloat)offset {
UIBezierPath *path = [[UIBezierPath alloc] init];
CGFloat radius = self.borderRadius;
CGFloat yOffset = f.origin.y;
CGFloat xOffset = f.origin.x;
// Draw the path
[path moveToPoint:CGPointMake(radius + xOffset, yOffset)];
if (self.textInput.mdf_effectiveUserInterfaceLayoutDirection ==
UIUserInterfaceLayoutDirectionLeftToRight) {
[path addLineToPoint:CGPointMake(offset + xOffset, yOffset)];
[path moveToPoint:CGPointMake(textSpace + offset + xOffset, yOffset)];
[path addLineToPoint:CGPointMake(f.size.width - radius + xOffset, yOffset)];
} else {
[path addLineToPoint:CGPointMake(xOffset + (f.size.width - (offset + textSpace)), yOffset)];
[path moveToPoint:CGPointMake(xOffset + (f.size.width - offset), yOffset)];
[path addLineToPoint:CGPointMake(xOffset + (f.size.width - radius), yOffset)];
}
[path addArcWithCenter:CGPointMake(f.size.width - radius + xOffset, radius + yOffset)
radius:radius
startAngle:-(CGFloat)(M_PI / 2)
endAngle:0
clockwise:YES];
[path addLineToPoint:CGPointMake(f.size.width + xOffset, f.size.height - radius + yOffset)];
[path addArcWithCenter:CGPointMake(f.size.width - radius + xOffset,
f.size.height - radius + yOffset)
radius:radius
startAngle:0
endAngle:-(CGFloat)((M_PI * 3) / 2)
clockwise:YES];
[path addLineToPoint:CGPointMake(radius + xOffset, f.size.height + yOffset)];
[path addArcWithCenter:CGPointMake(radius + xOffset, f.size.height - radius + yOffset)
radius:radius
startAngle:-(CGFloat)((M_PI * 3) / 2)
endAngle:-(CGFloat)M_PI
clockwise:YES];
[path addLineToPoint:CGPointMake(xOffset, radius + yOffset)];
[path addArcWithCenter:CGPointMake(radius + xOffset, radius + yOffset)
radius:radius
startAngle:-(CGFloat)M_PI
endAngle:-(CGFloat)(M_PI / 2)
clockwise:YES];
return path;
}
- (void)updatePlaceholder {
[super updatePlaceholder];
CGFloat scale = UIScreen.mainScreen.scale;
CGFloat placeholderEstimatedHeight =
ceil(self.textInput.placeholderLabel.font.lineHeight * scale) / scale;
CGFloat placeholderConstant = ([self borderHeight] / 2) - (placeholderEstimatedHeight / 2) +
self.textInput.placeholderLabel.font.lineHeight * (CGFloat)0.5;
if (!self.placeholderCenterY) {
self.placeholderCenterY = [NSLayoutConstraint constraintWithItem:self.textInput.placeholderLabel
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.textInput
attribute:NSLayoutAttributeTop
multiplier:1
constant:placeholderConstant];
self.placeholderCenterY.priority = UILayoutPriorityDefaultHigh;
self.placeholderCenterY.active = YES;
[self.textInput.placeholderLabel setContentHuggingPriority:UILayoutPriorityDefaultHigh + 1
forAxis:UILayoutConstraintAxisVertical];
}
self.placeholderCenterY.constant = placeholderConstant;
CGFloat placeholderLeadingConstant = MDCTextInputOutlinedTextFieldFullPadding;
if ([self.textInput conformsToProtocol:@protocol(MDCLeadingViewTextInput)]) {
UIView<MDCLeadingViewTextInput> *leadingViewInput =
(UIView<MDCLeadingViewTextInput> *)self.textInput;
if (leadingViewInput.leadingView.superview) {
placeholderLeadingConstant += CGRectGetWidth(leadingViewInput.leadingView.frame) +
[self leadingViewTrailingPaddingConstant];
}
}
if (!self.placeholderLeading) {
self.placeholderLeading = [NSLayoutConstraint constraintWithItem:self.textInput.placeholderLabel
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self.textInput
attribute:NSLayoutAttributeLeading
multiplier:1
constant:placeholderLeadingConstant];
self.placeholderLeading.priority = UILayoutPriorityDefaultHigh;
self.placeholderLeading.active = YES;
}
self.placeholderLeading.constant = placeholderLeadingConstant;
}
- (CGFloat)borderHeight {
CGFloat scale = UIScreen.mainScreen.scale;
CGFloat placeholderEstimatedHeight =
ceil(self.textInput.placeholderLabel.font.lineHeight * scale) / scale;
return MDCTextInputOutlinedTextFieldNormalPlaceholderPadding + placeholderEstimatedHeight +
MDCTextInputOutlinedTextFieldNormalPlaceholderPadding;
}
@end