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") {
sources = [
"collapsing_toolbar_height_constraint.h",
"collapsing_toolbar_height_constraint.mm",
"collapsing_toolbar_height_constraint_delegate.h",
"toolbar_container_view.h",
"toolbar_container_view.mm",
"toolbar_container_view_controller.h",
"toolbar_container_view_controller.mm",
"toolbar_height_range.h",
"toolbar_height_range.mm",
]
configs += [ "//build/config/compiler:enable_arc" ]
......@@ -63,6 +66,7 @@ source_set("unit_tests") {
sources = [
"collapsing_toolbar_height_constraint_unittest.mm",
"toolbar_container_view_controller_unittest.mm",
"toolbar_height_range_unittest.mm",
]
configs += [ "//build/config/compiler:enable_arc" ]
......
......@@ -7,34 +7,41 @@
#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.
@interface CollapsingToolbarHeightConstraint : NSLayoutConstraint
// Returns a constraint that manages the height of |view|. If |view|
// conforms to the ToolbarCollapsing protocol, the collapsed and expanded
// heights are set using those return values. Otherwise, the intrinsic height
// is used as both the collapsed and expanded height.
// Returns a constraint that manages the height of |view|. If |view| conforms
// to ToolbarCollapsing, updating |progress| will scale between its collapsed
// and expanded heights. Otherwise, the constraint will lock |view|'s height
// to its intrinisic content height. The height range can be increased using
// |additionalHeight|.
+ (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.
@property(nonatomic, assign) CGFloat additionalHeight;
// Whether the additional height should be collapsed. When set to YES, the
// view's height ranges from |collapsedHeight| to |expandedHeight| +
// |additionalHeight|. When set to NO, the view's height ranges from
// |additionalHeight| + |collapsedHeight| to |additionalHeight| +
// |expandedHeight|.
// Whether the additional height should be collapsed.
@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
// constraint's constant.
// constraint's constant. The value is clamped between 0.0 and 1.0.
@property(nonatomic, assign) CGFloat progress;
// Returns the height of the toolbar at |progress|
- (CGFloat)toolbarHeightForProgress:(CGFloat)progress;
// The constraint's delegate.
@property(nonatomic, weak, nullable)
id<CollapsingToolbarHeightConstraintDelegate>
delegate;
@end
......
......@@ -8,6 +8,7 @@
#include "base/logging.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"
#include "ios/chrome/browser/ui/ui_util.h"
......@@ -21,20 +22,29 @@ const CGFloat kMinProgress = 0.0;
const CGFloat kMaxProgress = 1.0;
} // namespace
@interface CollapsingToolbarHeightConstraint ()
// Redefine as readwrite.
@property(nonatomic, readwrite) CGFloat collapsedHeight;
@property(nonatomic, readwrite) CGFloat expandedHeight;
using toolbar_container::HeightRange;
@interface CollapsingToolbarHeightConstraint () {
// 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.
@property(nonatomic, weak) UIView<ToolbarCollapsing>* collapsingToolbar;
@end
@implementation CollapsingToolbarHeightConstraint
@synthesize collapsedHeight = _collapsedHeight;
@synthesize expandedHeight = _expandedHeight;
@synthesize collapsedToolbarHeight = _collapsedToolbarHeight;
@synthesize expandedToolbarHeight = _expandedToolbarHeight;
@synthesize additionalHeight = _additionalHeight;
@synthesize collapsesAdditionalHeight = _collapsesAdditionalHeight;
@synthesize progress = _progress;
@synthesize delegate = _delegate;
@synthesize collapsingToolbar = _collapsingToolbar;
+ (instancetype)constraintWithView:(UIView*)view {
......@@ -52,11 +62,11 @@ const CGFloat kMaxProgress = 1.0;
static_cast<UIView<ToolbarCollapsing>*>(view);
} else {
CGFloat intrinsicHeight = view.intrinsicContentSize.height;
constraint.collapsedHeight = intrinsicHeight;
constraint.expandedHeight = intrinsicHeight;
constraint.collapsedToolbarHeight = intrinsicHeight;
constraint.expandedToolbarHeight = intrinsicHeight;
[constraint updateToolbarHeightRange];
}
constraint.progress = 1.0;
[constraint updateHeight];
return constraint;
}
......@@ -71,32 +81,23 @@ const CGFloat kMaxProgress = 1.0;
[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 {
if (AreCGFloatsEqual(_additionalHeight, additionalHeight))
return;
_additionalHeight = additionalHeight;
[self updateHeight];
[self updateToolbarHeightRange];
}
- (void)setCollapsesAdditionalHeight:(BOOL)collapsesAdditionalHeight {
if (_collapsesAdditionalHeight == collapsesAdditionalHeight)
return;
_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 {
......@@ -104,16 +105,15 @@ const CGFloat kMaxProgress = 1.0;
if (AreCGFloatsEqual(_progress, progress))
return;
_progress = progress;
[self updateHeight];
[self updateHeightConstant];
}
- (void)setCollapsingToolbar:(UIView<ToolbarCollapsing>*)collapsingToolbar {
if (_collapsingToolbar == collapsingToolbar)
return;
[self stopObservingCollapsingToolbar];
_collapsingToolbar = collapsingToolbar;
[self updateToolbarHeightRange];
[self updateCollapsingToolbarHeights];
if (self.active)
[self startObservingCollapsingToolbar];
}
......@@ -122,8 +122,8 @@ const CGFloat kMaxProgress = 1.0;
- (CGFloat)toolbarHeightForProgress:(CGFloat)progress {
progress = base::ClampToRange(progress, kMinProgress, kMaxProgress);
CGFloat base = self.collapsedHeight;
CGFloat range = self.expandedHeight - self.collapsedHeight;
CGFloat base = self.collapsedToolbarHeight;
CGFloat range = self.expandedToolbarHeight - self.collapsedToolbarHeight;
if (self.collapsesAdditionalHeight) {
range += self.additionalHeight;
} else {
......@@ -138,7 +138,7 @@ const CGFloat kMaxProgress = 1.0;
ofObject:(id)object
change:(NSDictionary*)change
context:(void*)context {
[self updateToolbarHeightRange];
[self updateCollapsingToolbarHeights];
}
#pragma mark - KVO Helpers
......@@ -166,15 +166,32 @@ const CGFloat kMaxProgress = 1.0;
#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 {
self.collapsedHeight = self.collapsingToolbar.collapsedToolbarHeight;
self.expandedHeight = self.collapsingToolbar.expandedToolbarHeight;
HeightRange oldHeightRange = self.heightRange;
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
- (void)updateHeight {
self.constant = [self toolbarHeightForProgress:self.progress];
- (void)updateHeightConstant {
self.constant = self.heightRange.GetInterpolatedHeight(self.progress);
}
@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 {
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.
TEST_F(CollapsingToolbarHeightConstraintTest, CollapsingConstraint) {
CollapsingView* view = [[CollapsingView alloc] initWithFrame:CGRectZero];
......
......@@ -10,6 +10,7 @@
#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/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)
#error "This file requires ARC support."
......@@ -63,8 +64,11 @@
#pragma mark - Public
- (CGFloat)toolbarStackHeightForFullscreenProgress:(CGFloat)progress {
return [self.containerViewController
toolbarStackHeightForFullscreenProgress:progress];
if (!self.started)
return 0.0;
const toolbar_container::HeightRange& stackHeightRange =
self.containerViewController.heightRange;
return stackHeightRange.GetInterpolatedHeight(progress);
}
#pragma mark - ChromeCoordinator
......
......@@ -9,6 +9,10 @@
#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.
enum class ToolbarContainerOrientation { kTopToBottom, kBottomToTop };
......@@ -26,9 +30,10 @@ enum class ToolbarContainerOrientation { kTopToBottom, kBottomToTop };
// The toolbar view controllers being managed by this container.
@property(nonatomic, strong) NSArray<UIViewController*>* toolbars;
// Returns the height of the toolbar views managed by this container at
// |progress|.
- (CGFloat)toolbarStackHeightForFullscreenProgress:(CGFloat)progress;
// The height range of the overall stack. It is calculated using the collapsed
// and expanded heights of the views managed by |toolbars|.
@property(nonatomic, readonly)
const toolbar_container::HeightRange& heightRange;
@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_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