blob: 42f2aecffd559dc04116d8a2ebf1b1ca399b77f0 [file] [log] [blame] [edit]
// 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 <UIKit/UIKit.h>
#import "MDCBannerView.h"
#import "MDCBannerViewLayout.h"
static const CGFloat kLeadingPadding = 16.0f;
static const CGFloat kTrailingPadding = 8.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 kSpaceBetweenIconImageAndTextLabel = 16.0f;
static const CGFloat kHorizontalSpaceBetweenTextLabelAndButton = 36.0f;
static const CGFloat kVerticalSpaceBetweenButtonAndTextLabelLarge = 20.0f;
static const CGFloat kVerticalSpaceBetweenButtonAndTextLabelSmall = 12.0f;
typedef NS_ENUM(NSInteger, MDCBannerViewLayoutMode) {
MDCBannerViewLayoutSingleLineStyle = 0, // All elements lays on the same line
MDCBannerViewLayoutMultiLineStackedButtonStyle = 1, // Stacked button layout
MDCBannerViewLayoutMultiLineAlignedButtonStyle = 2 // All buttons lays on the same line
};
@interface MDCBannerViewLayout ()
@property(nonatomic, strong) NSMutableArray *internalButtonFrames;
@property(nonatomic, strong) UIView *imageContainer;
@property(nonatomic, strong) UILabel *textLabel;
@property(nonatomic, assign) CGSize sizeToFit;
@property(nonatomic, assign) CGSize frameSize;
@property(nonatomic, assign) MDCBannerViewLayoutMode style;
@end
@implementation MDCBannerViewLayout
- (instancetype)initWithSizeToFit:(CGSize)sizeToFit
textLabel:(UILabel *)textLabel
imageContainer:(UIView *)imageContainer
buttons:(NSArray<__kindof UIButton *> *)buttons {
self = [super init];
if (self) {
_sizeToFit = sizeToFit;
_internalButtonFrames = [[NSMutableArray alloc] init];
_imageContainer = [[UIView alloc] initWithFrame:imageContainer.frame];
_textLabel = [[UILabel alloc] initWithFrame:textLabel.frame];
_textLabel.text = textLabel.text;
_textLabel.font = textLabel.font;
_textLabel.numberOfLines = textLabel.numberOfLines;
for (NSUInteger index = 0; index < buttons.count; ++index) {
UIButton *button = buttons[index];
[button sizeToFit];
[_internalButtonFrames addObject:[NSValue valueWithCGRect:button.frame]];
}
[self layout];
}
return self;
}
- (void)layout {
self.style = [self layoutStyleForSizeToFit:self.sizeToFit];
self.frameSize = [self frameSizeForLayoutStyle:self.style withSizeToFit:self.sizeToFit];
[self updateLayoutWithStyle:self.style];
}
- (MDCBannerViewLayoutMode)layoutStyleForSizeToFit:(CGSize)sizeToFit {
CGFloat remainingWidth = self.sizeToFit.width;
remainingWidth -= (kLeadingPadding + kTrailingPadding);
remainingWidth -= [self buttonsWidthSum];
remainingWidth -= kHorizontalSpaceBetweenTextLabelAndButton;
if (self.imageContainer) {
remainingWidth -= self.imageContainer.frame.size.width;
remainingWidth -= kSpaceBetweenIconImageAndTextLabel;
}
if ([self isAbleToFitTextLabelWithWidthLimit:remainingWidth]) {
return MDCBannerViewLayoutSingleLineStyle;
} else {
if ([self buttonsWidthSum] + kLeadingPadding + kTrailingPadding > self.sizeToFit.width) {
return MDCBannerViewLayoutMultiLineStackedButtonStyle;
} else {
return MDCBannerViewLayoutMultiLineAlignedButtonStyle;
}
}
}
- (CGSize)frameSizeForLayoutStyle:(MDCBannerViewLayoutMode)style withSizeToFit:(CGSize)sizeToFit {
CGFloat frameHeight = 0.0f;
switch (style) {
case MDCBannerViewLayoutSingleLineStyle: {
frameHeight += kTopPaddingSmall + kBottomPadding;
NSMutableArray *singleLineViews = [[NSMutableArray alloc] init];
[singleLineViews addObject:self.textLabel];
if (self.imageContainer) {
[singleLineViews addObject:self.imageContainer];
}
frameHeight +=
MAX([self maximumHeightAmongViews:singleLineViews], [self maximumButtonHeight]);
break;
}
case MDCBannerViewLayoutMultiLineAlignedButtonStyle: {
frameHeight += [self getFrameHeightOfImageAndTextWithSizeToFit:sizeToFit];
frameHeight += [self maximumButtonHeight];
break;
}
case MDCBannerViewLayoutMultiLineStackedButtonStyle: {
frameHeight += [self getFrameHeightOfImageAndTextWithSizeToFit:sizeToFit];
frameHeight += [self buttonsHeightSum];
break;
}
}
return CGSizeMake(sizeToFit.width, frameHeight);
}
- (CGFloat)getFrameHeightOfImageAndTextWithSizeToFit:(CGSize)sizeToFit {
CGFloat frameHeight = kTopPaddingLarge + kBottomPadding;
CGFloat remainingWidth = sizeToFit.width - kLeadingPadding - kTrailingPadding;
CGSize textLabelSize = CGSizeZero;
if (self.imageContainer) {
remainingWidth -= (self.imageContainer.frame.size.width + kSpaceBetweenIconImageAndTextLabel);
textLabelSize = [self.textLabel sizeThatFits:CGSizeMake(remainingWidth, CGFLOAT_MAX)];
frameHeight += MAX(textLabelSize.height + kVerticalSpaceBetweenButtonAndTextLabelLarge,
76.0f); // Hard coded value here (Image side + padding)
} else {
frameHeight += kVerticalSpaceBetweenButtonAndTextLabelSmall;
textLabelSize = [self.textLabel sizeThatFits:CGSizeMake(remainingWidth, CGFLOAT_MAX)];
frameHeight += textLabelSize.height;
}
return frameHeight;
}
- (void)updateLayoutWithStyle:(MDCBannerViewLayoutMode)style {
switch (style) {
case MDCBannerViewLayoutSingleLineStyle: {
CGFloat currentXAxis = 0.0f;
currentXAxis += kLeadingPadding;
CGFloat remainingWidth = self.sizeToFit.width;
remainingWidth -= (kLeadingPadding + kTrailingPadding);
remainingWidth -= [self buttonsWidthSum];
remainingWidth -= kHorizontalSpaceBetweenTextLabelAndButton;
if (self.imageContainer) {
CGRect originalIconimageContainer = self.imageContainer.frame;
self.imageContainer.frame = CGRectMake(
currentXAxis, self.frameSize.height / 2 - originalIconimageContainer.size.height / 2,
originalIconimageContainer.size.width, originalIconimageContainer.size.height);
currentXAxis += self.imageContainer.frame.size.width;
currentXAxis += kSpaceBetweenIconImageAndTextLabel;
remainingWidth -= self.imageContainer.frame.size.width;
remainingWidth -= kSpaceBetweenIconImageAndTextLabel;
}
CGSize textLabelSize = [self.textLabel sizeThatFits:CGSizeMake(remainingWidth, CGFLOAT_MAX)];
self.textLabel.frame =
CGRectMake(currentXAxis, self.frameSize.height / 2 - textLabelSize.height / 2,
textLabelSize.width, textLabelSize.height);
currentXAxis += self.textLabelFrame.size.width;
for (NSUInteger index = 0; index < self.internalButtonFrames.count; ++index) {
currentXAxis += kButtonHorizontalIntervalSpace;
CGRect buttonFrame = [self.internalButtonFrames[index] CGRectValue];
CGRect newButtonFrame =
CGRectMake(currentXAxis, self.frameSize.height / 2 - buttonFrame.size.height / 2,
buttonFrame.size.width, buttonFrame.size.height);
self.internalButtonFrames[index] = [NSValue valueWithCGRect:newButtonFrame];
currentXAxis += newButtonFrame.size.width;
}
break;
}
case MDCBannerViewLayoutMultiLineAlignedButtonStyle: {
CGFloat currentXAxis = 0.0f;
CGFloat currentYAxis = 0.0f;
currentYAxis += kTopPaddingLarge;
currentXAxis += kLeadingPadding;
CGFloat remainingWidth = self.sizeToFit.width - kLeadingPadding - kTrailingPadding;
if (self.imageContainer) {
CGSize originalImageSize = self.imageContainerFrame.size;
self.imageContainer.frame = CGRectMake(currentXAxis, currentYAxis, originalImageSize.width,
originalImageSize.height);
currentXAxis += self.imageContainerFrame.size.width;
currentXAxis += kSpaceBetweenIconImageAndTextLabel;
remainingWidth -=
(self.imageContainer.frame.size.width + kSpaceBetweenIconImageAndTextLabel);
}
CGSize textLabelSize = [self.textLabel sizeThatFits:CGSizeMake(remainingWidth, CGFLOAT_MAX)];
self.textLabel.frame =
CGRectMake(currentXAxis, currentYAxis, textLabelSize.width, textLabelSize.height);
currentXAxis = self.frameSize.width;
currentYAxis = self.frameSize.height;
currentXAxis -= kTrailingPadding;
currentYAxis -= kBottomPadding;
CGFloat buttonMaximumHeight = [self maximumButtonHeight];
CGFloat currentYAxisCenter = currentYAxis - buttonMaximumHeight / 2;
NSUInteger buttonCount = self.internalButtonFrames.count;
for (NSUInteger index = 0; index <= buttonCount - 1; ++index) {
CGRect buttonFrame = [self.internalButtonFrames[buttonCount - index - 1] CGRectValue];
currentXAxis -= buttonFrame.size.width;
CGRect newButtonFrame =
CGRectMake(currentXAxis, currentYAxisCenter - buttonFrame.size.height / 2,
buttonFrame.size.width, buttonFrame.size.height);
self.internalButtonFrames[buttonCount - index - 1] =
[NSValue valueWithCGRect:newButtonFrame];
currentXAxis -= kButtonHorizontalIntervalSpace;
}
break;
}
case MDCBannerViewLayoutMultiLineStackedButtonStyle: {
CGFloat currentXAxis = 0.0f;
CGFloat currentYAxis = 0.0f;
currentYAxis += kTopPaddingLarge;
currentXAxis += kLeadingPadding;
CGFloat remainingWidth = self.sizeToFit.width - kLeadingPadding - kTrailingPadding;
if (self.imageContainer) {
CGSize originalIconImageSize = self.imageContainerFrame.size;
self.imageContainer.frame = CGRectMake(
currentXAxis, currentYAxis, originalIconImageSize.width, originalIconImageSize.height);
currentXAxis += self.imageContainerFrame.size.width;
currentXAxis += kSpaceBetweenIconImageAndTextLabel;
remainingWidth -=
(self.imageContainer.frame.size.width + kSpaceBetweenIconImageAndTextLabel);
}
CGSize textLabelSize = [self.textLabel sizeThatFits:CGSizeMake(remainingWidth, CGFLOAT_MAX)];
self.textLabel.frame =
CGRectMake(currentXAxis, currentYAxis, textLabelSize.width, textLabelSize.height);
currentXAxis = self.frameSize.width;
currentYAxis = self.frameSize.height;
currentXAxis -= kTrailingPadding;
currentYAxis -= kBottomPadding;
NSUInteger buttonCount = self.internalButtonFrames.count;
for (NSUInteger index = 0; index <= buttonCount - 1; ++index) {
CGRect buttonFrame = [self.internalButtonFrames[buttonCount - index - 1] CGRectValue];
currentYAxis -= buttonFrame.size.height;
CGRect newButtonFrame = CGRectMake(currentXAxis - buttonFrame.size.width, currentYAxis,
buttonFrame.size.width, buttonFrame.size.height);
self.internalButtonFrames[buttonCount - index - 1] =
[NSValue valueWithCGRect:newButtonFrame];
currentYAxis -= kButtonVerticalIntervalSpace;
}
break;
}
}
}
#pragma mark - Internal Layout Value Helpers
- (CGFloat)buttonsWidthSum {
CGFloat buttonsWidthSum = 0.0f;
for (NSValue *buttonFrame in self.internalButtonFrames) {
buttonsWidthSum += [buttonFrame CGRectValue].size.width;
}
if (self.internalButtonFrames.count > 0) {
buttonsWidthSum += (self.internalButtonFrames.count - 1) * kButtonHorizontalIntervalSpace;
}
return buttonsWidthSum;
}
- (CGFloat)buttonsHeightSum {
CGFloat buttonsHeightSum = 0.0f;
for (NSValue *buttonFrame in self.internalButtonFrames) {
buttonsHeightSum += [buttonFrame CGRectValue].size.height;
}
if (self.internalButtonFrames.count > 0) {
buttonsHeightSum += (self.internalButtonFrames.count - 1) * kButtonVerticalIntervalSpace;
}
return buttonsHeightSum;
}
- (BOOL)isAbleToFitTextLabelWithWidthLimit:(CGFloat)widthLimit {
CGSize size =
[self.textLabel.text sizeWithAttributes:@{NSFontAttributeName : self.textLabel.font}];
return size.width <= widthLimit;
}
- (CGFloat)maximumHeightAmongViews:(NSArray<__kindof UIView *> *)views {
CGFloat maximumHeight = 0.0f;
for (UIView *view in views) {
maximumHeight = MAX(maximumHeight, view.frame.size.height);
}
return maximumHeight;
}
- (CGFloat)maximumButtonHeight {
CGFloat maximumHeight = 0.0f;
for (NSValue *buttonFrame in self.internalButtonFrames) {
maximumHeight = MAX(maximumHeight, [buttonFrame CGRectValue].size.height);
}
return maximumHeight;
}
#pragma mark - APIs
- (NSArray *)buttonFrames {
return [self.internalButtonFrames copy];
}
- (CGRect)imageContainerFrame {
return self.imageContainer.frame;
}
- (CGRect)textLabelFrame {
return self.textLabel.frame;
}
@end