| // 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 "MDCAlertActionManager.h" | |
| #import "MDCAlertControllerView+Private.h" | |
| #import "MaterialShadowElevations.h" | |
| #import <MDFInternationalization/MDFInternationalization.h> | |
| #import "MaterialButtons.h" | |
| #import "MaterialDialogs.h" | |
| #import "MaterialTypography.h" | |
| #import "MaterialMath.h" | |
| // https://material.io/go/design-dialogs#dialogs-specs | |
| static const MDCFontTextStyle kTitleTextStyle = MDCFontTextStyleTitle; | |
| static const MDCFontTextStyle kMessageTextStyle = MDCFontTextStyleBody1; | |
| static const CGFloat MDCDialogMaximumWidth = 560.0f; | |
| static const CGFloat MDCDialogMinimumWidth = 280.0f; | |
| static const CGFloat MDCDialogActionButtonMinimumHeight = 36.0f; | |
| static const CGFloat MDCDialogActionButtonMinimumWidth = 48.0f; | |
| static const CGFloat MDCDialogActionMinTouchTarget = 48.0f; | |
| static const CGFloat MDCDialogMessageOpacity = 0.54f; | |
| /** Calculates the minimum text height for a single line text using device metrics. */ | |
| static CGFloat SingleLineTextViewHeight(NSString *_Nullable title, UIFont *_Nullable font) { | |
| if (title.length == 0) { | |
| return font.lineHeight; | |
| } | |
| NSDictionary<NSAttributedStringKey, id> *attributes = | |
| font == nil ? nil : @{NSFontAttributeName : font}; | |
| CGRect boundingRect = [title boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) | |
| options:NSStringDrawingUsesDeviceMetrics | |
| attributes:attributes | |
| context:nil]; | |
| // Some text height may exceed the UIFont.lineHeight. | |
| // https://developer.apple.com/documentation/uikit/uifont?language=objc | |
| // -------------------- | |
| // | | | |
| // | <= ascender | | |
| // | | <= lineHeight | |
| // -----------------| | |
| // | <= descender | | |
| // -------------------- | |
| // UIFont.lineHeight consist of UIFont.ascender and UIFont.descender. UIFont.descender is the | |
| // bottom offset from the baseline. The boundingRect.origin.y is the amount of space the text will | |
| // occupy in UIFont.descender. So the minimum text view line height is text height + the leftover | |
| // space in the UIFont.descender not occupied by the rendered text. | |
| CGFloat bottomPadding = boundingRect.origin.y - font.descender; | |
| return boundingRect.size.height + bottomPadding; | |
| } | |
| @interface MDCNonselectableTextView : UITextView | |
| @end | |
| @interface MDCAlertControllerView () | |
| @property(nonatomic, getter=isVerticalActionsLayout) BOOL verticalActionsLayout; | |
| @property(nonatomic, strong, nullable) UIColor *buttonColor UI_APPEARANCE_SELECTOR; | |
| @property(nonatomic, strong, nullable) UIColor *buttonInkColor UI_APPEARANCE_SELECTOR; | |
| @property(nonatomic, assign) BOOL enableRippleBehavior; | |
| @end | |
| @implementation MDCAlertControllerView { | |
| BOOL _mdc_adjustsFontForContentSizeCategory; | |
| } | |
| - (instancetype)initWithFrame:(CGRect)frame { | |
| self = [super initWithFrame:frame]; | |
| if (self) { | |
| self.autoresizesSubviews = NO; | |
| self.clipsToBounds = YES; | |
| self.shouldGroupAccessibilityChildren = YES; | |
| self.orderVerticalActionsByEmphasis = NO; | |
| self.actionsHorizontalAlignment = MDCContentHorizontalAlignmentTrailing; | |
| self.actionsHorizontalAlignmentInVerticalLayout = MDCContentHorizontalAlignmentCenter; | |
| self.titleIconInsets = UIEdgeInsetsMake(24.0f, 24.0f, 12.0f, 24.0f); | |
| self.titleInsets = UIEdgeInsetsMake(24.0f, 24.0f, 20.0f, 24.0f); | |
| self.contentInsets = UIEdgeInsetsMake(24.0f, 24.0f, 24.0f, 24.0f); | |
| self.actionsInsets = UIEdgeInsetsMake(8.0f, 8.0f, 8.0f, 8.0f); | |
| self.actionsHorizontalMargin = 8.0f; | |
| self.actionsVerticalMargin = 12.0f; | |
| self.accessoryViewVerticalInset = 20.0f; | |
| self.accessoryViewHorizontalInset = 0.0f; | |
| self.titleScrollView = [[UIScrollView alloc] initWithFrame:CGRectZero]; | |
| [self addSubview:self.titleScrollView]; | |
| self.contentScrollView = [[UIScrollView alloc] initWithFrame:CGRectZero]; | |
| [self addSubview:self.contentScrollView]; | |
| self.actionsScrollView = [[UIScrollView alloc] initWithFrame:CGRectZero]; | |
| [self addSubview:self.actionsScrollView]; | |
| // Set the background color after all surface subviews are added. | |
| self.backgroundColor = [UIColor whiteColor]; | |
| 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.titleLabel.accessibilityTraits |= UIAccessibilityTraitHeader; | |
| [self.titleScrollView addSubview:self.titleLabel]; | |
| self.messageTextView = [[MDCNonselectableTextView alloc] initWithFrame:CGRectZero]; | |
| self.messageTextView.textAlignment = NSTextAlignmentNatural; | |
| self.messageTextView.textContainerInset = UIEdgeInsetsZero; | |
| self.messageTextView.textContainer.lineFragmentPadding = 0.0f; | |
| self.messageTextView.editable = NO; | |
| self.messageTextView.scrollEnabled = NO; | |
| self.messageTextView.selectable = YES; // Enables link tap. | |
| if (self.mdc_adjustsFontForContentSizeCategory) { | |
| self.messageTextView.font = | |
| [UIFont mdc_preferredFontForMaterialTextStyle:MDCFontTextStyleBody1]; | |
| } else { | |
| self.messageTextView.font = [MDCTypography body1Font]; | |
| } | |
| self.messageTextView.textColor = [UIColor colorWithWhite:0 alpha:MDCDialogMessageOpacity]; | |
| // The messageTextView is a private API, and therefore it needs to inherit its background | |
| // color from the alert's background so it can be themed (necessary for dark mode support, | |
| // for instance). | |
| self.messageTextView.backgroundColor = UIColor.clearColor; | |
| [self.contentScrollView addSubview:self.messageTextView]; | |
| [self setNeedsLayout]; | |
| } | |
| return self; | |
| } | |
| - (NSString *)title { | |
| return self.titleLabel.text; | |
| } | |
| - (void)setTitle:(NSString *)title { | |
| self.titleLabel.text = title; | |
| [self setNeedsLayout]; | |
| } | |
| - (void)setBackgroundColor:(UIColor *)backgroundColor { | |
| super.backgroundColor = backgroundColor; | |
| self.titleScrollView.backgroundColor = backgroundColor; | |
| self.contentScrollView.backgroundColor = backgroundColor; | |
| self.actionsScrollView.backgroundColor = backgroundColor; | |
| } | |
| - (UIColor *)backgroundColor { | |
| return super.backgroundColor; | |
| } | |
| - (void)addActionButton:(nonnull MDCButton *)button { | |
| if (button.superview == nil) { | |
| [self.actionsScrollView addSubview:button]; | |
| if (_buttonColor) { | |
| // We only set if _buttonColor since settingTitleColor to nil doesn't | |
| // reset the title to the default | |
| [button setTitleColor:_buttonColor forState:UIControlStateNormal]; | |
| } | |
| button.enableRippleBehavior = self.enableRippleBehavior; | |
| button.inkColor = self.buttonInkColor; | |
| // These two lines must be after @c setTitleFont:forState: in order to @c MDCButton to handle | |
| // dynamic type correctly. | |
| button.adjustsFontForContentSizeCategoryWhenScaledFontIsUnavailable = | |
| self.adjustsFontForContentSizeCategoryWhenScaledFontIsUnavailable; | |
| button.mdc_adjustsFontForContentSizeCategory = self.mdc_adjustsFontForContentSizeCategory; | |
| // TODO(#1726): Determine default text color values for Normal and Disabled | |
| button.minimumSize = | |
| CGSizeMake(MDCDialogActionButtonMinimumWidth, MDCDialogActionButtonMinimumHeight); | |
| // Set centerVisibleArea to YES by default. | |
| button.centerVisibleArea = YES; | |
| } | |
| } | |
| + (void)styleAsTextButton:(nonnull MDCButton *)button { | |
| // This preserves default buttons style (as MDCFlatButton/text) for backward compatibility reasons | |
| UIColor *themeColor = [UIColor blackColor]; | |
| [button setBackgroundColor:UIColor.clearColor forState:UIControlStateNormal]; | |
| [button setTitleColor:themeColor forState:UIControlStateNormal]; | |
| [button setImageTintColor:themeColor forState:UIControlStateNormal]; | |
| [button setInkColor:[UIColor colorWithWhite:0.0f alpha:0.06f]]; | |
| button.disabledAlpha = 1.0f; | |
| [button setElevation:MDCShadowElevationNone forState:UIControlStateNormal]; | |
| } | |
| - (void)setTitleFont:(UIFont *)font { | |
| _titleFont = font; | |
| [self updateTitleFont]; | |
| } | |
| - (void)updateTitleFont { | |
| UIFont *titleFont = self.titleFont ?: [[self class] titleFontDefault]; | |
| if (self.mdc_adjustsFontForContentSizeCategory) { | |
| if (self.adjustsFontForContentSizeCategoryWhenScaledFontIsUnavailable) { | |
| titleFont = | |
| [titleFont mdc_fontSizedForMaterialTextStyle:kTitleTextStyle | |
| scaledForDynamicType:self.mdc_adjustsFontForContentSizeCategory]; | |
| } | |
| } | |
| self.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; | |
| } | |
| - (NSTextAlignment)titleAlignment { | |
| return self.titleLabel.textAlignment; | |
| } | |
| - (void)setTitleAlignment:(NSTextAlignment)titleAlignment { | |
| self.titleLabel.textAlignment = titleAlignment; | |
| } | |
| - (UIImage *)titleIcon { | |
| return self.titleIconImageView.image; | |
| } | |
| - (void)setTitleIcon:(UIImage *)titleIcon { | |
| if (titleIcon == nil) { | |
| [self.titleIconImageView removeFromSuperview]; | |
| self.titleIconImageView = nil; | |
| [self setNeedsLayout]; | |
| return; | |
| } | |
| if (self.titleIconImageView == nil) { | |
| self.titleIconImageView = [[UIImageView alloc] initWithImage:titleIcon]; | |
| self.titleIconImageView.contentMode = UIViewContentModeScaleAspectFit; | |
| [self.titleScrollView addSubview:self.titleIconImageView]; | |
| } else { | |
| self.titleIconImageView.image = titleIcon; | |
| } | |
| self.titleIconImageView.tintColor = self.titleIconTintColor; | |
| [self setNeedsLayout]; | |
| } | |
| - (void)setTitleIconTintColor:(UIColor *)titleIconTintColor { | |
| _titleIconTintColor = titleIconTintColor; | |
| self.titleIconImageView.tintColor = titleIconTintColor; | |
| } | |
| - (void)setTitleIconAlignment:(NSTextAlignment)titleIconAlignment { | |
| _titleIconAlignment = titleIconAlignment; | |
| [self setNeedsLayout]; | |
| } | |
| - (void)setTitleIconView:(UIView *)titleIconView { | |
| if (titleIconView != nil && self.titleIconImageView != nil) { | |
| NSLog(@"Warning: unintended use of the API. The following APIs are not expected to be used" | |
| "together: 'setTitleIconView:' and `setTitleIcon:` API. Please set either, but not " | |
| "both at the same time. If 'titleIconView' is set, 'titleIcon' is ignored."); | |
| [self.titleIconImageView removeFromSuperview]; | |
| self.titleIconImageView = nil; | |
| [self setNeedsLayout]; | |
| } | |
| if (_titleIconView == nil || ![_titleIconView isEqual:titleIconView]) { | |
| if (_titleIconView != nil) { | |
| [_titleIconView removeFromSuperview]; | |
| } | |
| _titleIconView = titleIconView; | |
| if (_titleIconView != nil) { | |
| [self.titleScrollView addSubview:_titleIconView]; | |
| } | |
| [self setNeedsLayout]; | |
| } | |
| } | |
| - (NSString *)message { | |
| return self.messageTextView.text; | |
| } | |
| - (void)setMessage:(NSString *)message { | |
| self.messageTextView.text = message; | |
| [self setNeedsLayout]; | |
| } | |
| - (void)setMessageFont:(UIFont *)messageFont { | |
| _messageFont = messageFont; | |
| [self updateMessageFont]; | |
| } | |
| - (void)updateMessageFont { | |
| UIFont *messageFont = self.messageFont ?: [[self class] messageFontDefault]; | |
| if (self.mdc_adjustsFontForContentSizeCategory) { | |
| if (self.adjustsFontForContentSizeCategoryWhenScaledFontIsUnavailable) { | |
| messageFont = [messageFont | |
| mdc_fontSizedForMaterialTextStyle:kMessageTextStyle | |
| scaledForDynamicType:self.mdc_adjustsFontForContentSizeCategory]; | |
| } | |
| } | |
| self.messageTextView.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; | |
| _messageTextView.textColor = messageColor; | |
| } | |
| - (NSTextAlignment)messageAlignment { | |
| return self.messageTextView.textAlignment; | |
| } | |
| - (void)setMessageAlignment:(NSTextAlignment)messageAlignment { | |
| self.messageTextView.textAlignment = messageAlignment; | |
| } | |
| - (void)setAccessoryView:(UIView *)accessoryView { | |
| if (_accessoryView == accessoryView) { | |
| return; | |
| } | |
| if (_accessoryView.superview == self.contentScrollView) { | |
| [_accessoryView removeFromSuperview]; | |
| } | |
| _accessoryView = accessoryView; | |
| if (_accessoryView) { | |
| [self.contentScrollView addSubview:_accessoryView]; | |
| } | |
| [self setNeedsLayout]; | |
| } | |
| - (void)updateButtonFont { | |
| for (MDCButton *button in self.actionManager.buttonsInActionOrder) { | |
| UIFont *buttonFont; | |
| if (button.enableTitleFontForState) { | |
| buttonFont = [button titleFontForState:UIControlStateNormal]; | |
| } else { | |
| buttonFont = button.titleLabel.font; | |
| } | |
| if (self.mdc_adjustsFontForContentSizeCategory) { | |
| if (self.adjustsFontForContentSizeCategoryWhenScaledFontIsUnavailable) { | |
| buttonFont = [buttonFont | |
| mdc_fontSizedForMaterialTextStyle:kTitleTextStyle | |
| scaledForDynamicType:self.mdc_adjustsFontForContentSizeCategory]; | |
| } | |
| } | |
| if (button.enableTitleFontForState) { | |
| [button setTitleFont:buttonFont forState:UIControlStateNormal]; | |
| } else { | |
| button.titleLabel.font = buttonFont; | |
| } | |
| } | |
| [self setNeedsLayout]; | |
| } | |
| - (void)setButtonColor:(UIColor *)color { | |
| _buttonColor = color; | |
| for (MDCButton *button in self.actionManager.buttonsInActionOrder) { | |
| [button setTitleColor:_buttonColor forState:UIControlStateNormal]; | |
| } | |
| } | |
| - (void)setButtonInkColor:(UIColor *)color { | |
| _buttonInkColor = color; | |
| for (MDCButton *button in self.actionManager.buttonsInActionOrder) { | |
| button.inkColor = color; | |
| } | |
| } | |
| - (CGFloat)cornerRadius { | |
| return self.layer.cornerRadius; | |
| } | |
| - (void)setCornerRadius:(CGFloat)cornerRadius { | |
| if (MDCCGFloatEqual(cornerRadius, self.layer.cornerRadius)) { | |
| return; | |
| } | |
| self.layer.cornerRadius = cornerRadius; | |
| [self setNeedsLayout]; | |
| } | |
| - (void)setEnableRippleBehavior:(BOOL)enableRippleBehavior { | |
| if (_enableRippleBehavior == enableRippleBehavior) { | |
| return; | |
| } | |
| _enableRippleBehavior = enableRippleBehavior; | |
| for (MDCButton *button in self.actionManager.buttonsInActionOrder) { | |
| button.enableRippleBehavior = enableRippleBehavior; | |
| } | |
| } | |
| #pragma mark - Internal | |
| - (CGSize)actionFittingSizeInHorizontalLayout { | |
| CGSize size = CGSizeMake([self horizontalSpacing], 0.0f); | |
| NSArray<MDCButton *> *buttons = self.actionManager.buttonsInActionOrder; | |
| UIEdgeInsets actionsInsets = [self actionsInsetsForButtons:buttons isHorizontalLayout:YES]; | |
| if (0 < buttons.count) { | |
| CGFloat maxButtonHeight = | |
| MAX(MDCDialogActionButtonMinimumHeight, MDCDialogActionMinTouchTarget); | |
| for (UIButton *button in buttons) { | |
| CGSize buttonSize = [button sizeThatFits:size]; | |
| size.width += buttonSize.width; | |
| maxButtonHeight = MAX(maxButtonHeight, buttonSize.height); | |
| } | |
| size.height = actionsInsets.top + maxButtonHeight + actionsInsets.bottom; | |
| } | |
| return size; | |
| } | |
| - (CGSize)actionButtonsSizeInVerticalLayout { | |
| CGSize size = CGSizeZero; | |
| NSArray<MDCButton *> *buttons = self.actionManager.buttonsInActionOrder; | |
| UIEdgeInsets actionsInsets = [self actionsInsetsForButtons:buttons isHorizontalLayout:NO]; | |
| if (0 < buttons.count) { | |
| size.height = actionsInsets.top + actionsInsets.bottom; | |
| CGFloat widthInset = actionsInsets.left + actionsInsets.right; | |
| for (NSUInteger index = 0; index < buttons.count; ++index) { | |
| MDCButton *button = buttons[index]; | |
| CGSize buttonSize = [button sizeThatFits:size]; | |
| CGFloat minButtonHeight = | |
| MAX(MDCDialogActionButtonMinimumHeight, MDCDialogActionMinTouchTarget); | |
| buttonSize.height = MAX(buttonSize.height, minButtonHeight); | |
| size.height += buttonSize.height; | |
| size.width = MAX(size.width, buttonSize.width + widthInset); | |
| if (button != buttons.lastObject) { | |
| MDCButton *nextButton = buttons[index + 1]; | |
| CGFloat verticalMargin = [self actionsVerticalMarginBetweenTopButton:button | |
| bottomButton:nextButton]; | |
| size.height += verticalMargin; | |
| } | |
| } | |
| } | |
| return size; | |
| } | |
| - (UIEdgeInsets)actionsInsetsForButtons:(NSArray<MDCButton *> *)buttons | |
| isHorizontalLayout:(BOOL)isHorizontalLayout { | |
| UIEdgeInsets actionInsets = self.actionsInsets; | |
| if (buttons.count == 0) { | |
| return actionInsets; | |
| } | |
| if (isHorizontalLayout) { | |
| CGFloat maxButtonHeight = MDCDialogActionButtonMinimumHeight; | |
| for (MDCButton *button in buttons) { | |
| CGSize buttonSize = [button sizeThatFits:CGSizeZero]; | |
| maxButtonHeight = MAX(maxButtonHeight, buttonSize.height); | |
| } | |
| if (maxButtonHeight < MDCDialogActionMinTouchTarget) { | |
| // Adjust actionInsets when buttons don't meet the minimum touch area requirement. | |
| CGFloat verticalInsetsAdjustment = MDCDialogActionMinTouchTarget - maxButtonHeight; | |
| actionInsets.top -= verticalInsetsAdjustment / 2; | |
| actionInsets.bottom -= verticalInsetsAdjustment / 2; | |
| } | |
| } else { | |
| MDCButton *topButton = buttons.firstObject; | |
| CGSize topButtonSize = [topButton sizeThatFits:CGSizeZero]; | |
| if (topButtonSize.height < MDCDialogActionMinTouchTarget) { | |
| CGFloat verticalInsetsAdjustment = MDCDialogActionMinTouchTarget - topButtonSize.height; | |
| actionInsets.top -= verticalInsetsAdjustment / 2; | |
| } | |
| MDCButton *bottomButton = buttons.lastObject; | |
| CGSize bottomButtonSize = [bottomButton sizeThatFits:CGSizeZero]; | |
| if (bottomButtonSize.height < MDCDialogActionMinTouchTarget) { | |
| CGFloat verticalInsetsAdjustment = MDCDialogActionMinTouchTarget - bottomButtonSize.height; | |
| actionInsets.bottom -= verticalInsetsAdjustment / 2; | |
| } | |
| } | |
| return actionInsets; | |
| } | |
| - (CGFloat)actionsVerticalMarginBetweenTopButton:(MDCButton *)topButton | |
| bottomButton:(MDCButton *)bottomButton { | |
| CGFloat actionsVerticalMargin = self.actionsVerticalMargin; | |
| CGSize topButtonSize = [topButton sizeThatFits:CGSizeZero]; | |
| if (topButtonSize.height < MDCDialogActionMinTouchTarget) { | |
| CGFloat verticalInsetsAdjustment = MDCDialogActionMinTouchTarget - topButtonSize.height; | |
| actionsVerticalMargin -= verticalInsetsAdjustment / 2; | |
| } | |
| CGSize bottomButtonSize = [bottomButton sizeThatFits:CGSizeZero]; | |
| if (bottomButtonSize.height < MDCDialogActionMinTouchTarget) { | |
| CGFloat verticalInsetsAdjustment = MDCDialogActionMinTouchTarget - bottomButtonSize.height; | |
| actionsVerticalMargin -= verticalInsetsAdjustment / 2; | |
| } | |
| return actionsVerticalMargin; | |
| } | |
| - (CGFloat)horizontalSpacing { | |
| NSUInteger count = self.actionManager.buttonsInActionOrder.count; | |
| CGFloat spacing = self.actionsInsets.left + self.actionsInsets.right + | |
| (count - 1) * self.actionsHorizontalMargin; | |
| return spacing; | |
| } | |
| - (CGFloat)widestAction { | |
| CGFloat widest = 0.0f; | |
| for (MDCButton *button in self.actionManager.buttonsInActionOrder) { | |
| // For justified alignment, minimumSize is used to control button's visible area. | |
| // If user sets a custom value, it is ignored because button's final size is decided | |
| // during layout. | |
| // We need to reset minimumSize during sizeThatFits: to make sure it doesn't affect the | |
| // calculation here. | |
| CGSize currentMinimumSize = CGSizeZero; | |
| if (self.actionsHorizontalAlignment == MDCContentHorizontalAlignmentJustified) { | |
| currentMinimumSize = button.minimumSize; | |
| button.minimumSize = | |
| CGSizeMake(MDCDialogActionButtonMinimumWidth, MDCDialogActionButtonMinimumHeight); | |
| } | |
| CGSize buttonSize = [button sizeThatFits:CGSizeZero]; | |
| if (buttonSize.width > widest) { | |
| widest = buttonSize.width; | |
| } | |
| if (self.actionsHorizontalAlignment == MDCContentHorizontalAlignmentJustified) { | |
| button.minimumSize = currentMinimumSize; | |
| } | |
| } | |
| return widest; | |
| } | |
| - (BOOL)hasTitleIconOrImage { | |
| return [self hasTitleIcon] || self.titleIconView != nil; | |
| } | |
| - (BOOL)hasTitleIcon { | |
| return self.titleIconImageView.image.size.height > 0.0f; | |
| } | |
| - (BOOL)hasTitle { | |
| return self.title.length > 0; | |
| } | |
| - (BOOL)hasMessage { | |
| return self.message.length > 0; | |
| } | |
| - (BOOL)hasAccessoryView { | |
| CGSize accessoryViewSize = [self.accessoryView systemLayoutSizeFittingSize:CGRectInfinite.size]; | |
| return accessoryViewSize.height > 0.0f; | |
| } | |
| - (CGSize)messageTextViewSizeThatFits:(CGSize)size { | |
| // Returns a size of zero when there's no message, to ensure no space is reserved for it. | |
| return [self.messageTextView hasText] ? [self.messageTextView sizeThatFits:size] : CGSizeZero; | |
| } | |
| /** Return the space between the title and the title icon, or 0 if any of them is missing. */ | |
| - (CGFloat)titleIconInsetBottom { | |
| return [self hasTitleIconOrImage] && [self hasTitle] ? self.titleIconInsets.bottom : 0.0f; | |
| } | |
| - (CGFloat)titleInsetTop { | |
| return [self hasTitleIconOrImage] ? self.titleIconInsets.top | |
| : ([self hasTitle] ? self.titleInsets.top : 0.f); | |
| } | |
| - (CGFloat)titleInsetBottom { | |
| return ([self hasTitle] || [self hasTitleIconOrImage]) ? self.titleInsets.bottom : 0.0f; | |
| } | |
| - (CGFloat)contentInsetTop { | |
| return ([self hasTitle] || [self hasTitleIconOrImage]) ? 0.0f : self.contentInsets.top; | |
| } | |
| - (CGFloat)accessoryVerticalInset { | |
| return ([self hasMessage] && [self hasAccessoryView]) ? self.accessoryViewVerticalInset : 0.0f; | |
| } | |
| // Returns the size of the title view or the current size of the title icon imageView. | |
| - (CGSize)titleIconViewSize { | |
| if (self.titleIconView != nil) { | |
| return self.titleIconView.frame.size; | |
| } else if (self.titleIconImageView != nil) { | |
| return self.titleIconImageView.frame.size; | |
| } | |
| return CGSizeZero; | |
| } | |
| // Returns the size of the title view or the original size of the title icon image. | |
| - (CGSize)titleIconImageSize { | |
| if (self.titleIconView != nil) { | |
| return self.titleIconView.frame.size; | |
| } else if (self.titleIcon != nil) { | |
| return self.titleIcon.size; | |
| } | |
| return CGSizeZero; | |
| } | |
| - (CGRect)titleFrameWithTitleSize:(CGSize)titleSize { | |
| CGFloat leftInset = self.titleInsets.left; | |
| CGFloat titleTop = | |
| [self titleIconViewSize].height + [self titleInsetTop] + [self titleIconInsetBottom]; | |
| return CGRectMake(leftInset, titleTop, titleSize.width, titleSize.height); | |
| } | |
| - (CGRect)messageFrameWithSize:(CGSize)messageSize { | |
| CGFloat top = [self contentInsetTop]; | |
| return CGRectMake(self.contentInsets.left, top, messageSize.width, messageSize.height); | |
| } | |
| /** | |
| Calculate the frame of the titleIcon, given the title size and bounds size. | |
| @param titleSize is the pre-calculated size of the title. | |
| @param boundsSize is the total bounds without any internal margins or padding. | |
| */ | |
| - (CGRect)titleIconFrameWithTitleSize:(CGSize)titleSize boundsSize:(CGSize)boundsSize { | |
| CGSize titleIconViewSize = [self titleIconImageSize]; | |
| CGFloat leftInset = self.titleIconInsets.left; | |
| CGFloat topInset = self.titleIconInsets.top; | |
| CGFloat titleIconHeight = titleIconViewSize.height; | |
| CGFloat titleIconWidth = titleIconViewSize.width; | |
| BOOL isRightOrRTLNatural = (self.titleIconAlignment == NSTextAlignmentRight || | |
| (self.titleIconAlignment == NSTextAlignmentNatural && | |
| [self mdf_effectiveUserInterfaceLayoutDirection] == | |
| UIUserInterfaceLayoutDirectionRightToLeft)); | |
| if (self.titleIconAlignment == NSTextAlignmentJustified) { | |
| // Justified images are sized to fit the alert's width (minus the insets). | |
| titleIconWidth = boundsSize.width - leftInset - self.titleIconInsets.right; | |
| if (titleIconViewSize.width > titleIconWidth && [self hasTitleIcon]) { | |
| // If the width decreased, then decrease the height proportionally. | |
| titleIconHeight *= titleIconWidth / titleIconViewSize.width; | |
| } | |
| } else { | |
| CGRect titleFrame = [self titleFrameWithTitleSize:titleSize]; | |
| if (self.titleIconAlignment == NSTextAlignmentCenter) { | |
| leftInset = | |
| CGRectGetMinX(titleFrame) + (CGRectGetWidth(titleFrame) - titleIconViewSize.width) / 2.0f; | |
| } else if (isRightOrRTLNatural) { | |
| leftInset = CGRectGetMaxX(titleFrame) - titleIconViewSize.width; | |
| } | |
| } | |
| return CGRectMake(leftInset, topInset, titleIconWidth, titleIconHeight); | |
| } | |
| // @param boundsSize should not include any internal margins or padding | |
| - (CGSize)calculatePreferredContentSizeForBounds:(CGSize)boundsSize { | |
| // Even if we have more room, limit our maximum width | |
| boundsSize.width = MIN(MAX(boundsSize.width, MDCDialogMinimumWidth), MDCDialogMaximumWidth); | |
| // Title, Content & Actions | |
| CGSize titleViewSize = [self calculateTitleViewSizeThatFitsWidth:boundsSize.width]; | |
| CGSize contentSize = [self calculateContentSizeThatFitsWidth:boundsSize.width]; | |
| CGSize actionSize = [self calculateActionsSizeThatFitsWidth:boundsSize.width]; | |
| // Final Sizing | |
| CGSize totalSize; | |
| totalSize.width = MAX(MAX(titleViewSize.width, MAX(contentSize.width, actionSize.width)), | |
| MDCDialogMinimumWidth); | |
| totalSize.height = titleViewSize.height + contentSize.height + actionSize.height; | |
| return totalSize; | |
| } | |
| // @param boundingWidth should not include any internal margins or padding | |
| - (CGSize)calculateContentSizeThatFitsWidth:(CGFloat)boundingWidth { | |
| CGFloat contentInsets = self.contentInsets.left + self.contentInsets.right; | |
| if (self.accessoryViewHorizontalInset > 0) { | |
| contentInsets += self.accessoryViewHorizontalInset * 2.0; | |
| } | |
| CGFloat titleInsets = self.titleInsets.left + self.titleInsets.right; | |
| CGSize boundsSize = CGRectInfinite.size; | |
| boundsSize.width = boundingWidth - titleInsets; | |
| CGFloat titleWidth = [self.titleLabel sizeThatFits:boundsSize].width; | |
| boundsSize.width = boundingWidth - contentInsets; | |
| CGSize messageSize = [self messageTextViewSizeThatFits:boundsSize]; | |
| CGSize accessoryViewSize = [self.accessoryView systemLayoutSizeFittingSize:boundsSize]; | |
| CGFloat maxWidth = MAX(messageSize.width, accessoryViewSize.width); | |
| CGFloat contentWidth = MAX(titleWidth + titleInsets, maxWidth + contentInsets); | |
| CGFloat totalElementsHeight = messageSize.height + accessoryViewSize.height; | |
| CGFloat contentHeight = (fabs(contentWidth) <= FLT_EPSILON) || totalElementsHeight <= FLT_EPSILON | |
| ? 0.0f | |
| : totalElementsHeight + [self accessoryVerticalInset] + | |
| self.contentInsets.bottom + [self contentInsetTop]; | |
| return CGSizeMake((CGFloat)ceil(contentWidth), (CGFloat)ceil(contentHeight)); | |
| } | |
| /** | |
| Calculate the size of the title frame, which includes an optional title, optional title icon and | |
| optional icon view. | |
| @param boundingWidth should not include any internal margins or padding | |
| */ | |
| - (CGSize)calculateTitleViewSizeThatFitsWidth:(CGFloat)boundingWidth { | |
| CGFloat contentInsets = self.contentInsets.left + self.contentInsets.right; | |
| CGFloat titleInsets = self.titleInsets.left + self.titleInsets.left; | |
| CGSize contentSize = CGRectInfinite.size; | |
| contentSize.width = boundingWidth - contentInsets; | |
| CGFloat messageWidth = [self messageTextViewSizeThatFits:contentSize].width; | |
| CGFloat contentWidth = | |
| MAX(messageWidth, [self.accessoryView systemLayoutSizeFittingSize:contentSize].width); | |
| CGSize titleViewSize = CGRectInfinite.size; | |
| titleViewSize.width = boundingWidth - titleInsets; | |
| CGSize titleLabelSize = [self.titleLabel sizeThatFits:titleViewSize]; | |
| CGSize titleIconSize = [self titleIconViewSize]; | |
| CGFloat titleViewWidth = MAX(titleLabelSize.width + titleInsets, contentWidth + contentInsets); | |
| CGFloat totalElementsHeight = titleIconSize.height + titleLabelSize.height; | |
| CGFloat titleHeight = totalElementsHeight + [self titleInsetTop] + [self titleIconInsetBottom] + | |
| [self titleInsetBottom]; | |
| return CGSizeMake((CGFloat)ceil(titleViewWidth), (CGFloat)ceil(titleHeight)); | |
| } | |
| // @param boundingWidth should not include any internal margins or padding | |
| - (CGSize)calculateActionsSizeThatFitsWidth:(CGFloat)boundingWidth { | |
| CGSize boundsSize = CGRectInfinite.size; | |
| boundsSize.width = boundingWidth; | |
| CGSize horizontalSize = [self actionFittingSizeInHorizontalLayout]; | |
| CGSize verticalSize = [self actionButtonsSizeInVerticalLayout]; | |
| BOOL isVertical = boundsSize.width < horizontalSize.width; | |
| NSUInteger count = self.actionManager.buttonsInActionOrder.count; | |
| if (self.actionsHorizontalAlignment == MDCContentHorizontalAlignmentJustified && count > 1) { | |
| // b/155350470: ensure long justified actions are vertically aligned based on the longest | |
| // button. | |
| isVertical = | |
| [self widestAction] > (CGFloat)ceil((boundingWidth - [self horizontalSpacing]) / count); | |
| } | |
| CGSize actionsSize; | |
| if (isVertical) { | |
| // Use VerticalLayout | |
| if (self.actionsHorizontalAlignmentInVerticalLayout == MDCContentHorizontalAlignmentJustified) { | |
| verticalSize.width = boundingWidth - (self.actionsInsets.left + self.actionsInsets.right); | |
| } | |
| actionsSize.width = MIN(verticalSize.width, boundsSize.width); | |
| actionsSize.height = MIN(verticalSize.height, boundsSize.height); | |
| self.verticalActionsLayout = YES; | |
| } else { | |
| // Use HorizontalLayout | |
| if (self.actionsHorizontalAlignment == MDCContentHorizontalAlignmentJustified) { | |
| horizontalSize.width = boundingWidth - (self.actionsInsets.left + self.actionsInsets.right); | |
| } | |
| actionsSize.width = MIN(horizontalSize.width, boundsSize.width); | |
| actionsSize.height = MIN(horizontalSize.height, boundsSize.height); | |
| self.verticalActionsLayout = NO; | |
| } | |
| actionsSize.width = (CGFloat)ceil(actionsSize.width); | |
| actionsSize.height = (CGFloat)ceil(actionsSize.height); | |
| return actionsSize; | |
| } | |
| - (BOOL)canBecomeFirstResponder { | |
| return YES; | |
| } | |
| - (void)layoutSubviews { | |
| [super layoutSubviews]; | |
| // Used to calculate the height of the scrolling content, so we limit the width. | |
| CGSize boundsSize = CGRectInfinite.size; | |
| boundsSize.width = CGRectGetWidth(self.bounds); | |
| // Title view | |
| CGSize titleViewSize = [self calculateTitleViewSizeThatFitsWidth:boundsSize.width]; | |
| CGRect titleViewRect = CGRectZero; | |
| titleViewRect.size.width = CGRectGetWidth(self.bounds); | |
| titleViewRect.size.height = titleViewSize.height; | |
| self.titleScrollView.contentSize = titleViewRect.size; | |
| self.titleScrollView.frame = titleViewRect; | |
| // 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 | |
| CGSize titleBoundsSize = boundsSize; | |
| titleBoundsSize.width = boundsSize.width - (self.titleInsets.left + self.titleInsets.right); | |
| CGSize titleSize = [self.titleLabel sizeThatFits:titleBoundsSize]; | |
| titleSize.width = titleBoundsSize.width; | |
| CGSize contentBoundsSize = boundsSize; | |
| contentBoundsSize.width = boundsSize.width - (self.contentInsets.left + self.contentInsets.right); | |
| CGSize messageSize = [self messageTextViewSizeThatFits:contentBoundsSize]; | |
| messageSize.width = contentBoundsSize.width; | |
| CGSize accessoryViewSize = | |
| [self.accessoryView systemLayoutSizeFittingSize:contentBoundsSize | |
| withHorizontalFittingPriority:UILayoutPriorityRequired | |
| verticalFittingPriority:UILayoutPriorityFittingSizeLevel]; | |
| accessoryViewSize.width = contentBoundsSize.width; | |
| CGRect titleIconImageViewRect = [self titleIconFrameWithTitleSize:titleSize | |
| boundsSize:boundsSize]; | |
| if (self.titleIconView != nil) { | |
| self.titleIconView.frame = titleIconImageViewRect; | |
| } else if (self.titleIconImageView != nil) { | |
| // Match the title icon alignment to the title alignment. | |
| self.titleIconImageView.frame = titleIconImageViewRect; | |
| } | |
| // Calculate the title frame after the title icon size has been determined. | |
| CGRect titleFrame = [self titleFrameWithTitleSize:titleSize]; | |
| CGRect messageFrame = [self messageFrameWithSize:messageSize]; | |
| CGRect accessoryViewFrame = CGRectMake( | |
| self.contentInsets.left + self.accessoryViewHorizontalInset, | |
| CGRectGetMaxY(messageFrame) + [self accessoryVerticalInset], | |
| accessoryViewSize.width - self.accessoryViewHorizontalInset * 2.0, accessoryViewSize.height); | |
| self.titleLabel.frame = titleFrame; | |
| self.messageTextView.frame = messageFrame; | |
| self.accessoryView.frame = accessoryViewFrame; | |
| // Actions | |
| NSArray<MDCButton *> *buttons = self.actionManager.buttonsInActionOrder; | |
| CGSize actionSize = [self calculateActionsSizeThatFitsWidth:boundsSize.width]; | |
| CGRect actionsFrame = CGRectZero; | |
| actionsFrame.size.width = CGRectGetWidth(self.bounds); | |
| if (buttons.count > 0) { | |
| actionsFrame.size.height = actionSize.height; | |
| } | |
| self.actionsScrollView.contentSize = actionsFrame.size; | |
| for (MDCButton *button in buttons) { | |
| [button sizeToFit]; | |
| CGRect buttonFrame = button.frame; | |
| button.frame = CGRectMake(buttonFrame.origin.x, buttonFrame.origin.y, | |
| MAX(MDCDialogActionMinTouchTarget, CGRectGetWidth(buttonFrame)), | |
| MAX(MDCDialogActionMinTouchTarget, CGRectGetHeight(buttonFrame))); | |
| } | |
| if (self.isVerticalActionsLayout) { | |
| [self layoutVerticalButtons:buttons]; | |
| } else { | |
| [self layoutHorizontalButtons:buttons actionSize:actionSize]; | |
| } | |
| // Place scrollviews | |
| CGRect contentScrollViewRect = CGRectZero; | |
| contentScrollViewRect.size = self.contentScrollView.contentSize; | |
| contentScrollViewRect.origin.y = CGRectGetMaxY(titleFrame) + [self titleInsetBottom]; | |
| CGRect actionsScrollViewRect = CGRectZero; | |
| actionsScrollViewRect.size = self.actionsScrollView.contentSize; | |
| actionsScrollViewRect.origin.y = CGRectGetHeight(self.bounds) - actionsScrollViewRect.size.height; | |
| const CGFloat requestedHeight = contentScrollViewRect.origin.y + | |
| self.contentScrollView.contentSize.height + | |
| self.actionsScrollView.contentSize.height; | |
| // Check the layout: do both content and actions fit on the screen at once? | |
| if (requestedHeight > CGRectGetHeight(self.bounds)) { | |
| // Complex layout case : Split the space between the two scrollviews. | |
| CGFloat maxActionsHeight = CGRectGetHeight(self.bounds) / 2.0f; | |
| if (CGRectGetHeight(contentScrollViewRect) < maxActionsHeight) { | |
| maxActionsHeight = | |
| MIN(maxActionsHeight, CGRectGetHeight(self.bounds) - contentScrollViewRect.size.height); | |
| } | |
| actionsScrollViewRect.size.height = MIN(maxActionsHeight, actionsScrollViewRect.size.height); | |
| contentScrollViewRect.size.height = | |
| MAX(0.f, actionsScrollViewRect.origin.y - contentScrollViewRect.origin.y); | |
| self.messageTextView.accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates( | |
| CGRectMake(messageFrame.origin.x, contentScrollViewRect.origin.y, messageFrame.size.width, | |
| MIN(contentScrollViewRect.size.height, messageFrame.size.height)), | |
| self); | |
| } | |
| self.actionsScrollView.frame = actionsScrollViewRect; | |
| self.contentScrollView.frame = contentScrollViewRect; | |
| } | |
| - (void)layoutHorizontalButtons:(NSArray<MDCButton *> *)buttons actionSize:(CGSize)actionSize { | |
| UIEdgeInsets actionsInsets = [self actionsInsetsForButtons:buttons isHorizontalLayout:YES]; | |
| CGFloat maxButtonWidth = | |
| self.actionsScrollView.contentSize.width - (actionsInsets.left + actionsInsets.right); | |
| CGPoint buttonOrigin = CGPointZero; | |
| CGFloat buttonWidth = 0.f; | |
| CGFloat multiplier = | |
| self.actionsHorizontalAlignment == MDCContentHorizontalAlignmentLeading ? 1.f : -1.f; | |
| if (self.actionsHorizontalAlignment == MDCContentHorizontalAlignmentLeading) { | |
| buttonOrigin.x = actionsInsets.left; | |
| } else if (self.actionsHorizontalAlignment == MDCContentHorizontalAlignmentCenter) { | |
| CGFloat actionWidthNoInsets = actionSize.width - actionsInsets.left - actionsInsets.right; | |
| buttonOrigin.x = ((self.actionsScrollView.contentSize.width - actionWidthNoInsets) / 2.f) + | |
| actionWidthNoInsets; | |
| } else { // trailing or justified | |
| buttonOrigin.x = self.actionsScrollView.contentSize.width - actionsInsets.right; | |
| } | |
| buttonOrigin.y = actionsInsets.top; | |
| for (MDCButton *button in buttons) { | |
| CGRect buttonRect = button.frame; | |
| buttonWidth = buttonRect.size.width; | |
| if (self.actionsHorizontalAlignment == MDCContentHorizontalAlignmentJustified) { | |
| if (buttons.count > 1) { | |
| CGFloat totalMargin = self.actionsHorizontalMargin * (buttons.count - 1); | |
| buttonWidth = (maxButtonWidth - totalMargin) / buttons.count; | |
| } else { | |
| buttonWidth = maxButtonWidth; | |
| } | |
| // Adjust minimumSize based on maxButtonWidth to increase the visible area of button. | |
| button.minimumSize = CGSizeMake(MAX(buttonWidth, MDCDialogActionButtonMinimumWidth), | |
| button.minimumSize.height); | |
| } | |
| if (self.actionsHorizontalAlignment != MDCContentHorizontalAlignmentLeading) { | |
| buttonOrigin.x += multiplier * buttonWidth; | |
| } | |
| buttonRect.origin = buttonOrigin; | |
| buttonRect.size.width = buttonWidth; | |
| button.frame = buttonRect; | |
| if (button != buttons.lastObject) { | |
| if (self.actionsHorizontalAlignment == MDCContentHorizontalAlignmentLeading) { | |
| buttonOrigin.x += multiplier * (buttonWidth + self.actionsHorizontalMargin); | |
| } else { | |
| buttonOrigin.x += multiplier * self.actionsHorizontalMargin; | |
| } | |
| } | |
| } | |
| // Handle RTL | |
| if (self.mdf_effectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft) { | |
| for (UIButton *button in buttons) { | |
| CGRect flippedRect = MDFRectFlippedHorizontally(button.frame, CGRectGetWidth(self.bounds)); | |
| button.frame = flippedRect; | |
| } | |
| } | |
| } | |
| - (void)layoutVerticalButtons:(NSArray<MDCButton *> *)buttons { | |
| UIEdgeInsets actionsInsets = [self actionsInsetsForButtons:buttons isHorizontalLayout:NO]; | |
| CGFloat maxButtonWidth = | |
| self.actionsScrollView.contentSize.width - (actionsInsets.left + actionsInsets.right); | |
| CGFloat multiplier = self.orderVerticalActionsByEmphasis ? 1.f : -1.f; | |
| CGPoint buttonCenter; | |
| CGPoint buttonOrigin; | |
| buttonOrigin.y = self.orderVerticalActionsByEmphasis | |
| ? actionsInsets.top | |
| : self.actionsScrollView.contentSize.height - actionsInsets.top; | |
| if (self.actionsHorizontalAlignmentInVerticalLayout == MDCContentHorizontalAlignmentCenter || | |
| self.actionsHorizontalAlignmentInVerticalLayout == MDCContentHorizontalAlignmentJustified) { | |
| buttonCenter.x = self.actionsScrollView.contentSize.width / 2.0f; | |
| buttonCenter.y = buttonOrigin.y; | |
| for (NSUInteger index = 0; index < buttons.count; ++index) { | |
| MDCButton *button = buttons[index]; | |
| CGRect buttonRect = button.bounds; | |
| if (CGRectGetWidth(buttonRect) > maxButtonWidth || | |
| self.actionsHorizontalAlignmentInVerticalLayout == | |
| MDCContentHorizontalAlignmentJustified) { | |
| buttonRect.size.width = maxButtonWidth; | |
| button.bounds = buttonRect; | |
| // Adjust minimumSize based on maxButtonWidth to increase the visible area of button. | |
| button.minimumSize = CGSizeMake(MAX(maxButtonWidth, MDCDialogActionButtonMinimumWidth), | |
| button.minimumSize.height); | |
| } | |
| buttonCenter.y += multiplier * (buttonRect.size.height / 2.0f); | |
| button.center = buttonCenter; | |
| if (button != buttons.lastObject) { | |
| buttonCenter.y += multiplier * (buttonRect.size.height / 2.0f); | |
| MDCButton *nextButton = buttons[index + 1]; | |
| CGFloat verticalMargin = [self actionsVerticalMarginBetweenTopButton:button | |
| bottomButton:nextButton]; | |
| buttonCenter.y += multiplier * verticalMargin; | |
| } | |
| } | |
| } else { // Leading/Trailing alignment. | |
| for (NSUInteger index = 0; index < buttons.count; ++index) { | |
| MDCButton *button = buttons[index]; | |
| CGRect buttonRect = button.bounds; | |
| buttonOrigin.x = actionsInsets.left; | |
| if (self.actionsHorizontalAlignmentInVerticalLayout == | |
| MDCContentHorizontalAlignmentTrailing) { | |
| buttonOrigin.x = | |
| self.actionsScrollView.contentSize.width - buttonRect.size.width - actionsInsets.right; | |
| } | |
| buttonRect.origin = buttonOrigin; | |
| button.frame = buttonRect; | |
| if (button != buttons.lastObject) { | |
| MDCButton *nextButton = buttons[index + 1]; | |
| CGFloat verticalMargin = [self actionsVerticalMarginBetweenTopButton:button | |
| bottomButton:nextButton]; | |
| buttonOrigin.y += multiplier * (buttonRect.size.height + verticalMargin); | |
| } | |
| } | |
| // Handle RTL | |
| if (self.mdf_effectiveUserInterfaceLayoutDirection == | |
| UIUserInterfaceLayoutDirectionRightToLeft) { | |
| for (UIButton *button in buttons) { | |
| CGRect flippedRect = MDFRectFlippedHorizontally(button.frame, CGRectGetWidth(self.bounds)); | |
| button.frame = flippedRect; | |
| } | |
| } | |
| } | |
| } | |
| #pragma mark - Dynamic Type | |
| - (BOOL)mdc_adjustsFontForContentSizeCategory { | |
| return _mdc_adjustsFontForContentSizeCategory; | |
| } | |
| - (void)mdc_setAdjustsFontForContentSizeCategory:(BOOL)adjusts { | |
| _mdc_adjustsFontForContentSizeCategory = adjusts; | |
| for (MDCButton *button in self.actionManager.buttonsInActionOrder) { | |
| 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 | |
| @implementation MDCNonselectableTextView | |
| #pragma mark - UITextView | |
| - (void)setAttributedText:(NSAttributedString *)attributedText { | |
| if ([self.attributedText isEqual:attributedText]) { | |
| return; | |
| } | |
| [super setAttributedText:attributedText]; | |
| [self updateTopInsetAndTextContainerInset:self.textContainerInset]; | |
| } | |
| - (void)setFont:(UIFont *)font { | |
| if ([self.font isEqual:font]) { | |
| return; | |
| } | |
| [super setFont:font]; | |
| [self updateTopInsetAndTextContainerInset:self.textContainerInset]; | |
| } | |
| - (void)setText:(NSString *)text { | |
| if ([self.text isEqual:text]) { | |
| return; | |
| } | |
| [super setText:text]; | |
| [self updateTopInsetAndTextContainerInset:self.textContainerInset]; | |
| } | |
| - (void)setTextContainerInset:(UIEdgeInsets)textContainerInset { | |
| [self updateTopInsetAndTextContainerInset:textContainerInset]; | |
| } | |
| #pragma mark - UIView | |
| // Disabling text selection when selectable is YES, while allowing gestures for inlined links. | |
| - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { | |
| if (UIAccessibilityIsVoiceOverRunning()) { | |
| return [super pointInside:point withEvent:event]; | |
| } | |
| if (![super pointInside:point withEvent:event]) { | |
| return NO; | |
| } | |
| UITextPosition *position = [self closestPositionToPoint:point]; | |
| if (!position) { | |
| return NO; | |
| } | |
| UITextRange *range = [self.tokenizer rangeEnclosingPosition:position | |
| withGranularity:UITextGranularityCharacter | |
| inDirection:UITextLayoutDirectionLeft]; | |
| if (!range) { | |
| return NO; | |
| } | |
| NSInteger offset = [self offsetFromPosition:self.beginningOfDocument toPosition:range.start]; | |
| id link = [self.attributedText attribute:NSLinkAttributeName atIndex:offset effectiveRange:nil]; | |
| return link != nil; | |
| } | |
| #pragma mark - Private | |
| /** | |
| * Updates the top text inset of the UITextView to avoid rendering text outside the view | |
| * boundary. We need to calculate the text margin when we change the text or the text font. | |
| * | |
| * @note Our title UILabel already has a top margin, so its text won't exceed the parent view's | |
| * boundary. | |
| * | |
| * @param textContainerInset The target text insets to display. It will update the following text | |
| * margin: left, right and bottom. | |
| */ | |
| - (void)updateTopInsetAndTextContainerInset:(UIEdgeInsets)textContainerInset { | |
| // To avoid changing the top margin when the view width changes, we only compare the text height | |
| // for a single line text. | |
| UIFont *font = self.font; | |
| CGFloat singleLineTextViewHeight = SingleLineTextViewHeight(self.text, font); | |
| textContainerInset.top = MAX(0.0f, singleLineTextViewHeight - font.lineHeight); | |
| if (!UIEdgeInsetsEqualToEdgeInsets(super.textContainerInset, textContainerInset)) { | |
| // Note that |self setTextContainerInset:| will call this method. | |
| super.textContainerInset = textContainerInset; | |
| } | |
| } | |
| @end |