| // 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 "MDCBannerView.h" |
| #import <UIKit/UIKit.h> |
| #import "MDCButton.h" |
| #import "M3CButton.h" |
| |
| NS_ASSUME_NONNULL_BEGIN |
| |
| static const NSInteger kTextNumberOfLineLimit = 3; |
| static const CGFloat kImageViewSideLength = 40; |
| static const CGFloat kLeadingPadding = 16.0f; |
| static const CGFloat kTextTrailingPadding = 16.0f; |
| static const CGFloat kButtonContainerTrailingPadding = 8.0f; |
| static const CGFloat kButtonContainerTrailingPaddingWithM3CButton = 16.0f; |
| static const CGFloat kTopPaddingSmall = 10.0f; |
| static const CGFloat kTopPaddingLarge = 24.0f; |
| static const CGFloat kBottomPadding = 8.0f; |
| static const CGFloat kButtonHorizontalIntervalSpace = 8.0f; |
| static const CGFloat kButtonVerticalIntervalSpace = 8.0f; |
| static const CGFloat kSpaceBetweenIconImageAndTextView = 16.0f; |
| static const CGFloat kHorizontalSpaceBetweenTextViewAndButton = 24.0f; |
| static const CGFloat kVerticalSpaceBetweenButtonAndTextView = 12.0f; |
| static const CGFloat kVerticalSpaceBetweenButtonAndTextViewWithM3CButton = 8.0f; |
| static const CGFloat kDividerDefaultOpacity = 0.12f; |
| static const CGFloat kDividerDefaultHeight = 1.0f; |
| static const CGFloat kTextDefaultOpacity = 0.87f; |
| static NSString *const kMDCBannerViewImageViewImageKeyPath = @"image"; |
| |
| @interface MDCBannerView () |
| |
| @property(nonatomic, readwrite, strong) UIView *contentView; |
| |
| @property(nonatomic, readwrite, strong) UITextView *textView; |
| |
| @property(nonatomic, readwrite, strong) UIImageView *imageView; |
| |
| @property(nonatomic, readwrite, strong) MDCButton *leadingButton; |
| @property(nonatomic, readwrite, strong) MDCButton *trailingButton; |
| @property(nonatomic, readwrite, strong) M3CButton *leadingM3CButton; |
| @property(nonatomic, readwrite, strong) M3CButton *trailingM3CButton; |
| @property(nonatomic, readwrite, strong) UIView *buttonContainerView; |
| @property(nonatomic, readonly, strong) UIButton *currentLeadingButton; |
| @property(nonatomic, readonly, strong) UIButton *currentTrailingButton; |
| |
| @property(nonatomic, readwrite, strong) UIView *divider; |
| @property(nonatomic, readwrite, assign) CGFloat dividerHeight; |
| |
| // Content constraints |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *contentViewConstraintTop; |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *contentViewConstraintBottom; |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *contentViewConstraintLeft; |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *contentViewConstraintRight; |
| |
| // Image constraints |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *imageViewConstraintLeading; |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *imageViewConstraintCenterY; |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *imageViewConstraintTopLarge; |
| |
| // Text View constraints |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *textViewConstraintLeadingWithMargin; |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *textViewConstraintLeadingWithImage; |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *textViewConstraintTrailing; |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *textViewConstraintTop; |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *textViewConstraintCenterY; |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *textViewConstraintHeight; |
| // Buttons constraints |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *buttonContainerConstraintLeading; |
| @property(nonatomic, readwrite, strong) |
| NSLayoutConstraint *buttonContainerConstraintWidthWithLeadingButton; |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint |
| *buttonContainerConstraintLeadingWithTextView; // The horizontal constraint between button |
| // container and text view. |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *buttonContainerConstraintTrailing; |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *buttonContainerConstraintTopWithMargin; |
| @property(nonatomic, readwrite, strong) |
| NSLayoutConstraint *buttonContainerConstraintTopWithImageViewGreater; |
| @property(nonatomic, readwrite, strong) |
| NSLayoutConstraint *buttonContainerConstraintTopWithTextView; |
| @property(nonatomic, readwrite, strong) |
| NSLayoutConstraint *buttonContainerConstraintTopWithTextViewGreater; |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *buttonContainerConstraintBottom; |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *buttonContainerConstraintHeight; |
| |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *leadingButtonConstraintLeading; |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *leadingButtonConstraintTop; |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *leadingButtonConstraintTrailing; |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *leadingButtonConstraintCenterY; |
| @property(nonatomic, readwrite, strong) |
| NSLayoutConstraint *leadingButtonConstraintBaseLineWithTrailingButton; |
| @property(nonatomic, readwrite, strong) |
| NSLayoutConstraint *leadingButtonConstraintTrailingWithTrailingButton; |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *leadingButtonConstraintHeightZero; |
| |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *trailingButtonConstraintBottom; |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *trailingButtonConstraintTop; |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *trailingButtonConstraintTrailing; |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *trailingButtonConstraintLeading; |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *trailingButtonConstraintHeightZero; |
| |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *dividerConstraintHeight; |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *dividerConstraintBottom; |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *dividerConstraintLeading; |
| @property(nonatomic, readwrite, strong) NSLayoutConstraint *dividerConstraintWidth; |
| |
| @end |
| |
| @implementation MDCBannerView |
| |
| @synthesize mdc_elevationDidChangeBlock = _mdc_elevationDidChangeBlock; |
| @synthesize mdc_overrideBaseElevation = _mdc_overrideBaseElevation; |
| @synthesize isM3CButtonEnabled = _isM3CButtonEnabled; |
| |
| - (instancetype)initForM3 { |
| self = [super initWithFrame:CGRectZero]; |
| if (self) { |
| _isM3CButtonEnabled = YES; |
| [self commonBannerViewInit]; |
| } |
| return self; |
| } |
| |
| - (instancetype)initWithFrame:(CGRect)frame { |
| self = [super initWithFrame:frame]; |
| if (self) { |
| _isM3CButtonEnabled = NO; |
| [self commonBannerViewInit]; |
| } |
| return self; |
| } |
| |
| - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { |
| self = [super initWithCoder:aDecoder]; |
| if (self) { |
| _isM3CButtonEnabled = NO; |
| [self commonBannerViewInit]; |
| } |
| return self; |
| } |
| |
| - (void)commonBannerViewInit { |
| self.backgroundColor = UIColor.whiteColor; |
| _bannerViewLayoutStyle = MDCBannerViewLayoutStyleAutomatic; |
| self.layoutMargins = UIEdgeInsetsZero; |
| self.contentEdgeInsets = UIEdgeInsetsZero; |
| self.adjustsFontForContentSizeCategory = NO; |
| self.numberOfLinesInTextView = 0; |
| |
| UIView *contentView = [[UIView alloc] init]; |
| contentView.translatesAutoresizingMaskIntoConstraints = NO; |
| _contentView = contentView; |
| [self addSubview:contentView]; |
| |
| // Create textView |
| UITextView *textView = [[UITextView alloc] init]; |
| textView.translatesAutoresizingMaskIntoConstraints = NO; |
| textView.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium]; |
| textView.textColor = UIColor.blackColor; |
| textView.alpha = kTextDefaultOpacity; |
| textView.textContainer.maximumNumberOfLines = kTextNumberOfLineLimit; |
| textView.textContainer.lineBreakMode = NSLineBreakByTruncatingTail; |
| textView.textContainer.lineFragmentPadding = 0; |
| textView.scrollEnabled = NO; |
| textView.editable = NO; |
| textView.textAlignment = NSTextAlignmentNatural; |
| textView.textContainerInset = UIEdgeInsetsZero; |
| textView.backgroundColor = UIColor.clearColor; |
| [contentView addSubview:textView]; |
| _textView = textView; |
| |
| // Create imageView |
| UIImageView *imageView = [[UIImageView alloc] init]; |
| imageView.translatesAutoresizingMaskIntoConstraints = NO; |
| [imageView.widthAnchor constraintEqualToConstant:kImageViewSideLength].active = YES; |
| [imageView.heightAnchor constraintEqualToConstant:kImageViewSideLength].active = YES; |
| imageView.contentMode = UIViewContentModeCenter; |
| imageView.clipsToBounds = YES; |
| imageView.hidden = YES; |
| [contentView addSubview:imageView]; |
| _imageView = imageView; |
| |
| // Create a button container to organize buttons |
| UIView *buttonContainerView = [[UIView alloc] init]; |
| buttonContainerView.translatesAutoresizingMaskIntoConstraints = NO; |
| [contentView addSubview:buttonContainerView]; |
| self.buttonContainerView = buttonContainerView; |
| |
| // Create leadingButton and trailingButton |
| MDCButton *leadingButton = [[MDCButton alloc] init]; |
| leadingButton.translatesAutoresizingMaskIntoConstraints = NO; |
| leadingButton.backgroundColor = UIColor.whiteColor; |
| _leadingButton = leadingButton; |
| |
| MDCButton *trailingButton = [[MDCButton alloc] init]; |
| trailingButton.translatesAutoresizingMaskIntoConstraints = NO; |
| trailingButton.backgroundColor = UIColor.whiteColor; |
| _trailingButton = trailingButton; |
| |
| // Create leadingM3CButton and trailingM3CButton |
| M3CButton *leadingM3CButton = [[M3CButton alloc] init]; |
| leadingM3CButton.translatesAutoresizingMaskIntoConstraints = NO; |
| leadingM3CButton.backgroundColor = UIColor.whiteColor; |
| _leadingM3CButton = leadingM3CButton; |
| |
| M3CButton *trailingM3CButton = [[M3CButton alloc] init]; |
| trailingM3CButton.translatesAutoresizingMaskIntoConstraints = NO; |
| trailingM3CButton.backgroundColor = UIColor.whiteColor; |
| _trailingM3CButton = trailingM3CButton; |
| |
| // Add the appropriate set of buttons to the container view. |
| if (_isM3CButtonEnabled) { |
| _currentLeadingButton = leadingM3CButton; |
| _currentTrailingButton = trailingM3CButton; |
| [buttonContainerView addSubview:leadingM3CButton]; |
| [buttonContainerView addSubview:trailingM3CButton]; |
| } else { |
| _currentLeadingButton = leadingButton; |
| _currentTrailingButton = trailingButton; |
| [buttonContainerView addSubview:leadingButton]; |
| [buttonContainerView addSubview:trailingButton]; |
| } |
| |
| // Create Divider |
| UIView *divider = [[UIView alloc] init]; |
| divider.translatesAutoresizingMaskIntoConstraints = NO; |
| divider.backgroundColor = [UIColor.blackColor colorWithAlphaComponent:kDividerDefaultOpacity]; |
| _dividerHeight = kDividerDefaultHeight; |
| [self addSubview:divider]; |
| _divider = divider; |
| |
| [self setupConstraints]; |
| |
| _mdc_overrideBaseElevation = -1; |
| } |
| |
| - (void)setDividerColor:(UIColor *)dividerColor { |
| self.divider.backgroundColor = dividerColor; |
| } |
| |
| - (UIColor *)dividerColor { |
| return self.divider.backgroundColor; |
| } |
| |
| - (void)setNumberOfLinesInTextView:(NSUInteger)numberOfLinesInTextView { |
| _numberOfLinesInTextView = numberOfLinesInTextView; |
| if (_numberOfLinesInTextView == 0) { |
| self.textView.textContainer.maximumNumberOfLines = kTextNumberOfLineLimit; |
| self.textView.textContainer.lineBreakMode = NSLineBreakByTruncatingTail; |
| } else { |
| self.textView.textContainer.maximumNumberOfLines = 0; |
| self.textView.textContainer.lineBreakMode = NSLineBreakByWordWrapping; |
| } |
| } |
| |
| - (void)setContentEdgeInsets:(UIEdgeInsets)contentEdgeInsets { |
| _contentEdgeInsets = contentEdgeInsets; |
| |
| self.contentViewConstraintBottom.constant = -contentEdgeInsets.bottom; |
| self.contentViewConstraintTop.constant = contentEdgeInsets.top; |
| self.contentViewConstraintLeft.constant = contentEdgeInsets.left; |
| self.contentViewConstraintRight.constant = -contentEdgeInsets.right; |
| } |
| |
| - (void)setAdjustsFontForContentSizeCategory:(BOOL)adjustsFontForContentSizeCategory { |
| _adjustsFontForContentSizeCategory = adjustsFontForContentSizeCategory; |
| self.textView.adjustsFontForContentSizeCategory = adjustsFontForContentSizeCategory; |
| self.currentLeadingButton.titleLabel.adjustsFontForContentSizeCategory = |
| adjustsFontForContentSizeCategory; |
| self.currentTrailingButton.titleLabel.adjustsFontForContentSizeCategory = |
| adjustsFontForContentSizeCategory; |
| [self setNeedsUpdateConstraints]; |
| } |
| |
| - (CGFloat)mdc_currentElevation { |
| return 0; |
| } |
| |
| #pragma mark - Constraints Helpers |
| |
| - (void)setupConstraints { |
| [self setUpContentConstraints]; |
| [self setUpImageViewConstraints]; |
| [self setUpTextViewConstraints]; |
| [self setUpButtonContainerConstraints]; |
| [self setUpButtonsConstraints]; |
| [self setUpDividerConstraints]; |
| } |
| |
| - (void)setUpContentConstraints { |
| self.contentViewConstraintLeft = |
| [self.contentView.leftAnchor constraintEqualToAnchor:self.layoutMarginsGuide.leftAnchor |
| constant:self.contentEdgeInsets.left]; |
| self.contentViewConstraintRight = |
| [self.contentView.rightAnchor constraintEqualToAnchor:self.layoutMarginsGuide.rightAnchor |
| constant:-self.contentEdgeInsets.right]; |
| self.contentViewConstraintTop = |
| [self.contentView.topAnchor constraintEqualToAnchor:self.layoutMarginsGuide.topAnchor |
| constant:self.contentEdgeInsets.top]; |
| self.contentViewConstraintBottom = |
| [self.contentView.bottomAnchor constraintEqualToAnchor:self.layoutMarginsGuide.bottomAnchor |
| constant:-self.contentEdgeInsets.bottom]; |
| } |
| |
| - (void)setUpImageViewConstraints { |
| self.imageViewConstraintLeading = |
| [self.imageView.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor |
| constant:kLeadingPadding]; |
| self.imageViewConstraintTopLarge = |
| [self.imageView.topAnchor constraintEqualToAnchor:self.contentView.topAnchor |
| constant:kTopPaddingLarge]; |
| self.imageViewConstraintCenterY = |
| [self.imageView.centerYAnchor constraintEqualToAnchor:self.buttonContainerView.centerYAnchor]; |
| } |
| |
| - (void)setUpTextViewConstraints { |
| self.textViewConstraintTop = |
| [self.textView.topAnchor constraintEqualToAnchor:self.contentView.topAnchor |
| constant:kTopPaddingLarge]; |
| self.textViewConstraintCenterY = |
| [self.textView.centerYAnchor constraintEqualToAnchor:self.buttonContainerView.centerYAnchor]; |
| self.textViewConstraintTrailing = |
| [self.textView.trailingAnchor constraintEqualToAnchor:self.contentView.trailingAnchor |
| constant:-kTextTrailingPadding]; |
| self.textViewConstraintLeadingWithImage = |
| [self.textView.leadingAnchor constraintEqualToAnchor:self.imageView.trailingAnchor |
| constant:kSpaceBetweenIconImageAndTextView]; |
| self.textViewConstraintLeadingWithMargin = |
| [self.textView.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor |
| constant:kLeadingPadding]; |
| self.textViewConstraintHeight = [self.textView.heightAnchor constraintEqualToConstant:0.f]; |
| } |
| |
| - (void)setUpButtonContainerConstraints { |
| UIButton *leadingButton = self.currentLeadingButton; |
| CGFloat verticalSpace = _isM3CButtonEnabled ? kVerticalSpaceBetweenButtonAndTextViewWithM3CButton |
| : kVerticalSpaceBetweenButtonAndTextView; |
| CGFloat trailingPadding = _isM3CButtonEnabled ? kButtonContainerTrailingPaddingWithM3CButton |
| : kButtonContainerTrailingPadding; |
| |
| self.buttonContainerConstraintLeading = |
| [self.buttonContainerView.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor |
| constant:kLeadingPadding]; |
| self.buttonContainerConstraintWidthWithLeadingButton = |
| [self.buttonContainerView.widthAnchor constraintEqualToAnchor:leadingButton.widthAnchor]; |
| self.buttonContainerConstraintTrailing = [self.buttonContainerView.trailingAnchor |
| constraintEqualToAnchor:self.contentView.trailingAnchor |
| constant:-trailingPadding]; |
| self.buttonContainerConstraintBottom = |
| [self.buttonContainerView.bottomAnchor constraintEqualToAnchor:self.contentView.bottomAnchor |
| constant:-kBottomPadding]; |
| self.buttonContainerConstraintLeadingWithTextView = [self.buttonContainerView.leadingAnchor |
| constraintEqualToAnchor:self.textView.trailingAnchor |
| constant:kHorizontalSpaceBetweenTextViewAndButton]; |
| self.buttonContainerConstraintTopWithMargin = |
| [self.buttonContainerView.topAnchor constraintEqualToAnchor:self.contentView.topAnchor |
| constant:kTopPaddingSmall]; |
| self.buttonContainerConstraintTopWithImageViewGreater = [self.buttonContainerView.topAnchor |
| constraintGreaterThanOrEqualToAnchor:self.imageView.bottomAnchor |
| constant:verticalSpace]; |
| self.buttonContainerConstraintTopWithTextView = |
| [self.buttonContainerView.topAnchor constraintEqualToAnchor:self.textView.bottomAnchor |
| constant:verticalSpace]; |
| self.buttonContainerConstraintTopWithTextView.priority = UILayoutPriorityDefaultLow; |
| self.buttonContainerConstraintTopWithTextViewGreater = [self.buttonContainerView.topAnchor |
| constraintGreaterThanOrEqualToAnchor:self.textView.bottomAnchor |
| constant:verticalSpace]; |
| self.buttonContainerConstraintHeight = [self.buttonContainerView.heightAnchor |
| constraintGreaterThanOrEqualToAnchor:leadingButton.heightAnchor |
| constant:0]; |
| } |
| |
| - (void)setUpButtonsConstraints { |
| UIButton *leadingButton = self.currentLeadingButton; |
| UIButton *trailingButton = self.currentTrailingButton; |
| |
| self.leadingButtonConstraintLeading = [leadingButton.leadingAnchor |
| constraintGreaterThanOrEqualToAnchor:self.buttonContainerView.leadingAnchor]; |
| self.leadingButtonConstraintTop = |
| [leadingButton.topAnchor constraintEqualToAnchor:self.buttonContainerView.topAnchor]; |
| self.leadingButtonConstraintTrailing = [leadingButton.trailingAnchor |
| constraintEqualToAnchor:self.buttonContainerView.trailingAnchor]; |
| self.leadingButtonConstraintCenterY = |
| [leadingButton.centerYAnchor constraintEqualToAnchor:self.buttonContainerView.centerYAnchor]; |
| self.leadingButtonConstraintBaseLineWithTrailingButton = |
| [leadingButton.lastBaselineAnchor constraintEqualToAnchor:trailingButton.lastBaselineAnchor]; |
| self.leadingButtonConstraintTrailingWithTrailingButton = |
| [leadingButton.trailingAnchor constraintEqualToAnchor:trailingButton.leadingAnchor |
| constant:-kButtonHorizontalIntervalSpace]; |
| [leadingButton setContentCompressionResistancePriority:UILayoutPriorityRequired |
| forAxis:UILayoutConstraintAxisHorizontal]; |
| [leadingButton setContentHuggingPriority:UILayoutPriorityRequired |
| forAxis:UILayoutConstraintAxisHorizontal]; |
| self.leadingButtonConstraintHeightZero = |
| [leadingButton.heightAnchor constraintEqualToConstant:0.f]; |
| self.trailingButtonConstraintBottom = |
| [trailingButton.bottomAnchor constraintEqualToAnchor:self.buttonContainerView.bottomAnchor]; |
| self.trailingButtonConstraintTop = |
| [trailingButton.topAnchor constraintEqualToAnchor:leadingButton.bottomAnchor |
| constant:kButtonVerticalIntervalSpace]; |
| self.trailingButtonConstraintTrailing = [trailingButton.trailingAnchor |
| constraintEqualToAnchor:self.buttonContainerView.trailingAnchor]; |
| self.trailingButtonConstraintLeading = [trailingButton.leadingAnchor |
| constraintGreaterThanOrEqualToAnchor:self.buttonContainerView.leadingAnchor]; |
| [trailingButton setContentCompressionResistancePriority:UILayoutPriorityRequired |
| forAxis:UILayoutConstraintAxisHorizontal]; |
| [trailingButton setContentHuggingPriority:UILayoutPriorityRequired |
| forAxis:UILayoutConstraintAxisHorizontal]; |
| self.trailingButtonConstraintHeightZero = |
| [trailingButton.heightAnchor constraintEqualToConstant:0.f]; |
| } |
| |
| - (void)setUpDividerConstraints { |
| self.dividerConstraintBottom = |
| [self.divider.bottomAnchor constraintEqualToAnchor:self.bottomAnchor]; |
| self.dividerConstraintHeight = |
| [self.divider.heightAnchor constraintEqualToConstant:self.dividerHeight]; |
| self.dividerConstraintWidth = [self.divider.widthAnchor constraintEqualToAnchor:self.widthAnchor]; |
| self.dividerConstraintLeading = |
| [self.divider.leadingAnchor constraintEqualToAnchor:self.leadingAnchor]; |
| } |
| |
| - (void)deactivateAllConstraints { |
| self.contentViewConstraintBottom.active = NO; |
| self.contentViewConstraintTop.active = NO; |
| self.contentViewConstraintLeft.active = NO; |
| self.contentViewConstraintRight.active = NO; |
| self.imageViewConstraintLeading.active = NO; |
| self.imageViewConstraintTopLarge.active = NO; |
| self.imageViewConstraintCenterY.active = NO; |
| self.textViewConstraintLeadingWithMargin.active = NO; |
| self.textViewConstraintLeadingWithImage.active = NO; |
| self.textViewConstraintTrailing.active = NO; |
| self.textViewConstraintTop.active = NO; |
| self.textViewConstraintCenterY.active = NO; |
| self.textViewConstraintHeight.active = NO; |
| self.buttonContainerConstraintLeading.active = NO; |
| self.buttonContainerConstraintWidthWithLeadingButton.active = NO; |
| self.buttonContainerConstraintLeadingWithTextView.active = NO; |
| self.buttonContainerConstraintTrailing.active = NO; |
| self.buttonContainerConstraintTopWithMargin.active = NO; |
| self.buttonContainerConstraintTopWithImageViewGreater.active = NO; |
| self.buttonContainerConstraintTopWithTextView.active = NO; |
| self.buttonContainerConstraintTopWithTextViewGreater.active = NO; |
| self.buttonContainerConstraintBottom.active = NO; |
| self.buttonContainerConstraintHeight.active = NO; |
| self.leadingButtonConstraintLeading.active = NO; |
| self.leadingButtonConstraintTop.active = NO; |
| self.leadingButtonConstraintTrailing.active = NO; |
| self.leadingButtonConstraintCenterY.active = NO; |
| self.leadingButtonConstraintBaseLineWithTrailingButton.active = NO; |
| self.leadingButtonConstraintTrailingWithTrailingButton.active = NO; |
| self.leadingButtonConstraintHeightZero.active = NO; |
| self.trailingButtonConstraintBottom.active = NO; |
| self.trailingButtonConstraintTop.active = NO; |
| self.trailingButtonConstraintTrailing.active = NO; |
| self.trailingButtonConstraintLeading.active = NO; |
| self.trailingButtonConstraintHeightZero.active = NO; |
| self.dividerConstraintBottom.active = NO; |
| self.dividerConstraintHeight.active = NO; |
| self.dividerConstraintLeading.active = NO; |
| self.dividerConstraintWidth.active = NO; |
| self.leadingButton.layoutTitleWithConstraints = NO; |
| self.trailingButton.layoutTitleWithConstraints = NO; |
| } |
| |
| #pragma mark - UIView overrides |
| |
| - (void)setFrame:(CGRect)frame { |
| [super setFrame:frame]; |
| |
| [self deactivateAllConstraints]; |
| [self setNeedsUpdateConstraints]; |
| } |
| |
| - (CGSize)sizeThatFits:(CGSize)size { |
| UIButton *leadingButton = self.currentLeadingButton; |
| UIButton *trailingButton = self.currentTrailingButton; |
| |
| MDCBannerViewLayoutStyle layoutStyle = [self layoutStyleForSizeToFit:size]; |
| CGFloat frameHeight = self.contentEdgeInsets.top + self.contentEdgeInsets.bottom; |
| CGSize contentSize = [self contentSizeForLayoutSize:size]; |
| switch (layoutStyle) { |
| case MDCBannerViewLayoutStyleSingleRow: { |
| frameHeight += kTopPaddingSmall + kBottomPadding; |
| CGFloat widthLimit = contentSize.width; |
| if (!leadingButton.hidden) { |
| [leadingButton sizeToFit]; |
| CGFloat buttonWidth = CGRectGetWidth(leadingButton.frame); |
| widthLimit -= (buttonWidth + kHorizontalSpaceBetweenTextViewAndButton); |
| } |
| if (!self.imageView.hidden) { |
| widthLimit -= kImageViewSideLength; |
| widthLimit -= kSpaceBetweenIconImageAndTextView; |
| } |
| CGFloat maximumHeight = [self getDisplayedTextViewHeight:widthLimit]; |
| if (!leadingButton.hidden) { |
| CGSize leadingButtonSize = [leadingButton sizeThatFits:CGSizeZero]; |
| maximumHeight = MAX(leadingButtonSize.height, maximumHeight); |
| } |
| if (!self.imageView.hidden) { |
| maximumHeight = MAX(kImageViewSideLength, maximumHeight); |
| } |
| frameHeight += maximumHeight; |
| break; |
| } |
| case MDCBannerViewLayoutStyleMultiRowAlignedButton: { |
| frameHeight += kTopPaddingLarge + kBottomPadding; |
| frameHeight += [self getFrameHeightOfImageViewAndTextViewWithSizeToFit:contentSize]; |
| CGSize leadingButtonSize = |
| leadingButton.hidden ? CGSizeZero : [leadingButton sizeThatFits:CGSizeZero]; |
| CGSize trailingButtonSize = |
| trailingButton.hidden ? CGSizeZero : [trailingButton sizeThatFits:CGSizeZero]; |
| frameHeight += MAX(leadingButtonSize.height, trailingButtonSize.height); |
| break; |
| } |
| case MDCBannerViewLayoutStyleMultiRowStackedButton: { |
| frameHeight += kTopPaddingLarge + kBottomPadding; |
| frameHeight += [self getFrameHeightOfImageViewAndTextViewWithSizeToFit:contentSize]; |
| |
| // Calculate button heights which do not respect the multi-line label size due to |
| // M3CButton's implementation. |
| CGFloat leadingButtonHeight = |
| [leadingButton sizeThatFits:CGSizeMake(contentSize.width, CGFLOAT_MAX)].height; |
| CGFloat trailingButtonHeight = |
| [trailingButton sizeThatFits:CGSizeMake(contentSize.width, CGFLOAT_MAX)].height; |
| |
| // Calculate button height via `titleLabel` to respect the multi-line label size. |
| CGFloat leadingButtonLabelAvailableWidth = contentSize.width - |
| leadingButton.contentEdgeInsets.left - |
| leadingButton.contentEdgeInsets.right; |
| CGFloat trailingButtonLabelAvailableWidth = contentSize.width - |
| trailingButton.contentEdgeInsets.left - |
| trailingButton.contentEdgeInsets.right; |
| |
| leadingButton.titleLabel.preferredMaxLayoutWidth = leadingButtonLabelAvailableWidth; |
| trailingButton.titleLabel.preferredMaxLayoutWidth = trailingButtonLabelAvailableWidth; |
| |
| CGSize leadingButtonLabelIntrinsicContentSize = |
| [leadingButton.titleLabel intrinsicContentSize]; |
| CGSize trailingButtonLabelIntrinsicContentSize = |
| [trailingButton.titleLabel intrinsicContentSize]; |
| |
| CGFloat leadingButtonLabelHeight = leadingButtonLabelIntrinsicContentSize.height + |
| leadingButton.contentEdgeInsets.top + |
| leadingButton.contentEdgeInsets.bottom; |
| CGFloat trailingButtonLabelHeight = trailingButtonLabelIntrinsicContentSize.height + |
| trailingButton.contentEdgeInsets.top + |
| trailingButton.contentEdgeInsets.bottom; |
| |
| // Sometimes the button height is larger than the label height due to minimum touch |
| // target clamping. In this case, we must respect the button height. |
| CGFloat leadingButtonTrueHeight; |
| if (leadingButtonLabelHeight >= leadingButtonHeight) { |
| leadingButtonTrueHeight = leadingButton.hidden ? 0.f : leadingButtonLabelHeight; |
| [self setLeadingButtonLayoutTitleWithConstraints:YES]; |
| } else { |
| leadingButtonTrueHeight = leadingButton.hidden ? 0.f : leadingButtonHeight; |
| [self setLeadingButtonLayoutTitleWithConstraints:NO]; |
| } |
| CGFloat trailingButtonTrueHeight; |
| if (trailingButtonLabelHeight >= trailingButtonHeight) { |
| trailingButtonTrueHeight = trailingButton.hidden ? 0.f : trailingButtonLabelHeight; |
| [self setTrailingButtonLayoutTitleWithConstraints:YES]; |
| } else { |
| trailingButtonTrueHeight = trailingButton.hidden ? 0.f : trailingButtonHeight; |
| [self setTrailingButtonLayoutTitleWithConstraints:NO]; |
| } |
| |
| CGFloat verticalIntervalSpace = kButtonVerticalIntervalSpace; |
| if (leadingButton.hidden || trailingButton.hidden) { |
| verticalIntervalSpace = 0.f; |
| } |
| frameHeight += leadingButtonTrueHeight + trailingButtonTrueHeight + verticalIntervalSpace; |
| break; |
| } |
| default: |
| break; |
| } |
| if (self.showsDivider) { |
| frameHeight += self.dividerHeight; |
| } |
| return CGSizeMake(size.width, frameHeight); |
| } |
| |
| - (void)setLeadingButtonLayoutTitleWithConstraints:(BOOL)layoutTitleWithConstraints { |
| if (_isM3CButtonEnabled) { |
| self.leadingM3CButton.layoutTitleWithConstraints = layoutTitleWithConstraints; |
| } else { |
| self.leadingButton.layoutTitleWithConstraints = layoutTitleWithConstraints; |
| } |
| } |
| |
| - (void)setTrailingButtonLayoutTitleWithConstraints:(BOOL)layoutTitleWithConstraints { |
| if (_isM3CButtonEnabled) { |
| self.trailingM3CButton.layoutTitleWithConstraints = layoutTitleWithConstraints; |
| } else { |
| self.trailingButton.layoutTitleWithConstraints = layoutTitleWithConstraints; |
| } |
| } |
| |
| - (CGSize)intrinsicContentSize { |
| CGFloat intrinsicContentHeight = [self sizeThatFits:self.bounds.size].height; |
| return CGSizeMake(UIViewNoIntrinsicMetric, intrinsicContentHeight); |
| } |
| |
| - (void)updateConstraints { |
| if (!CGSizeEqualToSize(self.bounds.size, CGSizeZero)) { |
| MDCBannerViewLayoutStyle layoutStyle = [self layoutStyleForSizeToFit:self.bounds.size]; |
| [self updateConstraintsWithLayoutStyle:layoutStyle]; |
| } |
| |
| [super updateConstraints]; |
| } |
| |
| - (void)layoutSubviews { |
| [super layoutSubviews]; |
| [self invalidateIntrinsicContentSize]; |
| if (self.textView.scrollEnabled) { |
| [self.textView flashScrollIndicators]; |
| } |
| } |
| |
| #pragma mark - Layout methods |
| |
| - (void)updateConstraintsWithLayoutStyle:(MDCBannerViewLayoutStyle)layoutStyle { |
| UIButton *leadingButton = self.currentLeadingButton; |
| UIButton *trailingButton = self.currentTrailingButton; |
| |
| [self deactivateAllConstraints]; |
| self.contentViewConstraintBottom.active = YES; |
| self.contentViewConstraintTop.active = YES; |
| self.contentViewConstraintLeft.active = YES; |
| self.contentViewConstraintRight.active = YES; |
| |
| self.imageViewConstraintLeading.active = YES; |
| if (!self.imageView.hidden) { |
| self.textViewConstraintLeadingWithImage.active = YES; |
| } else { |
| self.textViewConstraintLeadingWithMargin.active = YES; |
| } |
| self.buttonContainerConstraintTrailing.active = YES; |
| self.buttonContainerConstraintBottom.active = YES; |
| self.buttonContainerConstraintHeight.active = YES; |
| |
| if (layoutStyle == MDCBannerViewLayoutStyleSingleRow) { |
| self.imageViewConstraintCenterY.active = YES; |
| self.textViewConstraintCenterY.active = YES; |
| if (trailingButton.hidden) { |
| self.buttonContainerConstraintWidthWithLeadingButton.active = YES; |
| } |
| self.buttonContainerConstraintTopWithMargin.active = YES; |
| if (leadingButton.hidden) { |
| self.textViewConstraintTrailing.active = YES; |
| } else { |
| self.buttonContainerConstraintLeadingWithTextView.active = YES; |
| } |
| } else { |
| self.imageViewConstraintTopLarge.active = YES; |
| if (!self.imageView.hidden) { |
| self.buttonContainerConstraintTopWithImageViewGreater.active = YES; |
| } |
| self.textViewConstraintTop.active = YES; |
| self.textViewConstraintTrailing.active = YES; |
| self.buttonContainerConstraintTopWithTextViewGreater.active = YES; |
| self.buttonContainerConstraintTopWithTextView.active = YES; |
| self.buttonContainerConstraintLeading.active = YES; |
| } |
| if (self.numberOfLinesInTextView > 0) { |
| self.textViewConstraintHeight.active = YES; |
| } |
| [self updateButtonsConstraintsWithLayoutStyle:layoutStyle]; |
| |
| if (self.showsDivider) { |
| self.dividerConstraintWidth.active = YES; |
| self.dividerConstraintLeading.active = YES; |
| self.dividerConstraintHeight.active = YES; |
| self.dividerConstraintBottom.active = YES; |
| } |
| } |
| |
| #pragma mark - Layout helpers |
| |
| - (void)updateButtonsConstraintsWithLayoutStyle:(MDCBannerViewLayoutStyle)layoutStyle { |
| UIButton *leadingButton = self.currentLeadingButton; |
| UIButton *trailingButton = self.currentTrailingButton; |
| |
| if (trailingButton.hidden) { |
| self.leadingButtonConstraintTrailing.active = YES; |
| self.leadingButtonConstraintCenterY.active = YES; |
| self.trailingButtonConstraintHeightZero.active = YES; |
| } else { |
| if (layoutStyle == MDCBannerViewLayoutStyleMultiRowStackedButton) { |
| self.leadingButtonConstraintTop.active = YES; |
| self.leadingButtonConstraintTrailing.active = YES; |
| self.trailingButtonConstraintTop.active = YES; |
| self.trailingButtonConstraintLeading.active = YES; |
| } else { |
| self.leadingButtonConstraintTrailingWithTrailingButton.active = YES; |
| self.leadingButtonConstraintBaseLineWithTrailingButton.active = YES; |
| } |
| } |
| if (leadingButton.hidden) { |
| self.leadingButtonConstraintHeightZero.active = YES; |
| // Constrain trailing button to the leading edge of the button container when |
| // leading button not present. |
| self.trailingButtonConstraintLeading.active = YES; |
| } |
| self.leadingButtonConstraintLeading.active = YES; |
| self.trailingButtonConstraintTrailing.active = YES; |
| self.trailingButtonConstraintBottom.active = YES; |
| } |
| |
| - (MDCBannerViewLayoutStyle)layoutStyleForSizeToFit:(CGSize)sizeToFit { |
| if (self.bannerViewLayoutStyle != MDCBannerViewLayoutStyleAutomatic) { |
| return self.bannerViewLayoutStyle; |
| } |
| |
| UIButton *leadingButton = self.currentLeadingButton; |
| UIButton *trailingButton = self.currentTrailingButton; |
| |
| MDCBannerViewLayoutStyle layoutStyle; |
| CGSize contentSize = [self contentSizeForLayoutSize:sizeToFit]; |
| CGFloat remainingWidth = contentSize.width; |
| if (!leadingButton.hidden) { |
| leadingButton.titleLabel.preferredMaxLayoutWidth = 0; |
| } |
| [leadingButton sizeToFit]; |
| if (trailingButton.hidden) { |
| CGFloat buttonWidth = CGRectGetWidth(leadingButton.frame); |
| remainingWidth -= (buttonWidth + kHorizontalSpaceBetweenTextViewAndButton); |
| if (!self.imageView.hidden) { |
| remainingWidth -= kImageViewSideLength; |
| remainingWidth -= kSpaceBetweenIconImageAndTextView; |
| } |
| layoutStyle = [self isAbleToFitTextView:self.textView withWidthLimit:remainingWidth] |
| ? MDCBannerViewLayoutStyleSingleRow |
| : MDCBannerViewLayoutStyleMultiRowAlignedButton; |
| } else { |
| trailingButton.titleLabel.preferredMaxLayoutWidth = 0; |
| [trailingButton sizeToFit]; |
| CGFloat buttonWidth = [self widthSumForButtons:@[ leadingButton, trailingButton ]]; |
| remainingWidth -= buttonWidth; |
| layoutStyle = (remainingWidth > 0) ? MDCBannerViewLayoutStyleMultiRowAlignedButton |
| : MDCBannerViewLayoutStyleMultiRowStackedButton; |
| } |
| return layoutStyle; |
| } |
| |
| - (CGFloat)getFrameHeightOfImageViewAndTextViewWithSizeToFit:(CGSize)sizeToFit { |
| CGFloat frameHeight = 0; |
| CGFloat remainingWidth = sizeToFit.width; |
| CGFloat textViewHeight = 0; |
| |
| if (!self.imageView.hidden) { |
| remainingWidth -= (kImageViewSideLength + kSpaceBetweenIconImageAndTextView); |
| // If the text view will become smaller than image view, consider the image view as the |
| // limiting factor. |
| textViewHeight = [self getDisplayedTextViewHeight:remainingWidth]; |
| frameHeight += MAX(textViewHeight, kImageViewSideLength); |
| } else { |
| textViewHeight = [self getDisplayedTextViewHeight:remainingWidth]; |
| frameHeight += textViewHeight; |
| } |
| CGFloat verticalSpace = _isM3CButtonEnabled ? kVerticalSpaceBetweenButtonAndTextViewWithM3CButton |
| : kVerticalSpaceBetweenButtonAndTextView; |
| frameHeight += verticalSpace; |
| return frameHeight; |
| } |
| |
| - (CGSize)contentSizeForLayoutSize:(CGSize)layoutSize { |
| CGFloat remainingWidth = layoutSize.width; |
| CGFloat marginsPadding = self.layoutMargins.left + self.layoutMargins.right; |
| marginsPadding += self.contentEdgeInsets.left + self.contentEdgeInsets.right; |
| remainingWidth -= marginsPadding; |
| CGFloat trailingPadding = _isM3CButtonEnabled ? kButtonContainerTrailingPaddingWithM3CButton |
| : kButtonContainerTrailingPadding; |
| remainingWidth -= (kLeadingPadding + trailingPadding); |
| return CGSizeMake(remainingWidth, layoutSize.height); |
| } |
| |
| - (CGFloat)widthSumForButtons:(NSArray<__kindof UIButton *> *)buttons { |
| CGFloat buttonsWidthSum = 0; |
| for (UIButton *button in buttons) { |
| buttonsWidthSum += CGRectGetWidth(button.frame); |
| } |
| if (buttons.count > 1) { |
| buttonsWidthSum += (buttons.count - 1) * kButtonHorizontalIntervalSpace; |
| } |
| return buttonsWidthSum; |
| } |
| |
| - (BOOL)isAbleToFitTextView:(UITextView *)textView withWidthLimit:(CGFloat)widthLimit { |
| CGSize size = [textView sizeThatFits:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)]; |
| return size.width <= widthLimit; |
| } |
| |
| /// Returns the height of the text view that is currently displayed. |
| - (CGFloat)getDisplayedTextViewHeight:(CGFloat)availableWidth { |
| // If the text view height is not restricted, return the height of the text view that fits |
| // the remaining width. |
| if (self.numberOfLinesInTextView == 0) { |
| CGSize textViewSize = [self.textView sizeThatFits:CGSizeMake(availableWidth, CGFLOAT_MAX)]; |
| return textViewSize.height; |
| } |
| |
| // If the text view height is restricted, find the height of the text view that fits the |
| // remaining width and the maximum number of lines. |
| self.textView.textContainer.maximumNumberOfLines = self.numberOfLinesInTextView; |
| // Must manually notify the layout manager of new line restriction for sizeThatFits to return the |
| // correct height on iOS 15.5 and below. |
| [self.textView.layoutManager textContainerChangedGeometry:self.textView.textContainer]; |
| CGFloat lineLimitedHeight = |
| [self.textView sizeThatFits:CGSizeMake(availableWidth, CGFLOAT_MAX)].height; |
| |
| // Calculate height of text view with no line limit. |
| self.textView.textContainer.maximumNumberOfLines = 0; |
| // Must manually notify the layout manager of new line restriction for sizeThatFits to return the |
| // correct height on iOS 15.5 and below. |
| [self.textView.layoutManager textContainerChangedGeometry:self.textView.textContainer]; |
| CGFloat textViewFullHeight = |
| [self.textView sizeThatFits:CGSizeMake(availableWidth, CGFLOAT_MAX)].height; |
| |
| // If the text view intrinsic height is greater than the line limited height, enable scrolling and |
| // update the text view height constraint to the line limited height. |
| if (textViewFullHeight > lineLimitedHeight + FLT_EPSILON) { |
| self.textViewConstraintHeight.constant = lineLimitedHeight; |
| self.textView.scrollEnabled = YES; |
| self.textView.contentOffset = CGPointZero; |
| [self.textView flashScrollIndicators]; |
| return lineLimitedHeight; |
| } else { |
| self.textViewConstraintHeight.constant = textViewFullHeight; |
| self.textView.scrollEnabled = NO; |
| return textViewFullHeight; |
| } |
| } |
| |
| #pragma mark - Font |
| - (void)traitCollectionDidChange:(nullable UITraitCollection *)previousTraitCollection { |
| [super traitCollectionDidChange:previousTraitCollection]; |
| |
| if (self.traitCollectionDidChangeBlock) { |
| self.traitCollectionDidChangeBlock(self, previousTraitCollection); |
| } |
| |
| [self setNeedsLayout]; |
| [self layoutIfNeeded]; |
| } |
| |
| #pragma mark - Accessibility |
| |
| - (nullable NSArray *)accessibilityElements { |
| UIButton *leadingButton = self.currentLeadingButton; |
| UIButton *trailingButton = self.currentTrailingButton; |
| return @[ self.textView, leadingButton, trailingButton ]; |
| } |
| |
| @end |
| |
| NS_ASSUME_NONNULL_END |