blob: 354a04f2e61d7020eb341f52f4c9dc59417703f4 [file]
/*
Copyright 2018-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 "MDCAlertControllerView.h"
#import "MDCAlertControllerView+Private.h"
#import <MDFInternationalization/MDFInternationalization.h>
#import "MaterialButtons.h"
#import "MaterialTypography.h"
// https://material.io/go/design-dialogs#dialogs-specs
static const MDCFontTextStyle kTitleTextStyle = MDCFontTextStyleTitle;
static const MDCFontTextStyle kMessageTextStyle = MDCFontTextStyleBody1;
static const MDCFontTextStyle kButtonTextStyle = MDCFontTextStyleButton;
static const UIEdgeInsets MDCDialogContentInsets = {24.0, 24.0, 24.0, 24.0};
static const CGFloat MDCDialogContentVerticalPadding = 20.0;
static const UIEdgeInsets MDCDialogActionsInsets = {8.0, 8.0, 8.0, 8.0};
static const CGFloat MDCDialogActionsHorizontalPadding = 8.0;
static const CGFloat MDCDialogActionsVerticalPadding = 8.0;
static const CGFloat MDCDialogActionButtonHeight = 36.0;
static const CGFloat MDCDialogActionButtonMinimumWidth = 48.0;
static const CGFloat MDCDialogMessageOpacity = 0.54f;
@interface MDCAlertControllerView ()
@property(nonatomic, nonnull, strong) UIScrollView *contentScrollView;
@property(nonatomic, nonnull, strong) UIScrollView *actionsScrollView;
@property(nonatomic, getter=isVerticalActionsLayout) BOOL verticalActionsLayout;
@end
@implementation MDCAlertControllerView {
NSMutableArray<MDCFlatButton *> *_actionButtons;
BOOL _mdc_adjustsFontForContentSizeCategory;
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor whiteColor];
self.autoresizesSubviews = NO;
self.clipsToBounds = YES;
self.contentScrollView = [[UIScrollView alloc] initWithFrame:CGRectZero];
self.contentScrollView.backgroundColor = [UIColor whiteColor];
[self addSubview:self.contentScrollView];
self.actionsScrollView = [[UIScrollView alloc] initWithFrame:CGRectZero];
self.actionsScrollView.backgroundColor = [UIColor whiteColor];
[self addSubview:self.actionsScrollView];
self.titleLabel = [[UILabel alloc] initWithFrame:CGRectZero];
self.titleLabel.numberOfLines = 0;
self.titleLabel.textAlignment = NSTextAlignmentNatural;
if (self.mdc_adjustsFontForContentSizeCategory) {
self.titleLabel.font = [UIFont mdc_preferredFontForMaterialTextStyle:MDCFontTextStyleTitle];
} else {
self.titleLabel.font = [MDCTypography titleFont];
}
[self.contentScrollView addSubview:self.titleLabel];
self.messageLabel = [[UILabel alloc] initWithFrame:CGRectZero];
self.messageLabel.numberOfLines = 0;
self.messageLabel.textAlignment = NSTextAlignmentNatural;
if (self.mdc_adjustsFontForContentSizeCategory) {
self.messageLabel.font = [UIFont mdc_preferredFontForMaterialTextStyle:MDCFontTextStyleBody1];
} else {
self.messageLabel.font = [MDCTypography body1Font];
}
self.messageLabel.textColor = [UIColor colorWithWhite:0.0f alpha:MDCDialogMessageOpacity];
[self.contentScrollView addSubview:self.messageLabel];
_actionButtons = [[NSMutableArray alloc] init];
[self setNeedsLayout];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (NSArray<UIButton *>*)actionButtons{
return (NSArray<UIButton *>*)_actionButtons;
}
- (NSString *)title {
return self.titleLabel.text;
}
- (void)setTitle:(NSString *)title {
self.titleLabel.text = title;
[self setNeedsLayout];
}
- (void)addActionButtonTitle:(NSString *)actionTitle target:(id)target selector:(SEL)selector {
MDCFlatButton *actionButton = [[MDCFlatButton alloc] initWithFrame:CGRectZero];
actionButton.mdc_adjustsFontForContentSizeCategory = self.mdc_adjustsFontForContentSizeCategory;
[actionButton setTitle:actionTitle forState:UIControlStateNormal];
if (_buttonColor) {
// We only set if _buttonColor since settingTitleColor to nil doesn't reset the title to the
// default
[actionButton setTitleColor:_buttonColor forState:UIControlStateNormal];
}
[actionButton setTitleFont:_buttonFont forState:UIControlStateNormal];
// TODO(#1726): Determine default text color values for Normal and Disabled
CGRect buttonRect = actionButton.bounds;
buttonRect.size.height = MAX(buttonRect.size.height, MDCDialogActionButtonHeight);
buttonRect.size.width = MAX(buttonRect.size.width, MDCDialogActionButtonMinimumWidth);
actionButton.frame = buttonRect;
[actionButton addTarget:target
action:selector
forControlEvents:UIControlEventTouchUpInside];
[self.actionsScrollView addSubview:actionButton];
[_actionButtons addObject:actionButton];
}
- (void)setTitleFont:(UIFont *)font {
_titleFont = font;
[self updateTitleFont];
}
- (void)updateTitleFont {
UIFont *titleFont = _titleFont ?: [[self class] titleFontDefault];
if (_mdc_adjustsFontForContentSizeCategory) {
_titleLabel.font =
[titleFont mdc_fontSizedForMaterialTextStyle:kTitleTextStyle
scaledForDynamicType:_mdc_adjustsFontForContentSizeCategory];
} else {
_titleLabel.font = titleFont;
}
[self setNeedsLayout];
}
+ (UIFont *)titleFontDefault {
if ([MDCTypography.fontLoader isKindOfClass:[MDCSystemFontLoader class]]) {
return [UIFont mdc_standardFontForMaterialTextStyle:kTitleTextStyle];
}
return [MDCTypography titleFont];
}
- (void)setTitleColor:(UIColor *)titleColor {
_titleColor = titleColor;
_titleLabel.textColor = titleColor;
}
- (NSString *)message {
return self.messageLabel.text;
}
- (void)setMessage:(NSString *)message {
self.messageLabel.text = message;
[self setNeedsLayout];
}
- (void)setMessageFont:(UIFont *)messageFont {
_messageFont = messageFont;
[self updateMessageFont];
}
- (void)updateMessageFont {
UIFont *messageFont = _messageFont ?: [[self class] messageFontDefault];
if (_mdc_adjustsFontForContentSizeCategory) {
_messageLabel.font =
[messageFont mdc_fontSizedForMaterialTextStyle:kMessageTextStyle
scaledForDynamicType:_mdc_adjustsFontForContentSizeCategory];
} else {
_messageLabel.font = messageFont;
}
[self setNeedsLayout];
}
+ (UIFont *)messageFontDefault {
if ([MDCTypography.fontLoader isKindOfClass:[MDCSystemFontLoader class]]) {
return [UIFont mdc_standardFontForMaterialTextStyle:kMessageTextStyle];
}
return [MDCTypography body1Font];
}
- (void)setMessageColor:(UIColor *)messageColor {
_messageColor = messageColor;
_messageLabel.textColor = messageColor;
}
- (void)setButtonFont:(UIFont *)font {
_buttonFont = font;
[self updateButtonFont];
}
- (void)updateButtonFont {
UIFont *finalButtonFont = _buttonFont ?: [[self class] buttonFontDefault];
if (_mdc_adjustsFontForContentSizeCategory) {
finalButtonFont =
[finalButtonFont mdc_fontSizedForMaterialTextStyle:kTitleTextStyle
scaledForDynamicType:_mdc_adjustsFontForContentSizeCategory];
}
for (MDCFlatButton *button in self.actionButtons) {
[button setTitleFont:finalButtonFont forState:UIControlStateNormal];
}
[self setNeedsLayout];
}
+ (UIFont *)buttonFontDefault {
if ([MDCTypography.fontLoader isKindOfClass:[MDCSystemFontLoader class]]) {
return [UIFont mdc_standardFontForMaterialTextStyle:kButtonTextStyle];
}
return [MDCTypography titleFont];
}
- (void)setButtonColor:(UIColor *)color {
_buttonColor = color;
for (MDCFlatButton *button in self.actionButtons) {
[button setTitleColor:_buttonColor forState:UIControlStateNormal];
}
}
#pragma mark - Internal
- (CGSize)actionButtonsSizeInHorizontalLayout {
CGSize size = CGSizeZero;
if (0 < [self.actionButtons count]) {
size.height =
MDCDialogActionsInsets.top + MDCDialogActionButtonHeight + MDCDialogActionsInsets.bottom;
size.width = MDCDialogActionsInsets.left + MDCDialogActionsInsets.right;
for (UIButton *button in self.actionButtons) {
CGSize buttonSize = [button sizeThatFits:size];
size.width += buttonSize.width;
if (button != [self.actionButtons lastObject]) {
size.width += MDCDialogActionsHorizontalPadding;
}
}
}
return size;
}
- (CGSize)actionButtonsSizeInVericalLayout {
CGSize size = CGSizeZero;
if (0 < [self.actionButtons count]) {
size.height = MDCDialogActionsInsets.top + MDCDialogActionsInsets.bottom;
size.width = MDCDialogActionsInsets.left + MDCDialogActionsInsets.right;
for (UIButton *button in self.actionButtons) {
CGSize buttonSize = [button sizeThatFits:size];
size.height += buttonSize.height;
size.width = MAX(size.width, buttonSize.width);
if (button != [self.actionButtons lastObject]) {
size.height += MDCDialogActionsVerticalPadding;
}
}
}
return size;
}
// @param boundsSize should not include any internal margins or padding
- (CGSize)calculatePreferredContentSizeForBounds:(CGSize)boundsSize {
// Content & Actions
CGSize contentSize = [self calculateContentSizeThatFitsWidth:boundsSize.width];
CGSize actionSize = [self calculateActionsSizeThatFitsWidth:boundsSize.width];
// Final Sizing
CGSize totalSize;
totalSize.width = MAX(contentSize.width, actionSize.width);
totalSize.height = contentSize.height + actionSize.height;
return totalSize;
}
// @param boundingWidth should not include any internal margins or padding
- (CGSize)calculateContentSizeThatFitsWidth:(CGFloat)boundingWidth {
CGSize boundsSize = CGRectInfinite.size;
boundsSize.width = boundingWidth - MDCDialogContentInsets.left - MDCDialogContentInsets.right;
CGSize titleSize = [self.titleLabel sizeThatFits:boundsSize];
CGSize messageSize = [self.messageLabel sizeThatFits:boundsSize];
CGFloat contentWidth = MAX(titleSize.width, messageSize.width);
contentWidth += MDCDialogContentInsets.left + MDCDialogContentInsets.right;
CGFloat contentInternalVerticalPadding = 0.0;
if (0.0 < titleSize.height && 0.0 < messageSize.height) {
contentInternalVerticalPadding = MDCDialogContentVerticalPadding;
}
CGFloat contentHeight = titleSize.height + contentInternalVerticalPadding + messageSize.height;
contentHeight += MDCDialogContentInsets.top + MDCDialogContentInsets.bottom;
CGSize contentSize;
contentSize.width = (CGFloat)ceil(contentWidth);
contentSize.height = (CGFloat)ceil(contentHeight);
return contentSize;
}
// @param boundingWidth should not include any internal margins or padding
- (CGSize)calculateActionsSizeThatFitsWidth:(CGFloat)boundingWidth {
CGSize boundsSize = CGRectInfinite.size;
boundsSize.width = boundingWidth;
CGSize horizontalSize = [self actionButtonsSizeInHorizontalLayout];
CGSize verticalSize = [self actionButtonsSizeInVericalLayout];
CGSize actionsSize;
if (boundsSize.width < horizontalSize.width) {
// Use VerticalLayout
actionsSize.width = MIN(verticalSize.width, boundsSize.width);
actionsSize.height = MIN(verticalSize.height, boundsSize.height);
} else {
// Use HorizontalLayout
actionsSize.width = MIN(horizontalSize.width, boundsSize.width);
actionsSize.height = MIN(horizontalSize.height, boundsSize.height);
}
actionsSize.width = (CGFloat)ceil(actionsSize.width);
actionsSize.height = (CGFloat)ceil(actionsSize.height);
return actionsSize;
}
- (void)layoutSubviews {
[super layoutSubviews];
for (UIButton *button in self.actionButtons) {
[button sizeToFit];
}
// Used to calculate the height of the scrolling content, so we limit the width.
CGSize boundsSize = CGRectInfinite.size;
boundsSize.width = CGRectGetWidth(self.bounds);
// Content
CGSize contentSize = [self calculateContentSizeThatFitsWidth:boundsSize.width];
CGRect contentRect = CGRectZero;
contentRect.size.width = CGRectGetWidth(self.bounds);
contentRect.size.height = contentSize.height;
self.contentScrollView.contentSize = contentRect.size;
// Place Content in contentScrollView
boundsSize.width = boundsSize.width - MDCDialogContentInsets.left - MDCDialogContentInsets.right;
CGSize titleSize = [self.titleLabel sizeThatFits:boundsSize];
titleSize.width = boundsSize.width;
CGSize messageSize = [self.messageLabel sizeThatFits:boundsSize];
messageSize.width = boundsSize.width;
boundsSize.width = boundsSize.width + MDCDialogContentInsets.left + MDCDialogContentInsets.right;
CGFloat contentInternalVerticalPadding = 0.0;
if (0.0 < titleSize.height && 0.0 < messageSize.height) {
contentInternalVerticalPadding = MDCDialogContentVerticalPadding;
}
CGRect titleFrame = CGRectMake(MDCDialogContentInsets.left, MDCDialogContentInsets.top,
titleSize.width, titleSize.height);
CGRect messageFrame = CGRectMake(MDCDialogContentInsets.left,
CGRectGetMaxY(titleFrame) + contentInternalVerticalPadding,
messageSize.width, messageSize.height);
self.titleLabel.frame = titleFrame;
self.messageLabel.frame = messageFrame;
// Actions
CGSize actionSize = [self calculateActionsSizeThatFitsWidth:boundsSize.width];
const CGFloat horizontalActionHeight =
MDCDialogActionsInsets.top + MDCDialogActionButtonHeight + MDCDialogActionsInsets.bottom;
if (horizontalActionHeight < actionSize.height) {
self.verticalActionsLayout = YES;
} else {
self.verticalActionsLayout = NO;
}
CGRect actionsFrame = CGRectZero;
actionsFrame.size.width = CGRectGetWidth(self.bounds);
if (0 < [self.actionButtons count]) {
actionsFrame.size.height = actionSize.height;
}
self.actionsScrollView.contentSize = actionsFrame.size;
// Place buttons in actionsScrollView
if (self.isVerticalActionsLayout) {
CGPoint buttonCenter;
buttonCenter.x = self.actionsScrollView.contentSize.width * 0.5f;
buttonCenter.y = self.actionsScrollView.contentSize.height - MDCDialogActionsInsets.bottom;
for (UIButton *button in self.actionButtons) {
CGRect buttonRect = button.frame;
buttonCenter.y -= buttonRect.size.height * 0.5;
button.center = buttonCenter;
if (button != [self.actionButtons lastObject]) {
buttonCenter.y -= buttonRect.size.height * 0.5;
buttonCenter.y -= MDCDialogActionsVerticalPadding;
}
}
} else {
CGPoint buttonOrigin = CGPointZero;
buttonOrigin.x = self.actionsScrollView.contentSize.width - MDCDialogActionsInsets.right;
buttonOrigin.y = MDCDialogActionsInsets.top;
for (UIButton *button in self.actionButtons) {
CGRect buttonRect = button.frame;
buttonOrigin.x -= buttonRect.size.width;
buttonRect.origin = buttonOrigin;
button.frame = buttonRect;
if (button != [self.actionButtons lastObject]) {
buttonOrigin.x -= MDCDialogActionsHorizontalPadding;
}
}
// Handle RTL
if (self.mdf_effectiveUserInterfaceLayoutDirection ==
UIUserInterfaceLayoutDirectionRightToLeft) {
for (UIButton *button in self.actionButtons) {
CGRect flippedRect =
MDFRectFlippedHorizontally(button.frame, CGRectGetWidth(self.bounds));
button.frame = flippedRect;
}
}
}
// Place scrollviews
CGRect contentScrollViewRect = CGRectZero;
contentScrollViewRect.size = self.contentScrollView.contentSize;
CGRect actionsScrollViewRect = CGRectZero;
actionsScrollViewRect.size = self.actionsScrollView.contentSize;
const CGFloat requestedHeight =
self.contentScrollView.contentSize.height + self.actionsScrollView.contentSize.height;
if (requestedHeight <= CGRectGetHeight(self.bounds)) {
// Simple layout case : both content and actions fit on the screen at once
self.contentScrollView.frame = contentScrollViewRect;
actionsScrollViewRect.origin.y =
CGRectGetHeight(self.bounds) - actionsScrollViewRect.size.height;
self.actionsScrollView.frame = actionsScrollViewRect;
} else {
// Complex layout case : Split the space between the two scrollviews
if (CGRectGetHeight(contentScrollViewRect) < CGRectGetHeight(self.bounds) * 0.5f) {
actionsScrollViewRect.size.height =
CGRectGetHeight(self.bounds) - contentScrollViewRect.size.height;
} else {
CGFloat maxActionsHeight = CGRectGetHeight(self.bounds) * 0.5f;
actionsScrollViewRect.size.height = MIN(maxActionsHeight, actionsScrollViewRect.size.height);
}
actionsScrollViewRect.origin.y =
CGRectGetHeight(self.bounds) - actionsScrollViewRect.size.height;
self.actionsScrollView.frame = actionsScrollViewRect;
contentScrollViewRect.size.height =
CGRectGetHeight(self.bounds) - actionsScrollViewRect.size.height;
self.contentScrollView.frame = contentScrollViewRect;
}
}
#pragma mark - Dynamic Type
- (BOOL)mdc_adjustsFontForContentSizeCategory {
return _mdc_adjustsFontForContentSizeCategory;
}
- (void)mdc_setAdjustsFontForContentSizeCategory:(BOOL)adjusts {
_mdc_adjustsFontForContentSizeCategory = adjusts;
for (MDCFlatButton *button in _actionButtons) {
button.mdc_adjustsFontForContentSizeCategory = adjusts;
}
[self updateFonts];
}
// Update the fonts used based on whether Dynamic Type is enabled
- (void)updateFonts {
[self updateTitleFont];
[self updateMessageFont];
[self updateButtonFont];
[self setNeedsLayout];
}
@end