Commit 343f58e4 authored by Kurt Horimoto's avatar Kurt Horimoto Committed by Commit Bot

[iOS] Fix toolbar container progress logic.

The previous implementation of ToolbarContainer scaled
each toolbar's interpolation progress at the same rate,
resulting in accordion-like behavior where all toolbars
were expanded and collapsed at the same time.  The desired
effect is to have telescopic behavior, where the toolbars
are extended one at a time.  This is needed on iPad, where
a scroll event should first reduce the height of the tab
strip to 0.0 before reducing the height of the primary
toolbar.

This CL adds logic to convert the [0.0, 1.0] fullscreen
progress range for the overall stack to individual progress
values for each toolbar, such that the last toolbar is
expanded first, and the previous toolbars don't start
expanding until all subsequent ones are fully expanded.
The range of stack-level progress values corresponding to
a particular toolbar is proportional to that toolbar's
height delta relative to the height delta of the overall
stack.

In addition, this CL also introduces HeightRange, a simple
container object that can be used to hold max and min heights
and perform interpolation calculations.

Bug: 880672, 895766
Cq-Include-Trybots: luci.chromium.try:ios-simulator-cronet;luci.chromium.try:ios-simulator-full-configs
Change-Id: I1205ec75ac3e9a0e57dc19d1ebcc1f1740f8aeac
Reviewed-on: https://chromium-review.googlesource.com/c/1231933
Commit-Queue: Kurt Horimoto <kkhorimoto@chromium.org>
Reviewed-by: default avatarGauthier Ambard <gambard@chromium.org>
Cr-Commit-Position: refs/heads/master@{#600903}
parent 68b5626a
...@@ -41,10 +41,13 @@ source_set("ui") { ...@@ -41,10 +41,13 @@ source_set("ui") {
sources = [ sources = [
"collapsing_toolbar_height_constraint.h", "collapsing_toolbar_height_constraint.h",
"collapsing_toolbar_height_constraint.mm", "collapsing_toolbar_height_constraint.mm",
"collapsing_toolbar_height_constraint_delegate.h",
"toolbar_container_view.h", "toolbar_container_view.h",
"toolbar_container_view.mm", "toolbar_container_view.mm",
"toolbar_container_view_controller.h", "toolbar_container_view_controller.h",
"toolbar_container_view_controller.mm", "toolbar_container_view_controller.mm",
"toolbar_height_range.h",
"toolbar_height_range.mm",
] ]
configs += [ "//build/config/compiler:enable_arc" ] configs += [ "//build/config/compiler:enable_arc" ]
...@@ -63,6 +66,7 @@ source_set("unit_tests") { ...@@ -63,6 +66,7 @@ source_set("unit_tests") {
sources = [ sources = [
"collapsing_toolbar_height_constraint_unittest.mm", "collapsing_toolbar_height_constraint_unittest.mm",
"toolbar_container_view_controller_unittest.mm", "toolbar_container_view_controller_unittest.mm",
"toolbar_height_range_unittest.mm",
] ]
configs += [ "//build/config/compiler:enable_arc" ] configs += [ "//build/config/compiler:enable_arc" ]
......
...@@ -7,34 +7,41 @@ ...@@ -7,34 +7,41 @@
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
#import "ios/chrome/browser/ui/toolbar_container/toolbar_height_range.h"
@protocol CollapsingToolbarHeightConstraintDelegate;
// A constraint that scales between a collapsed and expanded height value. // A constraint that scales between a collapsed and expanded height value.
@interface CollapsingToolbarHeightConstraint : NSLayoutConstraint @interface CollapsingToolbarHeightConstraint : NSLayoutConstraint
// Returns a constraint that manages the height of |view|. If |view| // Returns a constraint that manages the height of |view|. If |view| conforms
// conforms to the ToolbarCollapsing protocol, the collapsed and expanded // to ToolbarCollapsing, updating |progress| will scale between its collapsed
// heights are set using those return values. Otherwise, the intrinsic height // and expanded heights. Otherwise, the constraint will lock |view|'s height
// is used as both the collapsed and expanded height. // to its intrinisic content height. The height range can be increased using
// |additionalHeight|.
+ (nullable instancetype)constraintWithView:(nonnull UIView*)view; + (nullable instancetype)constraintWithView:(nonnull UIView*)view;
// The collapsed and expanded toolbar heights.
@property(nonatomic, readonly) CGFloat collapsedHeight;
@property(nonatomic, readonly) CGFloat expandedHeight;
// Used to add additional height to the toolbar. // Used to add additional height to the toolbar.
@property(nonatomic, assign) CGFloat additionalHeight; @property(nonatomic, assign) CGFloat additionalHeight;
// Whether the additional height should be collapsed. When set to YES, the // Whether the additional height should be collapsed.
// view's height ranges from |collapsedHeight| to |expandedHeight| +
// |additionalHeight|. When set to NO, the view's height ranges from
// |additionalHeight| + |collapsedHeight| to |additionalHeight| +
// |expandedHeight|.
@property(nonatomic, assign) BOOL collapsesAdditionalHeight; @property(nonatomic, assign) BOOL collapsesAdditionalHeight;
// The height range for the constraint. If the constrained view conforms to
// ToolbarCollapsing, the range will be populated using the collapsed and
// expanded toolbar heights from that protocol, otherwise the intrinsic content
// height is used. |additionalHeight| and is added to the max height, and
// optionally added to the min height if |collapsesAdditionalHeight| is NO.
@property(nonatomic, readonly)
const toolbar_container::HeightRange& heightRange;
// The interpolation progress within the height range to use for the // The interpolation progress within the height range to use for the
// constraint's constant. // constraint's constant. The value is clamped between 0.0 and 1.0.
@property(nonatomic, assign) CGFloat progress; @property(nonatomic, assign) CGFloat progress;
// Returns the height of the toolbar at |progress| // The constraint's delegate.
- (CGFloat)toolbarHeightForProgress:(CGFloat)progress; @property(nonatomic, weak, nullable)
id<CollapsingToolbarHeightConstraintDelegate>
delegate;
@end @end
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
#include "base/logging.h" #include "base/logging.h"
#include "base/numerics/ranges.h" #include "base/numerics/ranges.h"
#import "ios/chrome/browser/ui/toolbar_container/collapsing_toolbar_height_constraint_delegate.h"
#import "ios/chrome/browser/ui/toolbar_container/toolbar_collapsing.h" #import "ios/chrome/browser/ui/toolbar_container/toolbar_collapsing.h"
#include "ios/chrome/browser/ui/ui_util.h" #include "ios/chrome/browser/ui/ui_util.h"
...@@ -21,20 +22,29 @@ const CGFloat kMinProgress = 0.0; ...@@ -21,20 +22,29 @@ const CGFloat kMinProgress = 0.0;
const CGFloat kMaxProgress = 1.0; const CGFloat kMaxProgress = 1.0;
} // namespace } // namespace
@interface CollapsingToolbarHeightConstraint () using toolbar_container::HeightRange;
// Redefine as readwrite.
@property(nonatomic, readwrite) CGFloat collapsedHeight; @interface CollapsingToolbarHeightConstraint () {
@property(nonatomic, readwrite) CGFloat expandedHeight; // Backing variable for property of same name.
HeightRange _heightRange;
}
// The height values extracted from the constrained view. If the view conforms
// to ToolbarCollapsing, these will be the values from that protocol, and will
// be updated using KVO if those values change. Otherwise, they will both be
// equal to the intrinsic height of the view.
@property(nonatomic, readwrite) CGFloat collapsedToolbarHeight;
@property(nonatomic, readwrite) CGFloat expandedToolbarHeight;
// The collapsing toolbar whose height range is being observed. // The collapsing toolbar whose height range is being observed.
@property(nonatomic, weak) UIView<ToolbarCollapsing>* collapsingToolbar; @property(nonatomic, weak) UIView<ToolbarCollapsing>* collapsingToolbar;
@end @end
@implementation CollapsingToolbarHeightConstraint @implementation CollapsingToolbarHeightConstraint
@synthesize collapsedHeight = _collapsedHeight; @synthesize collapsedToolbarHeight = _collapsedToolbarHeight;
@synthesize expandedHeight = _expandedHeight; @synthesize expandedToolbarHeight = _expandedToolbarHeight;
@synthesize additionalHeight = _additionalHeight; @synthesize additionalHeight = _additionalHeight;
@synthesize collapsesAdditionalHeight = _collapsesAdditionalHeight; @synthesize collapsesAdditionalHeight = _collapsesAdditionalHeight;
@synthesize progress = _progress; @synthesize progress = _progress;
@synthesize delegate = _delegate;
@synthesize collapsingToolbar = _collapsingToolbar; @synthesize collapsingToolbar = _collapsingToolbar;
+ (instancetype)constraintWithView:(UIView*)view { + (instancetype)constraintWithView:(UIView*)view {
...@@ -52,11 +62,11 @@ const CGFloat kMaxProgress = 1.0; ...@@ -52,11 +62,11 @@ const CGFloat kMaxProgress = 1.0;
static_cast<UIView<ToolbarCollapsing>*>(view); static_cast<UIView<ToolbarCollapsing>*>(view);
} else { } else {
CGFloat intrinsicHeight = view.intrinsicContentSize.height; CGFloat intrinsicHeight = view.intrinsicContentSize.height;
constraint.collapsedHeight = intrinsicHeight; constraint.collapsedToolbarHeight = intrinsicHeight;
constraint.expandedHeight = intrinsicHeight; constraint.expandedToolbarHeight = intrinsicHeight;
[constraint updateToolbarHeightRange];
} }
constraint.progress = 1.0; constraint.progress = 1.0;
[constraint updateHeight];
return constraint; return constraint;
} }
...@@ -71,32 +81,23 @@ const CGFloat kMaxProgress = 1.0; ...@@ -71,32 +81,23 @@ const CGFloat kMaxProgress = 1.0;
[self stopObservingCollapsingToolbar]; [self stopObservingCollapsingToolbar];
} }
- (void)setCollapsedHeight:(CGFloat)collapsedHeight {
if (AreCGFloatsEqual(_collapsedHeight, collapsedHeight))
return;
_collapsedHeight = collapsedHeight;
[self updateHeight];
}
- (void)setExpandedHeight:(CGFloat)expandedHeight {
if (AreCGFloatsEqual(_expandedHeight, expandedHeight))
return;
_expandedHeight = expandedHeight;
[self updateHeight];
}
- (void)setAdditionalHeight:(CGFloat)additionalHeight { - (void)setAdditionalHeight:(CGFloat)additionalHeight {
if (AreCGFloatsEqual(_additionalHeight, additionalHeight)) if (AreCGFloatsEqual(_additionalHeight, additionalHeight))
return; return;
_additionalHeight = additionalHeight; _additionalHeight = additionalHeight;
[self updateHeight]; [self updateToolbarHeightRange];
} }
- (void)setCollapsesAdditionalHeight:(BOOL)collapsesAdditionalHeight { - (void)setCollapsesAdditionalHeight:(BOOL)collapsesAdditionalHeight {
if (_collapsesAdditionalHeight == collapsesAdditionalHeight) if (_collapsesAdditionalHeight == collapsesAdditionalHeight)
return; return;
_collapsesAdditionalHeight = collapsesAdditionalHeight; _collapsesAdditionalHeight = collapsesAdditionalHeight;
[self updateHeight]; [self updateToolbarHeightRange];
}
- (const HeightRange&)heightRange {
// Custom getter is needed to support the C++ reference type.
return _heightRange;
} }
- (void)setProgress:(CGFloat)progress { - (void)setProgress:(CGFloat)progress {
...@@ -104,16 +105,15 @@ const CGFloat kMaxProgress = 1.0; ...@@ -104,16 +105,15 @@ const CGFloat kMaxProgress = 1.0;
if (AreCGFloatsEqual(_progress, progress)) if (AreCGFloatsEqual(_progress, progress))
return; return;
_progress = progress; _progress = progress;
[self updateHeight]; [self updateHeightConstant];
} }
- (void)setCollapsingToolbar:(UIView<ToolbarCollapsing>*)collapsingToolbar { - (void)setCollapsingToolbar:(UIView<ToolbarCollapsing>*)collapsingToolbar {
if (_collapsingToolbar == collapsingToolbar) if (_collapsingToolbar == collapsingToolbar)
return; return;
[self stopObservingCollapsingToolbar]; [self stopObservingCollapsingToolbar];
_collapsingToolbar = collapsingToolbar; _collapsingToolbar = collapsingToolbar;
[self updateToolbarHeightRange]; [self updateCollapsingToolbarHeights];
if (self.active) if (self.active)
[self startObservingCollapsingToolbar]; [self startObservingCollapsingToolbar];
} }
...@@ -122,8 +122,8 @@ const CGFloat kMaxProgress = 1.0; ...@@ -122,8 +122,8 @@ const CGFloat kMaxProgress = 1.0;
- (CGFloat)toolbarHeightForProgress:(CGFloat)progress { - (CGFloat)toolbarHeightForProgress:(CGFloat)progress {
progress = base::ClampToRange(progress, kMinProgress, kMaxProgress); progress = base::ClampToRange(progress, kMinProgress, kMaxProgress);
CGFloat base = self.collapsedHeight; CGFloat base = self.collapsedToolbarHeight;
CGFloat range = self.expandedHeight - self.collapsedHeight; CGFloat range = self.expandedToolbarHeight - self.collapsedToolbarHeight;
if (self.collapsesAdditionalHeight) { if (self.collapsesAdditionalHeight) {
range += self.additionalHeight; range += self.additionalHeight;
} else { } else {
...@@ -138,7 +138,7 @@ const CGFloat kMaxProgress = 1.0; ...@@ -138,7 +138,7 @@ const CGFloat kMaxProgress = 1.0;
ofObject:(id)object ofObject:(id)object
change:(NSDictionary*)change change:(NSDictionary*)change
context:(void*)context { context:(void*)context {
[self updateToolbarHeightRange]; [self updateCollapsingToolbarHeights];
} }
#pragma mark - KVO Helpers #pragma mark - KVO Helpers
...@@ -166,15 +166,32 @@ const CGFloat kMaxProgress = 1.0; ...@@ -166,15 +166,32 @@ const CGFloat kMaxProgress = 1.0;
#pragma mark - Private #pragma mark - Private
// Updates the constraint using the collapsing toolbar's height range. // Upates the collapsed and expanded heights from self.collapsingToolbar.
- (void)updateCollapsingToolbarHeights {
self.collapsedToolbarHeight = self.collapsingToolbar.collapsedToolbarHeight;
self.expandedToolbarHeight = self.collapsingToolbar.expandedToolbarHeight;
[self updateToolbarHeightRange];
}
// Updates the height range using the current collapsing toolbar height values
// and additional height behavior.
- (void)updateToolbarHeightRange { - (void)updateToolbarHeightRange {
self.collapsedHeight = self.collapsingToolbar.collapsedToolbarHeight; HeightRange oldHeightRange = self.heightRange;
self.expandedHeight = self.collapsingToolbar.expandedToolbarHeight; CGFloat minHeight =
self.collapsedToolbarHeight +
(self.collapsesAdditionalHeight ? 0.0 : self.additionalHeight);
CGFloat maxHeight = self.expandedToolbarHeight + self.additionalHeight;
_heightRange = HeightRange(minHeight, maxHeight);
if (_heightRange == oldHeightRange)
return;
[self updateHeightConstant];
[self.delegate collapsingHeightConstraint:self
didUpdateFromHeightRange:oldHeightRange];
} }
// Updates the constraint's constant // Updates the constraint's constant
- (void)updateHeight { - (void)updateHeightConstant {
self.constant = [self toolbarHeightForProgress:self.progress]; self.constant = self.heightRange.GetInterpolatedHeight(self.progress);
} }
@end @end
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef IOS_CHROME_BROWSER_UI_TOOLBAR_CONTAINER_COLLAPSING_TOOLBAR_HEIGHT_CONSTRAINT_DELEGATE_H_
#define IOS_CHROME_BROWSER_UI_TOOLBAR_CONTAINER_COLLAPSING_TOOLBAR_HEIGHT_CONSTRAINT_DELEGATE_H_
#import <Foundation/Foundation.h>
@class CollapsingToolbarHeightConstraint;
namespace toolbar_container {
class HeightRange;
} // namespace toolbar_container
// The delegate for the collapsing height constraint.
@protocol CollapsingToolbarHeightConstraintDelegate<NSObject>
// Called when |constraint|'s height range is changed from |oldHeightRange|.
- (void)collapsingHeightConstraint:
(nonnull CollapsingToolbarHeightConstraint*)constraint
didUpdateFromHeightRange:
(const toolbar_container::HeightRange&)oldHeightRange;
@end
#endif // IOS_CHROME_BROWSER_UI_TOOLBAR_CONTAINER_COLLAPSING_TOOLBAR_HEIGHT_CONSTRAINT_DELEGATE_H_
...@@ -78,48 +78,6 @@ class CollapsingToolbarHeightConstraintTest : public PlatformTest { ...@@ -78,48 +78,6 @@ class CollapsingToolbarHeightConstraintTest : public PlatformTest {
NSMutableArray<NSLayoutConstraint*>* constraints_ = nil; NSMutableArray<NSLayoutConstraint*>* constraints_ = nil;
}; };
// Tests that |-toolbarHeightForProgress:| returns the expected values.
TEST_F(CollapsingToolbarHeightConstraintTest, ToolbarHeightForProgress) {
CollapsingView* view = [[CollapsingView alloc] initWithFrame:CGRectZero];
view.expandedToolbarHeight = 100.0;
view.collapsedToolbarHeight = 50.0;
CollapsingToolbarHeightConstraint* constraint = AddViewToContainer(view);
// Test collapsing toolbar.
EXPECT_EQ([constraint toolbarHeightForProgress:1.0], 100.0);
EXPECT_EQ([constraint toolbarHeightForProgress:0.5], 75.0);
EXPECT_EQ([constraint toolbarHeightForProgress:0.0], 50.0);
// Tests with collapsing additional height.
constraint.additionalHeight = 100.0;
constraint.collapsesAdditionalHeight = YES;
EXPECT_EQ([constraint toolbarHeightForProgress:1.0], 200.0);
EXPECT_EQ([constraint toolbarHeightForProgress:0.5], 125.0);
EXPECT_EQ([constraint toolbarHeightForProgress:0.0], 50.0);
// Tests with non-collapsing additional height.
constraint.collapsesAdditionalHeight = NO;
EXPECT_EQ([constraint toolbarHeightForProgress:1.0], 200.0);
EXPECT_EQ([constraint toolbarHeightForProgress:0.5], 175.0);
EXPECT_EQ([constraint toolbarHeightForProgress:0.0], 150.0);
// Test non-collapsing toolbar.
constraint.additionalHeight = 0.0;
view.collapsedToolbarHeight = 100.0;
EXPECT_EQ([constraint toolbarHeightForProgress:1.0], 100.0);
EXPECT_EQ([constraint toolbarHeightForProgress:0.5], 100.0);
EXPECT_EQ([constraint toolbarHeightForProgress:0.0], 100.0);
// Tests with collapsing additional height.
constraint.additionalHeight = 100.0;
constraint.collapsesAdditionalHeight = YES;
EXPECT_EQ([constraint toolbarHeightForProgress:1.0], 200.0);
EXPECT_EQ([constraint toolbarHeightForProgress:0.5], 150.0);
EXPECT_EQ([constraint toolbarHeightForProgress:0.0], 100.0);
// Tests with non-collapsing additional height.
constraint.collapsesAdditionalHeight = NO;
EXPECT_EQ([constraint toolbarHeightForProgress:1.0], 200.0);
EXPECT_EQ([constraint toolbarHeightForProgress:0.5], 200.0);
EXPECT_EQ([constraint toolbarHeightForProgress:0.0], 200.0);
}
// Tests interpolating the height value of a collapsing view. // Tests interpolating the height value of a collapsing view.
TEST_F(CollapsingToolbarHeightConstraintTest, CollapsingConstraint) { TEST_F(CollapsingToolbarHeightConstraintTest, CollapsingConstraint) {
CollapsingView* view = [[CollapsingView alloc] initWithFrame:CGRectZero]; CollapsingView* view = [[CollapsingView alloc] initWithFrame:CGRectZero];
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
#import "ios/chrome/browser/ui/fullscreen/fullscreen_controller_factory.h" #import "ios/chrome/browser/ui/fullscreen/fullscreen_controller_factory.h"
#import "ios/chrome/browser/ui/fullscreen/fullscreen_ui_updater.h" #import "ios/chrome/browser/ui/fullscreen/fullscreen_ui_updater.h"
#import "ios/chrome/browser/ui/toolbar_container/toolbar_container_view_controller.h" #import "ios/chrome/browser/ui/toolbar_container/toolbar_container_view_controller.h"
#import "ios/chrome/browser/ui/toolbar_container/toolbar_height_range.h"
#if !defined(__has_feature) || !__has_feature(objc_arc) #if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support." #error "This file requires ARC support."
...@@ -63,8 +64,11 @@ ...@@ -63,8 +64,11 @@
#pragma mark - Public #pragma mark - Public
- (CGFloat)toolbarStackHeightForFullscreenProgress:(CGFloat)progress { - (CGFloat)toolbarStackHeightForFullscreenProgress:(CGFloat)progress {
return [self.containerViewController if (!self.started)
toolbarStackHeightForFullscreenProgress:progress]; return 0.0;
const toolbar_container::HeightRange& stackHeightRange =
self.containerViewController.heightRange;
return stackHeightRange.GetInterpolatedHeight(progress);
} }
#pragma mark - ChromeCoordinator #pragma mark - ChromeCoordinator
......
...@@ -9,6 +9,10 @@ ...@@ -9,6 +9,10 @@
#import "ios/chrome/browser/ui/fullscreen/fullscreen_ui_element.h" #import "ios/chrome/browser/ui/fullscreen/fullscreen_ui_element.h"
namespace toolbar_container {
class HeightRange;
} // namespace toolbar_container
// The layout orientation for a toolbar container. // The layout orientation for a toolbar container.
enum class ToolbarContainerOrientation { kTopToBottom, kBottomToTop }; enum class ToolbarContainerOrientation { kTopToBottom, kBottomToTop };
...@@ -26,9 +30,10 @@ enum class ToolbarContainerOrientation { kTopToBottom, kBottomToTop }; ...@@ -26,9 +30,10 @@ enum class ToolbarContainerOrientation { kTopToBottom, kBottomToTop };
// The toolbar view controllers being managed by this container. // The toolbar view controllers being managed by this container.
@property(nonatomic, strong) NSArray<UIViewController*>* toolbars; @property(nonatomic, strong) NSArray<UIViewController*>* toolbars;
// Returns the height of the toolbar views managed by this container at // The height range of the overall stack. It is calculated using the collapsed
// |progress|. // and expanded heights of the views managed by |toolbars|.
- (CGFloat)toolbarStackHeightForFullscreenProgress:(CGFloat)progress; @property(nonatomic, readonly)
const toolbar_container::HeightRange& heightRange;
@end @end
......
...@@ -4,11 +4,15 @@ ...@@ -4,11 +4,15 @@
#import "ios/chrome/browser/ui/toolbar_container/toolbar_container_view_controller.h" #import "ios/chrome/browser/ui/toolbar_container/toolbar_container_view_controller.h"
#include <vector>
#include "base/logging.h" #include "base/logging.h"
#include "base/mac/foundation_util.h" #include "base/mac/foundation_util.h"
#import "ios/chrome/browser/ui/fullscreen/fullscreen_animator.h" #import "ios/chrome/browser/ui/fullscreen/fullscreen_animator.h"
#import "ios/chrome/browser/ui/toolbar_container/collapsing_toolbar_height_constraint.h" #import "ios/chrome/browser/ui/toolbar_container/collapsing_toolbar_height_constraint.h"
#import "ios/chrome/browser/ui/toolbar_container/collapsing_toolbar_height_constraint_delegate.h"
#import "ios/chrome/browser/ui/toolbar_container/toolbar_container_view.h" #import "ios/chrome/browser/ui/toolbar_container/toolbar_container_view.h"
#import "ios/chrome/browser/ui/toolbar_container/toolbar_height_range.h"
#include "ios/chrome/browser/ui/ui_util.h" #include "ios/chrome/browser/ui/ui_util.h"
#import "ios/chrome/common/ui_util/constraints_ui_util.h" #import "ios/chrome/common/ui_util/constraints_ui_util.h"
...@@ -16,13 +20,31 @@ ...@@ -16,13 +20,31 @@
#error "This file requires ARC support." #error "This file requires ARC support."
#endif #endif
@interface ToolbarContainerViewController () using toolbar_container::HeightRange;
@interface ToolbarContainerViewController ()<
CollapsingToolbarHeightConstraintDelegate> {
// Backing variables for properties of same name.
HeightRange _heightRange;
std::vector<CGFloat> _toolbarExpansionStartProgresses;
}
// The constraint managing the height of the container. // The constraint managing the height of the container.
@property(nonatomic, strong, readonly) NSLayoutConstraint* heightConstraint; @property(nonatomic, strong, readonly) NSLayoutConstraint* heightConstraint;
// The height constraints for the toolbar views. // The height constraints for the toolbar views.
@property(nonatomic, strong, readonly) @property(nonatomic, strong, readonly)
NSMutableArray<CollapsingToolbarHeightConstraint*>* NSMutableArray<CollapsingToolbarHeightConstraint*>*
toolbarHeightConstraints; toolbarHeightConstraints;
// The fullscreen progresses at which the toolbars begin expanding. As the
// fullscreen progress goes from 0.0 to 1.0, the toolbars are expanded in the
// reverse of order of self.toolbars so that the toolbar closest to the page
// content is expanded first for scroll events. Each toolbar is expanded for a
// portion of the [0.0, 1.0] progress range proportional to its height delta
// relative to the overall height delta of the toolbar stack. This creates the
// effect of the overall stack height adjusting linearly while each individual
// toolbar's height is adjusted sequentially.
@property(nonatomic, assign, readonly)
std::vector<CGFloat>& toolbarExpansionStartProgresses;
// Returns the height constraint for the first toolbar in self.toolbars. // Returns the height constraint for the first toolbar in self.toolbars.
@property(nonatomic, readonly) @property(nonatomic, readonly)
CollapsingToolbarHeightConstraint* firstToolbarHeightConstraint; CollapsingToolbarHeightConstraint* firstToolbarHeightConstraint;
...@@ -40,6 +62,16 @@ ...@@ -40,6 +62,16 @@
#pragma mark - Accessors #pragma mark - Accessors
- (const HeightRange&)heightRange {
// Custom getter is needed to support the C++ reference type.
return _heightRange;
}
- (std::vector<CGFloat>&)toolbarExpansionStartProgresses {
// Custom getter is needed to support the C++ reference type.
return _toolbarExpansionStartProgresses;
}
- (CollapsingToolbarHeightConstraint*)firstToolbarHeightConstraint { - (CollapsingToolbarHeightConstraint*)firstToolbarHeightConstraint {
if (!self.viewLoaded || !self.toolbars.count) if (!self.viewLoaded || !self.toolbars.count)
return nil; return nil;
...@@ -49,35 +81,57 @@ ...@@ -49,35 +81,57 @@
} }
- (void)setAdditionalStackHeight:(CGFloat)additionalStackHeight { - (void)setAdditionalStackHeight:(CGFloat)additionalStackHeight {
DCHECK_GE(additionalStackHeight, 0.0);
if (AreCGFloatsEqual(_additionalStackHeight, additionalStackHeight)) if (AreCGFloatsEqual(_additionalStackHeight, additionalStackHeight))
return; return;
_additionalStackHeight = additionalStackHeight; _additionalStackHeight = additionalStackHeight;
self.firstToolbarHeightConstraint.additionalHeight = _additionalStackHeight; self.firstToolbarHeightConstraint.additionalHeight = _additionalStackHeight;
[self updateHeightConstraint];
} }
#pragma mark - Public #pragma mark - CollapsingToolbarHeightConstraintDelegate
- (CGFloat)toolbarStackHeightForFullscreenProgress:(CGFloat)progress { - (void)collapsingHeightConstraint:
CGFloat height = 0.0; (CollapsingToolbarHeightConstraint*)constraint
for (CollapsingToolbarHeightConstraint* constraint in self didUpdateFromHeightRange:
.toolbarHeightConstraints) { (const toolbar_container::HeightRange&)oldHeightRange {
height += [constraint toolbarHeightForProgress:progress]; [self updateHeightRangeWithRange:self.heightRange + constraint.heightRange -
} oldHeightRange];
return height;
} }
#pragma mark - FullscreenUIElement #pragma mark - FullscreenUIElement
- (void)updateForFullscreenProgress:(CGFloat)progress { - (void)updateForFullscreenProgress:(CGFloat)progress {
for (CollapsingToolbarHeightConstraint* heightConstraint in self // No changes are needed if there are no collapsing toolbars.
.toolbarHeightConstraints) { CGFloat stackHeightDelta = self.heightRange.delta();
heightConstraint.progress = progress; if (!self.viewLoaded || AreCGFloatsEqual(stackHeightDelta, 0.0) ||
!self.toolbars.count) {
return;
}
for (NSUInteger i = 0; i < self.toolbars.count; ++i) {
CollapsingToolbarHeightConstraint* constraint =
self.toolbarHeightConstraints[i];
// Calculate the progress range for the toolbar. |startProgress| is pre-
// calculated and stored in self.toolbarExpansionStartProgresses. The end
// progress is calculated by adding the proportion of the overall stack
// height delta created by this toolbar.
CGFloat startProgress = self.toolbarExpansionStartProgresses[i];
CGFloat endProgress =
startProgress + constraint.heightRange.delta() / stackHeightDelta;
// CollapsingToolbarHeightConstraint clamps its progress value between 0.0
// and 1.0, so |constraint|'s progress value will be set:
// - 0.0 when |progress| <= |startProgress|,
// - 1.0 when |progress| >= |endProgress|, and
// - scaled linearly from 0.0 to 1.0 for |progress| values within that
// range.
constraint.progress =
(progress - startProgress) / (endProgress - startProgress);
} }
} }
- (void)updateForFullscreenEnabled:(BOOL)enabled { - (void)updateForFullscreenEnabled:(BOOL)enabled {
[self updateForFullscreenProgress:1.0]; if (!enabled)
[self updateForFullscreenProgress:1.0];
} }
- (void)animateFullscreenWithAnimator:(FullscreenAnimator*)animator { - (void)animateFullscreenWithAnimator:(FullscreenAnimator*)animator {
...@@ -103,7 +157,8 @@ ...@@ -103,7 +157,8 @@
if (_collapsesSafeArea == collapsesSafeArea) if (_collapsesSafeArea == collapsesSafeArea)
return; return;
_collapsesSafeArea = collapsesSafeArea; _collapsesSafeArea = collapsesSafeArea;
self.firstToolbarHeightConstraint.collapsesAdditionalHeight = YES; self.firstToolbarHeightConstraint.collapsesAdditionalHeight =
_collapsesSafeArea;
} }
- (void)setToolbars:(NSArray<UIViewController*>*)toolbars { - (void)setToolbars:(NSArray<UIViewController*>*)toolbars {
...@@ -111,6 +166,7 @@ ...@@ -111,6 +166,7 @@
return; return;
[self removeToolbars]; [self removeToolbars];
_toolbars = toolbars; _toolbars = toolbars;
self.toolbarExpansionStartProgresses.resize(_toolbars.count);
[self setUpToolbarStack]; [self setUpToolbarStack];
} }
...@@ -161,7 +217,7 @@ ...@@ -161,7 +217,7 @@
} }
[self createToolbarHeightConstraints]; [self createToolbarHeightConstraints];
[self updateForSafeArea]; [self updateForSafeArea];
[self updateHeightConstraint]; [self calculateToolbarExpansionStartProgresses];
} }
// Removes all the toolbars from the view. // Removes all the toolbars from the view.
...@@ -178,8 +234,7 @@ ...@@ -178,8 +234,7 @@
- (void)addToolbarAtIndex:(NSUInteger)index { - (void)addToolbarAtIndex:(NSUInteger)index {
DCHECK_LT(index, self.toolbars.count); DCHECK_LT(index, self.toolbars.count);
UIViewController* toolbar = self.toolbars[index]; UIViewController* toolbar = self.toolbars[index];
if (toolbar.parentViewController == self) DCHECK(!toolbar.parentViewController);
return;
// Add the toolbar and its view controller. // Add the toolbar and its view controller.
UIView* toolbarView = toolbar.view; UIView* toolbarView = toolbar.view;
...@@ -215,9 +270,15 @@ ...@@ -215,9 +270,15 @@
// Deactivates the toolbar height constraints and resets the property. // Deactivates the toolbar height constraints and resets the property.
- (void)resetToolbarHeightConstraints { - (void)resetToolbarHeightConstraints {
if (_toolbarHeightConstraints.count) if (_toolbarHeightConstraints.count) {
[NSLayoutConstraint deactivateConstraints:_toolbarHeightConstraints]; [NSLayoutConstraint deactivateConstraints:_toolbarHeightConstraints];
for (CollapsingToolbarHeightConstraint* constraint in
_toolbarHeightConstraints) {
constraint.delegate = nil;
}
}
_toolbarHeightConstraints = nil; _toolbarHeightConstraints = nil;
_heightRange = HeightRange();
} }
// Creates and activates height constriants for the toolbars and adds them to // Creates and activates height constriants for the toolbars and adds them to
...@@ -226,6 +287,7 @@ ...@@ -226,6 +287,7 @@
- (void)createToolbarHeightConstraints { - (void)createToolbarHeightConstraints {
[self resetToolbarHeightConstraints]; [self resetToolbarHeightConstraints];
_toolbarHeightConstraints = [NSMutableArray array]; _toolbarHeightConstraints = [NSMutableArray array];
HeightRange heightRange;
for (NSUInteger i = 0; i < self.toolbars.count; ++i) { for (NSUInteger i = 0; i < self.toolbars.count; ++i) {
UIView* toolbarView = self.toolbars[i].view; UIView* toolbarView = self.toolbars[i].view;
CollapsingToolbarHeightConstraint* heightConstraint = CollapsingToolbarHeightConstraint* heightConstraint =
...@@ -236,11 +298,49 @@ ...@@ -236,11 +298,49 @@
heightConstraint.additionalHeight = self.additionalStackHeight; heightConstraint.additionalHeight = self.additionalStackHeight;
heightConstraint.collapsesAdditionalHeight = self.collapsesSafeArea; heightConstraint.collapsesAdditionalHeight = self.collapsesSafeArea;
} }
// Set as delegate to receive notifications of height range updates.
heightConstraint.delegate = self;
// Add the height range values.
heightRange += heightConstraint.heightRange;
[_toolbarHeightConstraints addObject:heightConstraint]; [_toolbarHeightConstraints addObject:heightConstraint];
} }
[self updateHeightRangeWithRange:heightRange];
}
// Updates the height range of the stack with |range|.
- (void)updateHeightRangeWithRange:(const HeightRange&)range {
if (_heightRange == range)
return;
BOOL maxHeightUpdated =
!AreCGFloatsEqual(_heightRange.max_height(), range.max_height());
BOOL deltaUpdated = !AreCGFloatsEqual(_heightRange.delta(), range.delta());
_heightRange = range;
if (maxHeightUpdated)
self.heightConstraint.constant = _heightRange.max_height();
if (deltaUpdated)
[self calculateToolbarExpansionStartProgresses];
}
// Calculates the fullscreen progress values at which the toolbars should start
// expanding. See comments for self.toolbarExpansionStartProgresses for more
// details.
- (void)calculateToolbarExpansionStartProgresses {
DCHECK_EQ(self.toolbarExpansionStartProgresses.size(), self.toolbars.count);
if (!self.toolbars.count)
return;
CGFloat startProgress = 0.0;
for (NSUInteger i = self.toolbars.count - 1; i > 0; --i) {
self.toolbarExpansionStartProgresses[i] = startProgress;
CGFloat delta = self.heightRange.delta();
if (delta > 0.0) {
startProgress +=
self.toolbarHeightConstraints[i].heightRange.delta() / delta;
}
}
self.toolbarExpansionStartProgresses[0] = startProgress;
} }
// Updates the height of the first toolbar to account for the safe area. // Adds additional height to the first toolbar to account for the safe area.
- (void)updateForSafeArea { - (void)updateForSafeArea {
if (@available(iOS 11, *)) { if (@available(iOS 11, *)) {
if (self.orientation == ToolbarContainerOrientation::kTopToBottom) { if (self.orientation == ToolbarContainerOrientation::kTopToBottom) {
...@@ -257,19 +357,4 @@ ...@@ -257,19 +357,4 @@
} }
} }
// Updates the height constraint's constant to the cumulative expanded height of
// all the toolbars.
- (void)updateHeightConstraint {
if (!self.viewLoaded)
return;
// Calculate the cumulative expanded toolbar height.
CGFloat cumulativeExpandedHeight = 0.0;
for (CollapsingToolbarHeightConstraint* constraint in self
.toolbarHeightConstraints) {
cumulativeExpandedHeight +=
constraint.expandedHeight + constraint.additionalHeight;
}
self.heightConstraint.constant = cumulativeExpandedHeight;
}
@end @end
...@@ -4,52 +4,87 @@ ...@@ -4,52 +4,87 @@
#import "ios/chrome/browser/ui/toolbar_container/toolbar_container_view_controller.h" #import "ios/chrome/browser/ui/toolbar_container/toolbar_container_view_controller.h"
#include <algorithm>
#include <vector>
#include "base/stl_util.h"
#include "base/strings/sys_string_conversions.h"
#import "ios/chrome/browser/ui/toolbar_container/toolbar_collapsing.h" #import "ios/chrome/browser/ui/toolbar_container/toolbar_collapsing.h"
#import "ios/chrome/browser/ui/toolbar_container/toolbar_height_range.h"
#import "ios/chrome/common/ui_util/constraints_ui_util.h" #import "ios/chrome/common/ui_util/constraints_ui_util.h"
#include "testing/platform_test.h" #include "testing/gtest/include/gtest/gtest.h"
#if !defined(__has_feature) || !__has_feature(objc_arc) #if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support." #error "This file requires ARC support."
#endif #endif
using toolbar_container::HeightRange;
namespace { namespace {
// The container view width. // The container view width.
const CGFloat kContainerViewWidth = 300.0; const CGFloat kContainerViewWidth = 300.0;
// The expanded height of the collapsing toolbar. // The number of toolbars to add.
const CGFloat kExpandedToolbarHeight = 100.0; const size_t kToolbarCount = 2;
// The collapsed height of the collapsing toolbar. // The collapsed and expanded heights of the toolbars.
const CGFloat kCollapsedToolbarHeight = 50.0; const CGFloat kCollapsedToolbarHeight = 50.0;
// The height of the non-collapsing toolbar. const CGFloat kExpandedToolbarHeight = 150.0;
// The non-collapsing toolbar height.
const CGFloat kNonCollapsingToolbarHeight = 75.0; const CGFloat kNonCollapsingToolbarHeight = 75.0;
// The inset into the stack for the safe area.
const CGFloat kSafeAreaStackInset = 100.0;
// The progress values to check.
const CGFloat kStackProgressValues[] = {0.0, 0.25, 0.5, 0.75, 1.0};
// Parameters used for the test fixtures.
typedef NS_ENUM(NSUInteger, ToolbarContainerTestConfig) {
kEmptyConfig = 0,
kTopToBottom = 1 << 0,
kCollapsingToolbars = 1 << 1,
kCollapsingSafeInset = 1 << 2,
kToolbarContainerConfigMax = 1 << 3,
};
// Returns a string version of |frame| to use for error printing.
std::string GetFrameString(CGRect frame) {
return base::SysNSStringToUTF8(NSStringFromCGRect(frame));
}
} // namespace } // namespace
// Test toolbar view. // Test toolbar view.
@interface TestToolbarView : UIView<ToolbarCollapsing> @interface TestToolbarView : UIView<ToolbarCollapsing> {
// Redefine ToolbarCollapsing properies as readwrite. HeightRange _heightRange;
@property(nonatomic, assign, readwrite) CGFloat expandedToolbarHeight; }
@property(nonatomic, assign, readwrite) CGFloat collapsedToolbarHeight; - (instancetype)initWithHeightRange:(const HeightRange&)heightRange;
@end @end
@implementation TestToolbarView @implementation TestToolbarView
@synthesize expandedToolbarHeight = _expandedToolbarHeight; - (instancetype)initWithHeightRange:(const HeightRange&)heightRange {
@synthesize collapsedToolbarHeight = _collapsedToolbarHeight; if (self = [super init])
_heightRange = heightRange;
return self;
}
- (CGFloat)expandedToolbarHeight {
return _heightRange.max_height();
}
- (CGFloat)collapsedToolbarHeight {
return _heightRange.min_height();
}
@end @end
// Test toolbar view controller. // Test toolbar view controller.
@interface TestToolbarViewController : UIViewController @interface TestToolbarViewController : UIViewController {
HeightRange _heightRange;
}
@property(nonatomic, strong, readonly) TestToolbarView* toolbarView; @property(nonatomic, strong, readonly) TestToolbarView* toolbarView;
@property(nonatomic, assign) CGFloat expandedToolbarHeight; - (instancetype)initWithHeightRange:(const HeightRange&)heightRange;
@property(nonatomic, assign) CGFloat collapsedToolbarHeight;
@end @end
@implementation TestToolbarViewController @implementation TestToolbarViewController
@synthesize expandedToolbarHeight = _expandedToolbarHeight; - (instancetype)initWithHeightRange:(const HeightRange&)heightRange {
@synthesize collapsedToolbarHeight = _collapsedToolbarHeight; if (self = [super init])
_heightRange = heightRange;
return self;
}
- (void)loadView { - (void)loadView {
TestToolbarView* view = [[TestToolbarView alloc] initWithFrame:CGRectZero]; self.view = [[TestToolbarView alloc] initWithHeightRange:_heightRange];
view.expandedToolbarHeight = self.expandedToolbarHeight;
view.collapsedToolbarHeight = self.collapsedToolbarHeight;
self.view = view;
} }
- (TestToolbarView*)toolbarView { - (TestToolbarView*)toolbarView {
return static_cast<TestToolbarView*>(self.view); return static_cast<TestToolbarView*>(self.view);
...@@ -57,28 +92,21 @@ const CGFloat kNonCollapsingToolbarHeight = 75.0; ...@@ -57,28 +92,21 @@ const CGFloat kNonCollapsingToolbarHeight = 75.0;
@end @end
// Test fixture for ToolbarContainerViewController. // Test fixture for ToolbarContainerViewController.
class ToolbarContainerViewControllerTest : public PlatformTest { class ToolbarContainerViewControllerTest
: public ::testing::TestWithParam<ToolbarContainerTestConfig> {
public: public:
ToolbarContainerViewControllerTest() ToolbarContainerViewControllerTest()
: PlatformTest(), : window_([[UIWindow alloc] init]),
window_([[UIWindow alloc] init]), view_controller_([[ToolbarContainerViewController alloc] init]) {
view_controller_([[ToolbarContainerViewController alloc] init]), // Resize the window and add the container view such that it hugs the
collapsing_toolbar_([[TestToolbarViewController alloc] init]), // leading/trailing edges.
non_collapsing_toolbar_([[TestToolbarViewController alloc] init]) {
collapsing_toolbar_.expandedToolbarHeight = kExpandedToolbarHeight;
collapsing_toolbar_.collapsedToolbarHeight = kCollapsedToolbarHeight;
non_collapsing_toolbar_.expandedToolbarHeight = kNonCollapsingToolbarHeight;
non_collapsing_toolbar_.collapsedToolbarHeight =
kNonCollapsingToolbarHeight;
[container_view().widthAnchor constraintEqualToConstant:kContainerViewWidth]
.active = YES;
window_.frame = CGRectMake(0.0, 0.0, kContainerViewWidth, 1000); window_.frame = CGRectMake(0.0, 0.0, kContainerViewWidth, 1000);
[window_ addSubview:container_view()]; [window_ addSubview:container_view()];
AddSameConstraintsToSides(window_, container_view(), AddSameConstraintsToSides(window_, container_view(),
LayoutSides::kLeading | LayoutSides::kTrailing); LayoutSides::kLeading | LayoutSides::kTrailing);
SetOrientation(ToolbarContainerOrientation::kTopToBottom); UpdateForOrientationConfig();
view_controller_.toolbars = UpdateForToolbarCollapsingConfig();
@[ non_collapsing_toolbar_, collapsing_toolbar_ ]; UpdateForSafeInsetCollapsingConfig();
ForceLayout(); ForceLayout();
} }
...@@ -86,76 +114,102 @@ class ToolbarContainerViewControllerTest : public PlatformTest { ...@@ -86,76 +114,102 @@ class ToolbarContainerViewControllerTest : public PlatformTest {
view_controller_.toolbars = nil; view_controller_.toolbars = nil;
} }
// Returns the additional stack height created by the safe area insets. // Convenience getters for each of the config option flags.
CGFloat GetAdditionalStackHeight() { bool IsTopToBottom() { return (GetParam() & kTopToBottom) == kTopToBottom; }
CGFloat additional_height = 0.0; bool HasCollapsingToolbars() {
bool top_to_bottom = view_controller_.orientation == return (GetParam() & kCollapsingToolbars) == kCollapsingToolbars;
ToolbarContainerOrientation::kTopToBottom; }
bool HasCollapsingSafeInset() {
return (GetParam() & kCollapsingSafeInset) == kCollapsingSafeInset;
}
// Sets the orientation of the container and constraints its view to the top
// or bottom of the window.
void UpdateForOrientationConfig() {
view_controller_.orientation =
IsTopToBottom() ? ToolbarContainerOrientation::kTopToBottom
: ToolbarContainerOrientation::kBottomToTop;
if (@available(iOS 11, *)) { if (@available(iOS 11, *)) {
additional_height = top_to_bottom UIEdgeInsets safe_insets = container_view().safeAreaInsets;
? container_view().safeAreaInsets.top if (IsTopToBottom())
: container_view().safeAreaInsets.bottom; safe_insets.top = kSafeAreaStackInset - safe_insets.top;
} else if (top_to_bottom) { else
additional_height = view_controller_.topLayoutGuide.length; safe_insets.bottom = kSafeAreaStackInset - safe_insets.bottom;
view_controller_.additionalSafeAreaInsets = safe_insets;
} else {
// Deactivate all pre-existing constraints for the |guide|'s height.
// They are added by UIKit at the maximum priority, so must be removed to
// update |guide|'s length.
id<UILayoutSupport> guide = IsTopToBottom()
? view_controller_.topLayoutGuide
: view_controller_.bottomLayoutGuide;
for (NSLayoutConstraint* constraint in container_view().constraints) {
if (constraint.firstItem == guide &&
constraint.firstAttribute == NSLayoutAttributeHeight) {
constraint.active = NO;
}
}
[guide.heightAnchor constraintEqualToConstant:kSafeAreaStackInset]
.active = YES;
} }
return additional_height;
} }
// Returns the expected height of the container. // Adds collapsible or non-collapsible toolbars to the container, depending on
CGFloat GetExpectedContainerHeight() { // the config flag.
return kNonCollapsingToolbarHeight + kExpandedToolbarHeight + void UpdateForToolbarCollapsingConfig() {
GetAdditionalStackHeight(); // Calculate the height range for the toolbars.
HeightRange toolbar_height_range;
if (HasCollapsingToolbars()) {
toolbar_height_range =
HeightRange(kCollapsedToolbarHeight, kExpandedToolbarHeight);
} else {
toolbar_height_range =
HeightRange(kNonCollapsingToolbarHeight, kNonCollapsingToolbarHeight);
}
// Add kToolbarCount toolbars with |toolbar_height_range|.
height_ranges_ =
std::vector<HeightRange>(kToolbarCount, toolbar_height_range);
NSMutableArray* toolbars = [NSMutableArray array];
for (const HeightRange& height_range : height_ranges_) {
TestToolbarViewController* toolbar =
[[TestToolbarViewController alloc] initWithHeightRange:height_range];
[toolbars addObject:toolbar];
}
view_controller_.toolbars = toolbars;
} }
// Expand or collapse the toolbars. // Updates the view controller safe inset collapsing behavior based on the
void SetExpanded(bool expanded) { // config.
[view_controller_ updateForFullscreenProgress:expanded ? 1.0 : 0.0]; void UpdateForSafeInsetCollapsingConfig() {
ForceLayout(); view_controller_.collapsesSafeArea = HasCollapsingSafeInset();
} }
// Sets the container orientation. // Returns the total height range for the toolbar view at |index|, accounting
void SetOrientation(ToolbarContainerOrientation orientation) { // for both the toolbar expansion and the collapsing safe area inset.
container_positioning_constraint_.active = NO; HeightRange GetTotalToolbarHeightRange(NSUInteger index) {
view_controller_.orientation = orientation; HeightRange height_range = height_ranges_[index];
if (orientation == ToolbarContainerOrientation::kTopToBottom) { if (index == 0) {
container_positioning_constraint_ = [container_view().topAnchor bool collapses_safe_area = view_controller_.collapsesSafeArea;
constraintEqualToAnchor:window_.topAnchor]; HeightRange safe_area_height_range = HeightRange(
} else { collapses_safe_area ? 0.0 : kSafeAreaStackInset, kSafeAreaStackInset);
container_positioning_constraint_ = [container_view().bottomAnchor height_range += safe_area_height_range;
constraintEqualToAnchor:window_.bottomAnchor];
} }
container_positioning_constraint_.active = YES; return height_range;
ForceLayout();
} }
// Sets whether the safe area should be collapsed. // Returns the expected height of the toolbar stack.
void SetCollapsesSafeArea(bool collapses_safe_area) { CGFloat GetExpectedStackHeight() {
view_controller_.collapsesSafeArea = collapses_safe_area; CGFloat expected_stack_height = 0.0;
for (NSUInteger index = 0; index < kToolbarCount; ++index) {
expected_stack_height += GetTotalToolbarHeightRange(index).max_height();
}
return expected_stack_height;
} }
// Sets the safe area insets or top layout guide for the container and forces // Set the stack progress.
// a layout. void SetStackProgress(CGFloat progress) {
void SetSafeAreaInsets(UIEdgeInsets insets) { stack_progress_ = progress;
if (@available(iOS 11, *)) { [view_controller_ updateForFullscreenProgress:progress];
view_controller_.additionalSafeAreaInsets = insets;
} else {
// Deactivate all pre-existing constraints for the layout guides' heights.
// They are added by UIKit at the maximum priority, so must be removed to
// update the lengths of the layout guides.
for (NSLayoutConstraint* constraint in container_view().constraints) {
if (constraint.firstAttribute == NSLayoutAttributeHeight &&
(constraint.firstItem == view_controller_.topLayoutGuide ||
constraint.firstItem == view_controller_.bottomLayoutGuide)) {
constraint.active = NO;
}
}
[view_controller_.topLayoutGuide.heightAnchor
constraintEqualToConstant:insets.top]
.active = YES;
[view_controller_.bottomLayoutGuide.heightAnchor
constraintEqualToConstant:insets.bottom]
.active = YES;
}
ForceLayout(); ForceLayout();
} }
...@@ -165,186 +219,105 @@ class ToolbarContainerViewControllerTest : public PlatformTest { ...@@ -165,186 +219,105 @@ class ToolbarContainerViewControllerTest : public PlatformTest {
[window_ layoutIfNeeded]; [window_ layoutIfNeeded];
[container_view() setNeedsLayout]; [container_view() setNeedsLayout];
[container_view() layoutIfNeeded]; [container_view() layoutIfNeeded];
stack_height_delta_ = 0.0;
for (NSUInteger index = 0; index < kToolbarCount; ++index) {
stack_height_delta_ += GetTotalToolbarHeightRange(index).delta();
}
} }
// The views. // Returns the toolbar view at |index|.
UIView* container_view() { return view_controller_.view; } TestToolbarView* GetToolbarView(NSUInteger index) {
TestToolbarView* collapsing_toolbar_view() { return static_cast<TestToolbarViewController*>(
return collapsing_toolbar_.toolbarView; view_controller_.toolbars[index])
.toolbarView;
} }
TestToolbarView* non_collapsing_toolbar_view() {
return non_collapsing_toolbar_.toolbarView; // Returns the progress value for the toolbar at |index| for the current stack
// progress.
CGFloat GetToolbarProgress(NSUInteger index) {
// Calculate the start progress.
CGFloat start_progress = 0.0;
for (NSUInteger i = kToolbarCount - 1; i > index; --i) {
if (stack_height_delta_ > 0.0) {
start_progress +=
GetTotalToolbarHeightRange(i).delta() / stack_height_delta_;
}
}
// Get the individual toolbar progress.
HeightRange height_range = GetTotalToolbarHeightRange(index);
CGFloat end_progress =
start_progress + height_range.delta() / stack_height_delta_;
CGFloat progress =
(stack_progress_ - start_progress) / (end_progress - start_progress);
progress = std::min(static_cast<CGFloat>(1.0), progress);
progress = std::max(static_cast<CGFloat>(0.0), progress);
return progress;
} }
TestToolbarView* first_toolbar_view() {
return static_cast<TestToolbarViewController*>(view_controller_.toolbars[0]) // Returns the expected frame for the toolbar at |index| at the current stack
.toolbarView; // progress.
CGRect GetExpectedToolbarFrame(NSUInteger index) {
const HeightRange& height_range = GetTotalToolbarHeightRange(index);
CGSize size = CGSizeMake(
kContainerViewWidth,
height_range.GetInterpolatedHeight(GetToolbarProgress(index)));
CGFloat origin_y = 0.0;
bool is_first_toolbar = index == 0;
if (IsTopToBottom()) {
origin_y = is_first_toolbar
? 0.0
: CGRectGetMaxY(GetExpectedToolbarFrame(index - 1));
} else {
CGFloat bottom_edge =
is_first_toolbar ? CGRectGetMaxY(container_view().bounds)
: CGRectGetMinY(GetExpectedToolbarFrame(index - 1));
origin_y = bottom_edge - size.height;
}
return CGRectMake(0.0, origin_y, size.width, size.height);
} }
// Checks that the frames of the toolbar views are expected for the current
// stack progress.
void CheckToolbarFrames() {
for (NSUInteger index = 0; index < kToolbarCount; ++index) {
CGRect toolbar_frame = GetToolbarView(index).frame;
CGRect expected_toolbar_frame = GetExpectedToolbarFrame(index);
EXPECT_TRUE(CGRectEqualToRect(toolbar_frame, expected_toolbar_frame)) <<
"IsTopToBottom : " << IsTopToBottom() << "\n"
"HasCollapsingToolbars : " << HasCollapsingToolbars() << "\n"
"HasCollapsingSafeInset : " << HasCollapsingSafeInset() << "\n"
"Stack Progress : " << stack_progress_ << "\n"
"Toolbar Index : " << index << "\n"
"toolbar_frame : " << GetFrameString(toolbar_frame) << "\n"
"expected_toolbar_frame : " << GetFrameString(expected_toolbar_frame);
}
}
// The view.
UIView* container_view() { return view_controller_.view; }
private: private:
__strong UIWindow* window_ = nil; __strong UIWindow* window_ = nil;
__strong ToolbarContainerViewController* view_controller_ = nil; __strong ToolbarContainerViewController* view_controller_ = nil;
__strong NSLayoutConstraint* container_positioning_constraint_ = nil; std::vector<HeightRange> height_ranges_;
__strong TestToolbarViewController* collapsing_toolbar_ = nil; CGFloat stack_progress_ = 1.0;
__strong TestToolbarViewController* non_collapsing_toolbar_ = nil; CGFloat stack_height_delta_ = 0.0;
}; };
// Tests the layout of the toolbar views in when oriented from top to bottom // Tests the layout of the toolbar stack configured using the
// and the toolbars are fully expanded. // ToolbarContainerTestConfig test fixture parameter.
TEST_F(ToolbarContainerViewControllerTest, TopToBottomExpanded) { TEST_P(ToolbarContainerViewControllerTest, VerifyStackLayoutForProgresses) {
SetOrientation(ToolbarContainerOrientation::kTopToBottom); // Check that the container height is as expected.
SetExpanded(true); EXPECT_EQ(CGRectGetHeight(container_view().bounds), GetExpectedStackHeight());
EXPECT_EQ(GetExpectedContainerHeight(), // Set the stack progress to the progress values in kStackProgressValues and
CGRectGetHeight(container_view().bounds)); // verify the toolbar frames for each of these stack progress values.
CGRect non_collapsing_toolbar_frame = for (size_t index = 0; index < base::size(kStackProgressValues); ++index) {
CGRectMake(0.0, 0.0, kContainerViewWidth, SetStackProgress(kStackProgressValues[index]);
kNonCollapsingToolbarHeight + GetAdditionalStackHeight()); CheckToolbarFrames();
EXPECT_TRUE(CGRectEqualToRect(non_collapsing_toolbar_frame, }
non_collapsing_toolbar_view().frame));
CGRect collapsing_toolbar_frame =
CGRectMake(0.0, CGRectGetMaxY(non_collapsing_toolbar_frame),
kContainerViewWidth, kExpandedToolbarHeight);
EXPECT_TRUE(CGRectEqualToRect(collapsing_toolbar_frame,
collapsing_toolbar_view().frame));
}
// Tests the layout of the toolbar views in when oriented from top to bottom
// and the toolbars are fully collapsed.
TEST_F(ToolbarContainerViewControllerTest, TopToBottomCollapsed) {
SetOrientation(ToolbarContainerOrientation::kTopToBottom);
SetExpanded(false);
EXPECT_EQ(GetExpectedContainerHeight(),
CGRectGetHeight(container_view().bounds));
CGRect non_collapsing_toolbar_frame =
CGRectMake(0.0, 0.0, kContainerViewWidth,
kNonCollapsingToolbarHeight + GetAdditionalStackHeight());
EXPECT_TRUE(CGRectEqualToRect(non_collapsing_toolbar_frame,
non_collapsing_toolbar_view().frame));
CGRect collapsing_toolbar_frame =
CGRectMake(0.0, CGRectGetMaxY(non_collapsing_toolbar_frame),
kContainerViewWidth, kCollapsedToolbarHeight);
EXPECT_TRUE(CGRectEqualToRect(collapsing_toolbar_frame,
collapsing_toolbar_view().frame));
}
// Tests the layout of the toolbar views in when oriented from bottom to top
// and the toolbars are fully expanded.
// TODO(crbug.com/895766): reenable these tests on device.
#if TARGET_IPHONE_SIMULATOR
#define MAYBE_BottomToTopExpanded BottomToTopExpanded
#else
#define MAYBE_BottomToTopExpanded DISABLED_BottomToTopExpanded
#endif
TEST_F(ToolbarContainerViewControllerTest, MAYBE_BottomToTopExpanded) {
SetOrientation(ToolbarContainerOrientation::kBottomToTop);
SetExpanded(true);
CGFloat container_height = CGRectGetHeight(container_view().bounds);
EXPECT_EQ(GetExpectedContainerHeight(), container_height);
CGRect non_collapsing_toolbar_frame = CGRectMake(
0.0, container_height - kNonCollapsingToolbarHeight, kContainerViewWidth,
kNonCollapsingToolbarHeight + GetAdditionalStackHeight());
EXPECT_TRUE(CGRectEqualToRect(non_collapsing_toolbar_frame,
non_collapsing_toolbar_view().frame));
CGRect collapsing_toolbar_frame = CGRectMake(
0.0, CGRectGetMinY(non_collapsing_toolbar_frame) - kExpandedToolbarHeight,
kContainerViewWidth, kExpandedToolbarHeight);
EXPECT_TRUE(CGRectEqualToRect(collapsing_toolbar_frame,
collapsing_toolbar_view().frame));
}
// Tests the layout of the toolbar views in when oriented from bottom to top
// and the toolbars are fully collapsed.
// TODO(crbug.com/895766): reenable these tests on device.
#if TARGET_IPHONE_SIMULATOR
#define MAYBE_BottomToTopCollapsed BottomToTopCollapsed
#else
#define MAYBE_BottomToTopCollapsed DISABLED_BottomToTopCollapsed
#endif
TEST_F(ToolbarContainerViewControllerTest, MAYBE_BottomToTopCollapsed) {
SetOrientation(ToolbarContainerOrientation::kBottomToTop);
SetExpanded(false);
CGFloat container_height = CGRectGetHeight(container_view().bounds);
EXPECT_EQ(GetExpectedContainerHeight(), container_height);
CGRect non_collapsing_toolbar_frame = CGRectMake(
0.0, container_height - kNonCollapsingToolbarHeight, kContainerViewWidth,
kNonCollapsingToolbarHeight + GetAdditionalStackHeight());
EXPECT_TRUE(CGRectEqualToRect(non_collapsing_toolbar_frame,
non_collapsing_toolbar_view().frame));
CGRect collapsing_toolbar_frame = CGRectMake(
0.0,
CGRectGetMinY(non_collapsing_toolbar_frame) - kCollapsedToolbarHeight,
kContainerViewWidth, kCollapsedToolbarHeight);
EXPECT_TRUE(CGRectEqualToRect(collapsing_toolbar_frame,
collapsing_toolbar_view().frame));
}
// Tests that the container and the top toolbar's height accounts for the non-
// collapsing safe area.
TEST_F(ToolbarContainerViewControllerTest, NonCollapsingTopSafeArea) {
const UIEdgeInsets kSafeInsets = UIEdgeInsetsMake(100.0, 0.0, 0.0, 0.0);
SetCollapsesSafeArea(false);
SetSafeAreaInsets(kSafeInsets);
SetOrientation(ToolbarContainerOrientation::kTopToBottom);
EXPECT_EQ(GetExpectedContainerHeight(),
CGRectGetHeight(container_view().bounds));
SetExpanded(true);
TestToolbarView* toolbar_view = first_toolbar_view();
EXPECT_EQ(toolbar_view.expandedToolbarHeight + GetAdditionalStackHeight(),
CGRectGetHeight(toolbar_view.frame));
SetExpanded(false);
EXPECT_EQ(toolbar_view.collapsedToolbarHeight + GetAdditionalStackHeight(),
CGRectGetHeight(toolbar_view.frame));
}
// Tests that the container and the bottom toolbar's height accounts for the
// non-collapsing safe area.
TEST_F(ToolbarContainerViewControllerTest, NonCollapsingBottomSafeArea) {
const UIEdgeInsets kSafeInsets = UIEdgeInsetsMake(100.0, 0.0, 0.0, 0.0);
SetCollapsesSafeArea(false);
SetSafeAreaInsets(kSafeInsets);
SetOrientation(ToolbarContainerOrientation::kBottomToTop);
EXPECT_EQ(GetExpectedContainerHeight(),
CGRectGetHeight(container_view().bounds));
SetExpanded(true);
TestToolbarView* toolbar_view = first_toolbar_view();
EXPECT_EQ(toolbar_view.expandedToolbarHeight + GetAdditionalStackHeight(),
CGRectGetHeight(toolbar_view.frame));
SetExpanded(false);
EXPECT_EQ(toolbar_view.collapsedToolbarHeight + GetAdditionalStackHeight(),
CGRectGetHeight(toolbar_view.frame));
}
// Tests that the container and the top toolbar's height accounts for the
// collapsing safe area.
TEST_F(ToolbarContainerViewControllerTest, CollapsingTopSafeArea) {
const UIEdgeInsets kSafeInsets = UIEdgeInsetsMake(100.0, 0.0, 0.0, 0.0);
SetCollapsesSafeArea(true);
SetSafeAreaInsets(kSafeInsets);
SetOrientation(ToolbarContainerOrientation::kTopToBottom);
EXPECT_EQ(GetExpectedContainerHeight(),
CGRectGetHeight(container_view().bounds));
SetExpanded(true);
TestToolbarView* toolbar_view = first_toolbar_view();
EXPECT_EQ(toolbar_view.expandedToolbarHeight + GetAdditionalStackHeight(),
CGRectGetHeight(toolbar_view.frame));
SetExpanded(false);
EXPECT_EQ(toolbar_view.collapsedToolbarHeight,
CGRectGetHeight(toolbar_view.frame));
} }
// Tests that the container and the bottom toolbar's height accounts for the INSTANTIATE_TEST_CASE_P(,
// collapsing safe area. ToolbarContainerViewControllerTest,
TEST_F(ToolbarContainerViewControllerTest, CollapsingBottomSafeArea) { ::testing::Range(kEmptyConfig,
const UIEdgeInsets kSafeInsets = UIEdgeInsetsMake(100.0, 0.0, 0.0, 0.0); kToolbarContainerConfigMax));
SetCollapsesSafeArea(true);
SetSafeAreaInsets(kSafeInsets);
SetOrientation(ToolbarContainerOrientation::kBottomToTop);
EXPECT_EQ(GetExpectedContainerHeight(),
CGRectGetHeight(container_view().bounds));
SetExpanded(true);
TestToolbarView* toolbar_view = first_toolbar_view();
EXPECT_EQ(toolbar_view.expandedToolbarHeight + GetAdditionalStackHeight(),
CGRectGetHeight(toolbar_view.frame));
SetExpanded(false);
EXPECT_EQ(toolbar_view.collapsedToolbarHeight,
CGRectGetHeight(toolbar_view.frame));
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef IOS_CHROME_BROWSER_UI_TOOLBAR_CONTAINER_TOOLBAR_HEIGHT_RANGE_H_
#define IOS_CHROME_BROWSER_UI_TOOLBAR_CONTAINER_TOOLBAR_HEIGHT_RANGE_H_
#import <QuartzCore/QuartzCore.h>
namespace toolbar_container {
// A simple container object used to store the height range for a collapsible
// toolbar.
class HeightRange {
public:
HeightRange() = default;
HeightRange(CGFloat min_height, CGFloat max_height);
// The max and min heights.
CGFloat min_height() const { return min_height_; }
CGFloat max_height() const { return max_height_; }
// Returns the delta between the max and min height.
CGFloat delta() const { return max_height_ - min_height_; }
// Returns the height value at the given interpolation value.
CGFloat GetInterpolatedHeight(CGFloat progress) const;
// Operators.
bool operator==(const HeightRange& other) const;
bool operator!=(const HeightRange& other) const;
HeightRange operator+(const HeightRange& other) const;
HeightRange operator-(const HeightRange& other) const;
HeightRange& operator+=(const HeightRange& other);
HeightRange& operator-=(const HeightRange& other);
private:
// The min and max heights.
CGFloat min_height_ = 0.0;
CGFloat max_height_ = 0.0;
};
} // namespace toolbar_container
#endif // IOS_CHROME_BROWSER_UI_TOOLBAR_CONTAINER_TOOLBAR_HEIGHT_RANGE_H_
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/ui/toolbar_container/toolbar_height_range.h"
#include <algorithm>
#import "base/logging.h"
#include "ios/chrome/browser/ui/ui_util.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace toolbar_container {
HeightRange::HeightRange(CGFloat min_height, CGFloat max_height)
: min_height_(min_height), max_height_(max_height) {}
CGFloat HeightRange::GetInterpolatedHeight(CGFloat progress) const {
progress = std::min(static_cast<CGFloat>(1.0), progress);
progress = std::max(static_cast<CGFloat>(0.0), progress);
return min_height() + progress * delta();
}
bool HeightRange::operator==(const HeightRange& other) const {
return AreCGFloatsEqual(min_height(), other.min_height()) &&
AreCGFloatsEqual(max_height(), other.max_height());
}
bool HeightRange::operator!=(const HeightRange& other) const {
return !(*this == other);
}
HeightRange HeightRange::operator+(const HeightRange& other) const {
return HeightRange(min_height() + other.min_height(),
max_height() + other.max_height());
}
HeightRange HeightRange::operator-(const HeightRange& other) const {
return HeightRange(min_height() - other.min_height(),
max_height() - other.max_height());
}
HeightRange& HeightRange::operator+=(const HeightRange& other) {
min_height_ += other.min_height();
max_height_ += other.max_height();
return *this;
}
HeightRange& HeightRange::operator-=(const HeightRange& other) {
min_height_ -= other.min_height();
max_height_ -= other.max_height();
return *this;
}
} // namespace toolbar_container
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/ui/toolbar_container/toolbar_height_range.h"
#include "testing/platform_test.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using toolbar_container::HeightRange;
// Test fixture for HeightRange.
using HeightRangeTest = PlatformTest;
// Tests that the default constructor creates a [0.0, 0.0] range.
TEST_F(HeightRangeTest, DefaultConstructor) {
HeightRange range;
EXPECT_EQ(range.min_height(), 0.0);
EXPECT_EQ(range.max_height(), 0.0);
}
// Simple test for setting the height range values.
TEST_F(HeightRangeTest, Creation) {
const CGFloat kMin = 50.0;
const CGFloat kMax = 100.0;
HeightRange range(kMin, kMax);
EXPECT_EQ(range.min_height(), kMin);
EXPECT_EQ(range.max_height(), kMax);
EXPECT_EQ(range.delta(), kMax - kMin);
}
// Test for getting interpolation values.
TEST_F(HeightRangeTest, Interpolation) {
const CGFloat kMin = 0.0;
const CGFloat kMax = 100.0;
HeightRange range(kMin, kMax);
EXPECT_EQ(range.GetInterpolatedHeight(-0.5), 0.0);
EXPECT_EQ(range.GetInterpolatedHeight(0.0), 0.0);
EXPECT_EQ(range.GetInterpolatedHeight(0.25), 25.0);
EXPECT_EQ(range.GetInterpolatedHeight(0.5), 50.0);
EXPECT_EQ(range.GetInterpolatedHeight(0.75), 75.0);
EXPECT_EQ(range.GetInterpolatedHeight(1.0), 100.0);
EXPECT_EQ(range.GetInterpolatedHeight(1.5), 100.0);
}
// Test for comparing ranges.
TEST_F(HeightRangeTest, Equality) {
const CGFloat kMin = 0.0;
const CGFloat kMax = 100.0;
HeightRange range(kMin, kMax);
HeightRange equal_range(kMin, kMax);
EXPECT_EQ(range, equal_range);
HeightRange unequal_range;
EXPECT_NE(range, unequal_range);
}
// Test for the + and - operators
TEST_F(HeightRangeTest, AdditionSubtraction) {
const CGFloat kMin1 = 0.0;
const CGFloat kMax1 = 100.0;
HeightRange range1(kMin1, kMax1);
const CGFloat kMin2 = 80.0;
const CGFloat kMax2 = 110.0;
HeightRange range2(kMin2, kMax2);
HeightRange sum = range1 + range2;
EXPECT_EQ(sum, HeightRange(kMin1 + kMin2, kMax1 + kMax2));
EXPECT_EQ(sum - range2, range1);
}
// Test for the += and -= operators.
TEST_F(HeightRangeTest, AssignAdditionSubtraction) {
const CGFloat kMin = 0.0;
const CGFloat kMax = 100.0;
HeightRange range(kMin, kMax);
range += range;
EXPECT_EQ(range, HeightRange(2.0 * kMin, 2.0 * kMax));
range -= HeightRange(kMin, kMax);
EXPECT_EQ(range, HeightRange(kMin, kMax));
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment