Skip to content

Commit

Permalink
[FlexibleHeader] Fixes to support scroll views with Safe Area insets. (
Browse files Browse the repository at this point in the history
…#2063)

* [FlexibleHeader] Fixes to support scroll views with Safe Area insets.

* Fix typo

* [FlexibleHeader] Update header height when safe area changes.

* [FlexibleHeader] Only adjust sizes and margins on iOS11+.

* [FlexibleHeader] Bring back hardocded status bar const.

* [Catalog] Fix unintended hidden status bar when in landscape.

* Code reviews

* [Podspec] Add MaterialApplication as a dependency on MDCAppBar

* [FlexibleHeader] Make Xcode 8 stop complaining about unused parameter.

* [FlexibleHeader] Fix typo.

* [UnitTest] Remove hardcoded status bar height.
  • Loading branch information
Andrés committed Sep 29, 2017
1 parent 3fc78fd commit 80d054e
Show file tree
Hide file tree
Showing 11 changed files with 179 additions and 23 deletions.
1 change: 1 addition & 0 deletions MaterialComponents.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ Pod::Spec.new do |s|
sss.dependency "MaterialComponents/HeaderStackView"
sss.dependency "MaterialComponents/NavigationBar"
sss.dependency "MaterialComponents/Typography"
sss.dependency "MaterialComponents/private/Application"

# Flexible header + shadow
sss.dependency "MaterialComponents/FlexibleHeader"
Expand Down
2 changes: 0 additions & 2 deletions catalog/MDCCatalog/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
Expand Down
4 changes: 4 additions & 0 deletions catalog/MDCCatalog/MDCCatalogComponentsController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ class MDCCatalogComponentsController: UICollectionViewController, MDCInkTouchCon
return self.headerViewController
}

override var childViewControllerForStatusBarHidden: UIViewController? {
return self.headerViewController
}

#if swift(>=3.2)
@available(iOS 11, *)
override func viewSafeAreaInsetsDidChange() {
Expand Down
4 changes: 4 additions & 0 deletions catalog/MDCCatalog/MDCNodeListViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ class MDCNodeListViewController: CBCNodeListViewController {
override var childViewControllerForStatusBarStyle: UIViewController? {
return appBar.headerViewController
}

override var childViewControllerForStatusBarHidden: UIViewController? {
return appBar.headerViewController
}
}

// MARK: UIScrollViewDelegate
Expand Down
15 changes: 12 additions & 3 deletions components/AppBar/src/MDCAppBar.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

#import "MDCAppBarContainerViewController.h"

#import "MaterialApplication.h"
#import "MaterialFlexibleHeader.h"
#import "MaterialIcons+ic_arrow_back.h"
#import "MaterialRTL.h"
Expand All @@ -33,7 +34,7 @@
static NSString *const MDCAppBarHeaderViewControllerKey = @"MDCAppBarHeaderViewControllerKey";
static NSString *const MDCAppBarNavigationBarKey = @"MDCAppBarNavigationBarKey";
static NSString *const MDCAppBarHeaderStackViewKey = @"MDCAppBarHeaderStackViewKey";
static const CGFloat kStatusBarHeight = 20;
static const CGFloat kPreIOS11StatusBarHeight = 20;

// The Bundle for string resources.
static NSString *const kMaterialAppBarBundle = @"MaterialAppBar.bundle";
Expand Down Expand Up @@ -300,20 +301,28 @@ - (void)viewDidLoad {
[self.view addSubview:self.headerStackView];

// Bar stack expands vertically, but has a margin above it for the status bar.

NSArray<NSLayoutConstraint *> *horizontalConstraints = [NSLayoutConstraint
constraintsWithVisualFormat:[NSString stringWithFormat:@"H:|[%@]|", kBarStackKey]
options:0
metrics:nil
views:@{kBarStackKey : self.headerStackView}];
[self.view addConstraints:horizontalConstraints];

CGFloat topMargin = kPreIOS11StatusBarHeight;
#if defined(__IPHONE_11_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0)
if (@available(iOS 11.0, *)) {
// Starting from iOS 11, the top margin should be the actual status bar height.
// This is because the flexible header could be smaller in height when in landscape mode
// due to the status bar being hidden by default on some devices (like the iPhone X).
topMargin = [UIApplication mdc_safeSharedApplication].statusBarFrame.size.height;
}
#endif
NSArray<NSLayoutConstraint *> *verticalConstraints = [NSLayoutConstraint
constraintsWithVisualFormat:[NSString stringWithFormat:@"V:|-%@-[%@]|", kStatusBarHeightKey,
kBarStackKey]
options:0
metrics:@{
kStatusBarHeightKey : @(kStatusBarHeight)
kStatusBarHeightKey : @(topMargin)
}
views:@{kBarStackKey : self.headerStackView}];
[self.view addConstraints:verticalConstraints];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder {

- (void)commonMDCFlexibleHeaderViewControllerInit {
self.fhvc = [[MDCFlexibleHeaderViewController alloc] initWithNibName:nil bundle:nil];
self.fhvc.headerView.sharedWithManyScrollViews = YES;
[self addChildViewController:_fhvc];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ - (void)setupScrollViewContent {
UIColor *color =
[UIColor colorWithRed:(97.0 / 255.0) green:(97.0 / 255.0) blue:(97.0 / 255.0) alpha:1.0];
UIView *scrollViewContent =
[[UIView alloc] initWithFrame:CGRectMake(0, 0, self.scrollView.frame.size.width, 700)];
[[UIView alloc] initWithFrame:CGRectMake(0,
0,
self.scrollView.frame.size.width,
self.scrollView.frame.size.height * 2)];
scrollViewContent.autoresizingMask =
UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
UILabel *pullDownLabel = [[UILabel alloc]
Expand Down
121 changes: 113 additions & 8 deletions components/FlexibleHeader/src/MDCFlexibleHeaderView.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@

#import "MDCFlexibleHeaderView.h"

#import "MaterialApplication.h"
#import "private/MDCStatusBarShifter.h"

#if TARGET_IPHONE_SIMULATOR
float UIAnimationDragCoefficient(void); // Private API for simulator animation speed
#endif

static const CGFloat kExpectedStatusBarHeight = 20;
static const CGFloat kPreIOS11ExpectedStatusBarHeight = 20;

// The default maximum height for the header. Includes the status bar height.
static const CGFloat kFlexibleHeaderDefaultHeight = 76;
// The default maximum height for the header. Does not include the status bar height.
static const CGFloat kFlexibleHeaderDefaultHeight = 56;

// The maximum default opacity of the shadow.
static const float kDefaultVisibleShadowOpacity = 0.4f;
Expand Down Expand Up @@ -109,6 +110,12 @@ @interface MDCFlexibleHeaderScrollViewInfo : NSObject
// The amount injected into scrollIndicatorInsets.top
@property(nonatomic) CGFloat injectedTopScrollIndicatorInset;

// The adjustment we've made to account for the scroll view's Safe Area.
@property(nonatomic) CGFloat topSafeAreaInsetAdjustment;

// Whether or not we've adjusted the inset to account for the scroll view's Safe Area.
@property(nonatomic) BOOL hasTopSafeAreaInsetAdjustment;

@end

@implementation MDCFlexibleHeaderView {
Expand All @@ -125,6 +132,10 @@ @implementation MDCFlexibleHeaderView {
// is interacting with the header or if we're presently animating it.
BOOL _wantsToBeHidden;

// This will help us track if the size has been explicitly set or if we're using the defaults.
BOOL _hasExplicitlySetMinHeight;
BOOL _hasExplicitlySetMaxHeight;

// Shift behavior state

// Prevents delta calculations on first update pass.
Expand Down Expand Up @@ -287,8 +298,16 @@ - (void)commonMDCFlexibleHeaderViewInit {
_headerContentImportance = MDCFlexibleHeaderContentImportanceDefault;
_statusBarHintCanOverlapHeader = YES;

_minimumHeight = kFlexibleHeaderDefaultHeight;
_maximumHeight = kFlexibleHeaderDefaultHeight;
_minimumHeight = kFlexibleHeaderDefaultHeight + kPreIOS11ExpectedStatusBarHeight;
#if defined(__IPHONE_11_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0)
if (@available(iOS 11.0, *)) {
// Starting from iOS 11, we should adapt the size of this component to take into account
// the new status bar sizes and the "no status bar in landscape" functionality.
CGFloat statusBarHeight = [UIApplication mdc_safeSharedApplication].statusBarFrame.size.height;
_minimumHeight = kFlexibleHeaderDefaultHeight + statusBarHeight;
}
#endif
_maximumHeight = _minimumHeight;
_visibleShadowOpacity = kDefaultVisibleShadowOpacity;
_canOverExtend = YES;

Expand Down Expand Up @@ -414,6 +433,36 @@ - (void)setBackgroundColor:(UIColor *)backgroundColor {
_defaultShadowLayer.backgroundColor = self.backgroundColor.CGColor;
}

- (void)safeAreaInsetsDidChange {
#if defined(__IPHONE_11_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0)
if (@available(iOS 11.0, *)) {
[self fhv_adjustInsetsForSafeAreaInScrollView:_trackingScrollView];

// If the min/max height hasn't been explicitly set, we need to update the layout to reflect
// the change in the Safe Area insets.
if (_hasExplicitlySetMinHeight && _hasExplicitlySetMaxHeight) {
return;
}
if (!_hasExplicitlySetMinHeight) {
// Edge case for UITableViewController: If we're a subview of _trackingScrollView,
// we need to get the Safe Area insets from there and not use ours.
if ([self isDescendantOfView:_trackingScrollView]) {
_minimumHeight = kFlexibleHeaderDefaultHeight + _trackingScrollView.safeAreaInsets.top;
} else {
_minimumHeight = kFlexibleHeaderDefaultHeight + self.safeAreaInsets.top;
}
}
if (!_hasExplicitlySetMaxHeight) {
_maximumHeight = _minimumHeight;
}
if (_maximumHeight < _minimumHeight) {
_maximumHeight = _minimumHeight;
}
[self fhv_updateLayout];
}
#endif
}

#pragma mark - Private (fhv_ prefix)

- (void)fhv_removeInsetsFromScrollView:(UIScrollView *)scrollView {
Expand Down Expand Up @@ -468,6 +517,42 @@ - (MDCFlexibleHeaderScrollViewInfo *)fhv_addInsetsToScrollView:(UIScrollView *)s
return info;
}

- (void)fhv_adjustInsetsForSafeAreaInScrollView:(UIScrollView *)scrollView {
if (!scrollView) {
return;
}
#if defined(__IPHONE_11_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0)
if (@available(iOS 11.0, *)) {
if (scrollView.contentInsetAdjustmentBehavior == UIScrollViewContentInsetAdjustmentAlways ||
scrollView.contentInsetAdjustmentBehavior == UIScrollViewContentInsetAdjustmentAutomatic) {

// For Automatic: If contentInset and adjustedContentInset are the same, no-op.
if (scrollView.contentInset.top == scrollView.adjustedContentInset.top) {
return;
}

// If the injected Safe Area inset hasn't changed, no-op.
MDCFlexibleHeaderScrollViewInfo *info = [_trackedScrollViews objectForKey:scrollView];
if (!info || info.topSafeAreaInsetAdjustment == scrollView.safeAreaInsets.top) {
return;
}

UIEdgeInsets insets = scrollView.contentInset;
if (info.hasTopSafeAreaInsetAdjustment) {
insets.top += info.topSafeAreaInsetAdjustment;
}
info.topSafeAreaInsetAdjustment = scrollView.safeAreaInsets.top;
insets.top -= info.topSafeAreaInsetAdjustment;
info.hasTopSafeAreaInsetAdjustment = YES;
scrollView.contentInset = insets;

// Update injectedTopContentInset to account for the scroll view's Safe Area inset.
info.injectedTopContentInset = _maximumHeight - info.topSafeAreaInsetAdjustment;
}
}
#endif
}

- (void)fhv_updateShadowPath {
UIBezierPath *path =
[UIBezierPath bezierPathWithRect:CGRectInset(self.bounds, -self.layer.shadowRadius, 0)];
Expand Down Expand Up @@ -509,7 +594,8 @@ - (CGFloat)fhv_projectedHeaderBottomEdge {

- (CGFloat)fhv_accumulatorMax {
BOOL shouldCollapseToStatusBar = [self fhv_shouldCollapseToStatusBar];
return (shouldCollapseToStatusBar ? _minimumHeight - kExpectedStatusBarHeight : _minimumHeight);
CGFloat statusBarHeight = [UIApplication mdc_safeSharedApplication].statusBarFrame.size.height;
return (shouldCollapseToStatusBar ? _minimumHeight - statusBarHeight : _minimumHeight);
}

#pragma mark Logical short forms
Expand Down Expand Up @@ -560,7 +646,9 @@ - (void)fhv_recalculatePhase {
_scrollPhaseValue = frame.origin.y + _minimumHeight;
CGFloat adjustedHeight = _minimumHeight;
if ([self fhv_shouldCollapseToStatusBar]) {
adjustedHeight -= kExpectedStatusBarHeight;
CGFloat statusBarHeight =
[UIApplication mdc_safeSharedApplication].statusBarFrame.size.height;
adjustedHeight -= statusBarHeight;
}
if (adjustedHeight > 0) {
_scrollPhasePercentage = -frame.origin.y / adjustedHeight;
Expand Down Expand Up @@ -718,8 +806,15 @@ - (void)fhv_accumulatorDidChange {
scrollIndicatorInsets.top -= _trackingInfo.injectedTopScrollIndicatorInset;

_trackingInfo.injectedTopScrollIndicatorInset = frame.size.height - boundedAccumulator;
scrollIndicatorInsets.top += _trackingInfo.injectedTopScrollIndicatorInset;

// If on iOS 11, take into account the scroll view's Safe Area insets.
#if defined(__IPHONE_11_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0)
if (@available(iOS 11.0, *)) {
_trackingInfo.injectedTopScrollIndicatorInset -= _trackingScrollView.safeAreaInsets.top;
}
#endif

scrollIndicatorInsets.top += _trackingInfo.injectedTopScrollIndicatorInset;
_trackingScrollView.scrollIndicatorInsets = scrollIndicatorInsets;
}

Expand Down Expand Up @@ -985,6 +1080,10 @@ - (void)setTrackingScrollView:(UIScrollView *)trackingScrollView {
if (!_sharedWithManyScrollViews || !_trackingInfo) {
[self fhv_addInsetsToScrollView:_trackingScrollView];
}

// Before we update the layout, take into account the scroll view's Safe Area insets.
[self fhv_adjustInsetsForSafeAreaInScrollView:_trackingScrollView];

void (^animate)(void) = ^{
[self fhv_updateLayout];
};
Expand All @@ -1009,6 +1108,10 @@ - (void)setTrackingScrollView:(UIScrollView *)trackingScrollView {
}

- (void)trackingScrollViewDidScroll {
// This could've been triggered by a change in the scroll view's Safe Area, so we need to check
// if it did. If it didn't, this is a no-op.
[self fhv_adjustInsetsForSafeAreaInScrollView:_trackingScrollView];

[self fhv_contentOffsetDidChange];
}

Expand Down Expand Up @@ -1140,6 +1243,7 @@ - (void)stopForwardingTouchEventsForView:(UIView *)view {
}

- (void)setMinimumHeight:(CGFloat)minimumHeight {
_hasExplicitlySetMinHeight = YES;
if (_minimumHeight == minimumHeight) {
return;
}
Expand All @@ -1154,6 +1258,7 @@ - (void)setMinimumHeight:(CGFloat)minimumHeight {
}

- (void)setMaximumHeight:(CGFloat)maximumHeight {
_hasExplicitlySetMaxHeight = YES;
if (_maximumHeight == maximumHeight) {
return;
}
Expand Down
11 changes: 9 additions & 2 deletions components/FlexibleHeader/src/MDCFlexibleHeaderViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

#import "MDCFlexibleHeaderViewController.h"

#import "MaterialApplication.h"
#import "MDCFlexibleHeaderContainerViewController.h"
#import "MDCFlexibleHeaderView.h"
#import "MDFTextAccessibility.h"
Expand Down Expand Up @@ -145,7 +146,9 @@ - (void)viewWillAppear:(BOOL)animated {
}

- (UIStatusBarStyle)preferredStatusBarStyle {
return (ShouldUseLightStatusBarOnBackgroundColor(_headerView.backgroundColor)
UIColor *backgroundColor =
[MDCFlexibleHeaderView appearance].backgroundColor ?: _headerView.backgroundColor;
return (ShouldUseLightStatusBarOnBackgroundColor(backgroundColor)
? UIStatusBarStyleLightContent
: UIStatusBarStyleDefault);
}
Expand Down Expand Up @@ -223,9 +226,13 @@ - (void)updateTopLayoutGuide {
}

- (CGFloat)headerViewControllerHeight {
BOOL shiftEnabledForStatusBar =
_headerView.shiftBehavior == MDCFlexibleHeaderShiftBehaviorEnabledWithStatusBar;
CGFloat statusBarHeight =
[UIApplication mdc_safeSharedApplication].statusBarFrame.size.height;
CGFloat height =
MAX(_headerView.frame.origin.y + _headerView.frame.size.height,
_headerView.shiftBehavior == MDCFlexibleHeaderShiftBehaviorEnabledWithStatusBar ? 0 : 20);
shiftEnabledForStatusBar ? 0 : statusBarHeight);
return height;
}

Expand Down
Loading

0 comments on commit 80d054e

Please sign in to comment.