blob: 2fb167396b41fc9ab8718a1013983a325e2127d4 [file] [log] [blame] [edit]
// Copyright 2015-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 "MaterialFlexibleHeader+CanAlwaysExpandToMaximumHeight.h"
#import "MaterialFlexibleHeader.h"
#import "MaterialFlexibleHeader+ShiftBehaviorEnabledWithStatusBar.h"
#import "supplemental/FlexibleHeaderConfiguratorControlItem.h"
typedef enum : NSUInteger {
FlexibleHeaderConfiguratorFieldCanOverExtend,
FlexibleHeaderConfiguratorFieldInFrontOfInfiniteContent,
FlexibleHeaderConfiguratorFieldHideStatusBar,
FlexibleHeaderConfiguratorFieldContentImportance,
FlexibleHeaderConfiguratorFieldShiftBehaviorEnabled,
FlexibleHeaderConfiguratorFieldShiftBehaviorEnabledWithStatusBar,
FlexibleHeaderConfiguratorFieldShiftBehaviorHideable,
FlexibleHeaderConfiguratorFieldShiftOffscreen,
FlexibleHeaderConfiguratorFieldShiftOnscreen,
FlexibleHeaderConfiguratorFieldMinimumHeight,
FlexibleHeaderConfiguratorFieldMaximumHeight,
FlexibleHeaderConfiguratorFieldMinMaxHeightIncludeSafeArea,
FlexibleHeaderConfiguratorFieldCanAlwaysExpandToMaximumHeight,
} FlexibleHeaderConfiguratorField;
static const UITableViewStyle kStyle = UITableViewStyleGrouped;
@interface FlexibleHeaderConfiguratorExample
: UITableViewController <MDCFlexibleHeaderViewLayoutDelegate>
@property(nonatomic, strong) UILabel *titleLabel;
@property(nonatomic, strong) MDCFlexibleHeaderViewController *fhvc;
@property(nonatomic, assign) CGFloat minimumHeaderHeight;
// Supplemental properties
@property(nonatomic, copy) NSArray *sections;
@property(nonatomic, copy) NSArray *sectionTitles;
@property(nonatomic, assign) BOOL overrideStatusBarHidden;
@end
@implementation FlexibleHeaderConfiguratorExample
#pragma mark - MDCFlexibleHeaderViewLayoutDelegate
- (void)flexibleHeaderViewController:(MDCFlexibleHeaderViewController *)flexibleHeaderViewController
flexibleHeaderViewFrameDidChange:(MDCFlexibleHeaderView *)flexibleHeaderView {
NSLog(@"Scroll phase: %@ percentage: %@ value: %@", @(flexibleHeaderView.scrollPhase),
@(flexibleHeaderView.scrollPhasePercentage), @(flexibleHeaderView.scrollPhaseValue));
}
// Invoked when the user has changed a control's value.
- (void)field:(FlexibleHeaderConfiguratorField)field didChangeValue:(NSNumber *)value {
MDCFlexibleHeaderView *headerView = self.fhvc.headerView;
switch (field) {
// Basic behavior
case FlexibleHeaderConfiguratorFieldCanOverExtend:
headerView.canOverExtend = [value boolValue];
break;
case FlexibleHeaderConfiguratorFieldInFrontOfInfiniteContent:
headerView.inFrontOfInfiniteContent = [value boolValue];
break;
case FlexibleHeaderConfiguratorFieldHideStatusBar: {
self.overrideStatusBarHidden = [value boolValue];
BOOL statusBarCanBeVisible = !self.overrideStatusBarHidden;
headerView.statusBarHintCanOverlapHeader = statusBarCanBeVisible;
[UIView animateWithDuration:0.4
animations:^{
[self setNeedsStatusBarAppearanceUpdate];
}];
break;
}
// Shift behavior
case FlexibleHeaderConfiguratorFieldShiftBehaviorEnabled: {
BOOL isOn = [value boolValue];
if (!isOn) {
headerView.shiftBehavior = MDCFlexibleHeaderShiftBehaviorDisabled;
[self
didChangeValueForField:FlexibleHeaderConfiguratorFieldShiftBehaviorEnabledWithStatusBar
animated:YES];
} else {
headerView.shiftBehavior = MDCFlexibleHeaderShiftBehaviorEnabled;
[self didChangeValueForField:FlexibleHeaderConfiguratorFieldShiftBehaviorHideable
animated:YES];
}
break;
}
case FlexibleHeaderConfiguratorFieldShiftBehaviorEnabledWithStatusBar: {
BOOL isOn = [value boolValue];
if (!isOn) {
headerView.shiftBehavior = MDCFlexibleHeaderShiftBehaviorEnabled;
} else {
headerView.shiftBehavior = MDCFlexibleHeaderShiftBehaviorEnabledWithStatusBar;
[self didChangeValueForField:FlexibleHeaderConfiguratorFieldShiftBehaviorEnabled
animated:YES];
[self didChangeValueForField:FlexibleHeaderConfiguratorFieldShiftBehaviorHideable
animated:YES];
}
break;
}
case FlexibleHeaderConfiguratorFieldShiftBehaviorHideable: {
BOOL isOn = [value boolValue];
if (!isOn) {
headerView.shiftBehavior = MDCFlexibleHeaderShiftBehaviorDisabled;
} else {
headerView.shiftBehavior = MDCFlexibleHeaderShiftBehaviorHideable;
[self didChangeValueForField:FlexibleHeaderConfiguratorFieldShiftBehaviorEnabled
animated:YES];
[self
didChangeValueForField:FlexibleHeaderConfiguratorFieldShiftBehaviorEnabledWithStatusBar
animated:YES];
}
break;
}
case FlexibleHeaderConfiguratorFieldContentImportance:
headerView.headerContentImportance =
([value boolValue] ? MDCFlexibleHeaderContentImportanceHigh
: MDCFlexibleHeaderContentImportanceDefault);
break;
case FlexibleHeaderConfiguratorFieldShiftOffscreen: {
[headerView shiftHeaderOffScreenAnimated:YES];
break;
}
case FlexibleHeaderConfiguratorFieldShiftOnscreen: {
[headerView shiftHeaderOnScreenAnimated:YES];
break;
}
// Header height
case FlexibleHeaderConfiguratorFieldMinimumHeight:
headerView.minimumHeight = [self heightDenormalized:[value floatValue]];
break;
case FlexibleHeaderConfiguratorFieldMaximumHeight:
headerView.maximumHeight = [self heightDenormalized:[value floatValue]];
break;
case FlexibleHeaderConfiguratorFieldMinMaxHeightIncludeSafeArea:
headerView.minMaxHeightIncludesSafeArea = [value boolValue];
break;
case FlexibleHeaderConfiguratorFieldCanAlwaysExpandToMaximumHeight:
headerView.canAlwaysExpandToMaximumHeight = [value boolValue];
break;
}
}
#pragma mark - Typical Flexible Header implementations
// Required for shiftBehavior == MDCFlexibleHeaderShiftBehaviorEnabledWithStatusBar.
- (BOOL)prefersStatusBarHidden {
return _overrideStatusBarHidden || self.fhvc.prefersStatusBarHidden;
}
- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation {
return UIStatusBarAnimationSlide;
}
#pragma mark - UIScrollViewDelegate
// Note that, unlike the Typical Use example, we are explicitly forwarding the UIScrollViewDelegate
// methods to the header view. This is because this example controller also needs to handle other
// UITableViewDelegate events.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView == self.fhvc.headerView.trackingScrollView) {
[self.fhvc.headerView trackingScrollViewDidScroll];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
if (scrollView == self.fhvc.headerView.trackingScrollView) {
[self.fhvc.headerView trackingScrollViewDidEndDecelerating];
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
MDCFlexibleHeaderView *headerView = self.fhvc.headerView;
if (scrollView == headerView.trackingScrollView) {
[headerView trackingScrollViewDidEndDraggingWillDecelerate:decelerate];
}
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint *)targetContentOffset {
MDCFlexibleHeaderView *headerView = self.fhvc.headerView;
if (scrollView == headerView.trackingScrollView) {
[headerView trackingScrollViewWillEndDraggingWithVelocity:velocity
targetContentOffset:targetContentOffset];
}
}
#pragma mark - Field data manipulation
static const CGFloat kHeightScalar = 300;
- (CGFloat)normalizedHeight:(CGFloat)height {
return (height - self.minimumHeaderHeight) / (kHeightScalar - self.minimumHeaderHeight);
}
- (CGFloat)heightDenormalized:(CGFloat)normalized {
return normalized * (kHeightScalar - self.minimumHeaderHeight) + self.minimumHeaderHeight;
}
- (NSNumber *)valueForField:(FlexibleHeaderConfiguratorField)field {
switch (field) {
case FlexibleHeaderConfiguratorFieldCanOverExtend:
return @(self.fhvc.headerView.canOverExtend);
case FlexibleHeaderConfiguratorFieldContentImportance:
return @(
(self.fhvc.headerView.headerContentImportance == MDCFlexibleHeaderContentImportanceHigh));
case FlexibleHeaderConfiguratorFieldHideStatusBar:
return @(self.overrideStatusBarHidden);
case FlexibleHeaderConfiguratorFieldShiftBehaviorEnabled: {
MDCFlexibleHeaderShiftBehavior behavior = self.fhvc.headerView.shiftBehavior;
BOOL enabled = (behavior == MDCFlexibleHeaderShiftBehaviorEnabled ||
behavior == MDCFlexibleHeaderShiftBehaviorEnabledWithStatusBar);
return @(enabled);
}
case FlexibleHeaderConfiguratorFieldShiftBehaviorEnabledWithStatusBar: {
MDCFlexibleHeaderShiftBehavior behavior = self.fhvc.headerView.shiftBehavior;
BOOL enabled = (behavior == MDCFlexibleHeaderShiftBehaviorEnabledWithStatusBar);
return @(enabled);
}
case FlexibleHeaderConfiguratorFieldShiftBehaviorHideable: {
MDCFlexibleHeaderShiftBehavior behavior = self.fhvc.headerView.shiftBehavior;
BOOL enabled = (behavior == MDCFlexibleHeaderShiftBehaviorHideable);
return @(enabled);
}
// Buttons have no value
case FlexibleHeaderConfiguratorFieldShiftOffscreen:
case FlexibleHeaderConfiguratorFieldShiftOnscreen:
return nil;
case FlexibleHeaderConfiguratorFieldInFrontOfInfiniteContent:
return @(self.fhvc.headerView.inFrontOfInfiniteContent);
case FlexibleHeaderConfiguratorFieldMinimumHeight:
return @([self normalizedHeight:self.fhvc.headerView.minimumHeight]);
case FlexibleHeaderConfiguratorFieldMaximumHeight:
return @([self normalizedHeight:self.fhvc.headerView.maximumHeight]);
case FlexibleHeaderConfiguratorFieldMinMaxHeightIncludeSafeArea:
return @(self.fhvc.headerView.minMaxHeightIncludesSafeArea);
case FlexibleHeaderConfiguratorFieldCanAlwaysExpandToMaximumHeight:
return @(self.fhvc.headerView.canAlwaysExpandToMaximumHeight);
}
}
- (instancetype)init {
return [self initWithStyle:kStyle];
}
- (instancetype)initWithStyle:(UITableViewStyle)style {
self = [super initWithStyle:style];
if (self) {
self.fhvc = [[MDCFlexibleHeaderViewController alloc] initWithNibName:nil bundle:nil];
// Behavioral flags.
self.fhvc.topLayoutGuideAdjustmentEnabled = YES;
self.fhvc.inferTopSafeAreaInsetFromViewController = YES;
self.fhvc.headerView.minMaxHeightIncludesSafeArea = NO;
[self addChildViewController:self.fhvc];
self.title = @"Configurator";
}
return self;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.navigationController setNavigationBarHidden:YES animated:animated];
}
- (UIStatusBarStyle)preferredStatusBarStyle {
return UIStatusBarStyleLightContent;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.minimumHeaderHeight = 0;
self.fhvc.headerView.trackingScrollView = self.tableView;
self.fhvc.layoutDelegate = self;
self.fhvc.view.frame = self.view.bounds;
[self.view addSubview:self.fhvc.view];
[self.fhvc didMoveToParentViewController:self];
self.fhvc.headerView.backgroundColor = [UIColor colorWithWhite:(CGFloat)0.1 alpha:1];
self.titleLabel = [[UILabel alloc] init];
self.titleLabel.text = self.title;
self.titleLabel.textColor = [UIColor whiteColor];
self.titleLabel.font = [UIFont systemFontOfSize:22];
self.titleLabel.textAlignment = NSTextAlignmentCenter;
[self.titleLabel sizeToFit];
CGRect frame = self.fhvc.headerView.bounds;
self.titleLabel.frame = CGRectMake(0, 0, frame.size.width, frame.size.height);
self.titleLabel.autoresizingMask =
UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleBottomMargin;
[self.fhvc.headerView addSubview:self.titleLabel];
self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[NSLayoutConstraint constraintWithItem:self.titleLabel
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.fhvc.headerView.topSafeAreaGuide
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:0],
[NSLayoutConstraint constraintWithItem:self.titleLabel
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.fhvc.headerView
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:0],
[NSLayoutConstraint constraintWithItem:self.titleLabel
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:self.fhvc.headerView
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:0],
[NSLayoutConstraint constraintWithItem:self.titleLabel
attribute:NSLayoutAttributeRight
relatedBy:NSLayoutRelationEqual
toItem:self.fhvc.headerView
attribute:NSLayoutAttributeRight
multiplier:1.0
constant:0]
]];
[self.fhvc.headerView hideViewWhenShifted:self.titleLabel];
id (^buttonItem)(NSString *, FlexibleHeaderConfiguratorField) = ^(
NSString *title, FlexibleHeaderConfiguratorField field) {
FlexibleHeaderConfiguratorControlType type = FlexibleHeaderConfiguratorControlTypeButton;
return [FlexibleHeaderConfiguratorControlItem itemWithTitle:title controlType:type field:field];
};
id (^switchItem)(NSString *, FlexibleHeaderConfiguratorField) = ^(
NSString *title, FlexibleHeaderConfiguratorField field) {
FlexibleHeaderConfiguratorControlType type = FlexibleHeaderConfiguratorControlTypeSwitch;
return [FlexibleHeaderConfiguratorControlItem itemWithTitle:title controlType:type field:field];
};
id (^sliderItem)(NSString *, FlexibleHeaderConfiguratorField) = ^(
NSString *title, FlexibleHeaderConfiguratorField field) {
FlexibleHeaderConfiguratorControlType type = FlexibleHeaderConfiguratorControlTypeSlider;
return [FlexibleHeaderConfiguratorControlItem itemWithTitle:title controlType:type field:field];
};
id (^filler)(void) = ^{
return [NSNull null];
};
NSMutableArray *sections = [NSMutableArray array];
NSMutableArray *sectionTitles = [NSMutableArray array];
void (^createSection)(NSString *, NSArray *) = ^(NSString *title, NSArray *items) {
[sectionTitles addObject:title ?: @""];
[sections addObject:items ?: @[]];
};
createSection(@"Swipe right from left edge to go back", nil);
createSection(@"Basic behavior", @[
switchItem(@"Can over-extend", FlexibleHeaderConfiguratorFieldCanOverExtend),
switchItem(@"In front of infinite content",
FlexibleHeaderConfiguratorFieldInFrontOfInfiniteContent),
switchItem(@"Hide status bar", FlexibleHeaderConfiguratorFieldHideStatusBar),
]);
createSection(@"Shift behavior", @[
switchItem(@"Enabled", FlexibleHeaderConfiguratorFieldShiftBehaviorEnabled),
switchItem(@"Enabled with status bar",
FlexibleHeaderConfiguratorFieldShiftBehaviorEnabledWithStatusBar),
switchItem(@"Hideable", FlexibleHeaderConfiguratorFieldShiftBehaviorHideable),
switchItem(@"Header content is important", FlexibleHeaderConfiguratorFieldContentImportance),
buttonItem(@"Shift header off-screen", FlexibleHeaderConfiguratorFieldShiftOffscreen),
buttonItem(@"Shift header on-screen", FlexibleHeaderConfiguratorFieldShiftOnscreen)
]);
createSection(@"Header height", @[
sliderItem(@"Minimum", FlexibleHeaderConfiguratorFieldMinimumHeight),
sliderItem(@"Maximum", FlexibleHeaderConfiguratorFieldMaximumHeight),
switchItem(@"Min / max height includes Safe Area",
FlexibleHeaderConfiguratorFieldMinMaxHeightIncludeSafeArea),
switchItem(@"Can always expand to maximum height",
FlexibleHeaderConfiguratorFieldCanAlwaysExpandToMaximumHeight)
]);
NSMutableArray *fillerItems = [NSMutableArray array];
for (NSUInteger ix = 0; ix < 100; ++ix) {
[fillerItems addObject:filler()];
}
createSection(nil, fillerItems);
self.sections = sections;
self.sectionTitles = sectionTitles;
self.view.backgroundColor = [UIColor whiteColor];
}
- (UIControl *)controlForControlType:(FlexibleHeaderConfiguratorControlType)controlType {
switch (controlType) {
case FlexibleHeaderConfiguratorControlTypeButton:
return nil;
case FlexibleHeaderConfiguratorControlTypeSwitch:
return [[UISwitch alloc] init];
case FlexibleHeaderConfiguratorControlTypeSlider:
return [[UISlider alloc] init];
}
}
- (NSNumber *)valueForControl:(UIControl *)control {
if ([control isKindOfClass:[UISwitch class]]) {
return @([(UISwitch *)control isOn]);
} else if ([control isKindOfClass:[UISlider class]]) {
return @([(UISlider *)control value]);
}
return nil;
}
- (void)setValue:(NSNumber *)value onControl:(UIControl *)control animated:(BOOL)animated {
if ([control isKindOfClass:[UISwitch class]]) {
[(UISwitch *)control setOn:[value boolValue] animated:animated];
} else if ([control isKindOfClass:[UISlider class]]) {
[(UISlider *)control setValue:[value floatValue] animated:animated];
}
}
- (void)didChangeValueForField:(FlexibleHeaderConfiguratorField)field animated:(BOOL)animated {
UIControl *control = [self.tableView viewWithTag:field];
NSNumber *value = [self valueForField:field];
[self setValue:value onControl:control animated:animated];
}
#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return [self.sections count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [(NSArray *)self.sections[section] count];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
return self.sectionTitles[section];
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:@"cell"];
}
id item = self.sections[indexPath.section][indexPath.row];
if ([item isKindOfClass:[FlexibleHeaderConfiguratorControlItem class]]) {
FlexibleHeaderConfiguratorControlItem *fieldItem =
(FlexibleHeaderConfiguratorControlItem *)item;
cell.textLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleCaption1];
cell.textLabel.text = fieldItem.title;
UIControl *control = [self controlForControlType:fieldItem.controlType];
control.tag = fieldItem.field;
[control addTarget:self
action:@selector(didChangeControl:)
forControlEvents:UIControlEventValueChanged];
[self setValue:[self valueForField:fieldItem.field] onControl:control animated:NO];
cell.accessoryView = control;
} else {
cell.textLabel.text = nil;
cell.accessoryView = nil;
}
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
id item = self.sections[indexPath.section][indexPath.row];
if ([item isKindOfClass:[FlexibleHeaderConfiguratorControlItem class]]) {
FlexibleHeaderConfiguratorControlItem *fieldItem =
(FlexibleHeaderConfiguratorControlItem *)item;
if (fieldItem.controlType == FlexibleHeaderConfiguratorControlTypeButton) {
[self field:(FlexibleHeaderConfiguratorField)fieldItem.field didChangeValue:nil];
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
}
}
- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath {
id item = self.sections[indexPath.section][indexPath.row];
if ([item isKindOfClass:[FlexibleHeaderConfiguratorControlItem class]]) {
FlexibleHeaderConfiguratorControlItem *fieldItem =
(FlexibleHeaderConfiguratorControlItem *)item;
if (fieldItem.controlType == FlexibleHeaderConfiguratorControlTypeButton) {
return YES;
}
}
return NO;
}
#pragma mark - User actions
- (void)didChangeControl:(UIControl *)sender {
NSNumber *value = [self valueForControl:sender];
[self field:(FlexibleHeaderConfiguratorField)sender.tag didChangeValue:value];
}
- (void)didTapBackButton:(id)button {
[self.navigationController popViewControllerAnimated:YES];
}
@end
@implementation FlexibleHeaderConfiguratorExample (CatalogByConvention)
+ (NSDictionary *)catalogMetadata {
return @{
@"breadcrumbs" : @[ @"Flexible Header", @"Configurator" ],
@"primaryDemo" : @NO,
@"presentable" : @NO,
};
}
- (BOOL)catalogShouldHideNavigation {
return YES;
}
@end