1// Copyright 2018 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#import "ios/chrome/browser/ui/toolbar_container/collapsing_toolbar_height_constraint.h"
6
7#include <algorithm>
8
9#include "base/check.h"
10#include "base/numerics/ranges.h"
11#import "ios/chrome/browser/ui/toolbar_container/collapsing_toolbar_height_constraint_delegate.h"
12#import "ios/chrome/browser/ui/toolbar_container/toolbar_collapsing.h"
13#include "ios/chrome/browser/ui/util/ui_util.h"
14
15#if !defined(__has_feature) || !__has_feature(objc_arc)
16#error "This file requires ARC support."
17#endif
18
19namespace {
20// The progress range.
21const CGFloat kMinProgress = 0.0;
22const CGFloat kMaxProgress = 1.0;
23}  // namespace
24
25using toolbar_container::HeightRange;
26
27@interface CollapsingToolbarHeightConstraint () {
28  // Backing variable for property of same name.
29  HeightRange _heightRange;
30}
31// The height values extracted from the constrained view.  If the view conforms
32// to ToolbarCollapsing, these will be the values from that protocol, and will
33// be updated using KVO if those values change.  Otherwise, they will both be
34// equal to the intrinsic height of the view.
35@property(nonatomic, readwrite) CGFloat collapsedToolbarHeight;
36@property(nonatomic, readwrite) CGFloat expandedToolbarHeight;
37// The collapsing toolbar whose height range is being observed.
38@property(nonatomic, weak) UIView<ToolbarCollapsing>* collapsingToolbar;
39@end
40
41@implementation CollapsingToolbarHeightConstraint
42@synthesize collapsedToolbarHeight = _collapsedToolbarHeight;
43@synthesize expandedToolbarHeight = _expandedToolbarHeight;
44@synthesize additionalHeight = _additionalHeight;
45@synthesize collapsesAdditionalHeight = _collapsesAdditionalHeight;
46@synthesize progress = _progress;
47@synthesize delegate = _delegate;
48@synthesize collapsingToolbar = _collapsingToolbar;
49
50+ (instancetype)constraintWithView:(UIView*)view {
51  DCHECK(view);
52  CollapsingToolbarHeightConstraint* constraint =
53      [[self class] constraintWithItem:view
54                             attribute:NSLayoutAttributeHeight
55                             relatedBy:NSLayoutRelationEqual
56                                toItem:nil
57                             attribute:NSLayoutAttributeNotAnAttribute
58                            multiplier:0.0
59                              constant:0.0];
60  if ([view conformsToProtocol:@protocol(ToolbarCollapsing)]) {
61    constraint.collapsingToolbar =
62        static_cast<UIView<ToolbarCollapsing>*>(view);
63  } else {
64    CGFloat intrinsicHeight = view.intrinsicContentSize.height;
65    constraint.collapsedToolbarHeight = intrinsicHeight;
66    constraint.expandedToolbarHeight = intrinsicHeight;
67    [constraint updateToolbarHeightRange];
68  }
69  constraint.progress = 1.0;
70
71  return constraint;
72}
73
74#pragma mark - Accessors
75
76- (void)setActive:(BOOL)active {
77  [super setActive:active];
78  if (self.active)
79    [self startObservingCollapsingToolbar];
80  else
81    [self stopObservingCollapsingToolbar];
82}
83
84- (void)setAdditionalHeight:(CGFloat)additionalHeight {
85  if (AreCGFloatsEqual(_additionalHeight, additionalHeight))
86    return;
87  _additionalHeight = additionalHeight;
88  [self updateToolbarHeightRange];
89}
90
91- (void)setCollapsesAdditionalHeight:(BOOL)collapsesAdditionalHeight {
92  if (_collapsesAdditionalHeight == collapsesAdditionalHeight)
93    return;
94  _collapsesAdditionalHeight = collapsesAdditionalHeight;
95  [self updateToolbarHeightRange];
96}
97
98- (const HeightRange&)heightRange {
99  // Custom getter is needed to support the C++ reference type.
100  return _heightRange;
101}
102
103- (void)setProgress:(CGFloat)progress {
104  progress = base::ClampToRange(progress, kMinProgress, kMaxProgress);
105  if (AreCGFloatsEqual(_progress, progress))
106    return;
107  _progress = progress;
108  [self updateHeightConstant];
109}
110
111- (void)setCollapsingToolbar:(UIView<ToolbarCollapsing>*)collapsingToolbar {
112  if (_collapsingToolbar == collapsingToolbar)
113    return;
114  [self stopObservingCollapsingToolbar];
115  _collapsingToolbar = collapsingToolbar;
116  [self updateCollapsingToolbarHeights];
117  if (self.active)
118    [self startObservingCollapsingToolbar];
119}
120
121#pragma mark - Public
122
123- (CGFloat)toolbarHeightForProgress:(CGFloat)progress {
124  progress = base::ClampToRange(progress, kMinProgress, kMaxProgress);
125  CGFloat base = self.collapsedToolbarHeight;
126  CGFloat range = self.expandedToolbarHeight - self.collapsedToolbarHeight;
127  if (self.collapsesAdditionalHeight) {
128    range += self.additionalHeight;
129  } else {
130    base += self.additionalHeight;
131  }
132  return base + progress * range;
133}
134
135#pragma mark - KVO
136
137- (void)observeValueForKeyPath:(NSString*)key
138                      ofObject:(id)object
139                        change:(NSDictionary*)change
140                       context:(void*)context {
141  [self updateCollapsingToolbarHeights];
142}
143
144#pragma mark - KVO Helpers
145
146- (NSArray<NSString*>* const)collapsingToolbarKeyPaths {
147  static NSArray<NSString*>* const kKeyPaths =
148      @[ @"expandedToolbarHeight", @"collapsedToolbarHeight" ];
149  return kKeyPaths;
150}
151
152- (void)startObservingCollapsingToolbar {
153  for (NSString* keyPath in [self collapsingToolbarKeyPaths]) {
154    [self.collapsingToolbar addObserver:self
155                             forKeyPath:keyPath
156                                options:NSKeyValueObservingOptionNew
157                                context:nullptr];
158  }
159}
160
161- (void)stopObservingCollapsingToolbar {
162  for (NSString* keyPath in [self collapsingToolbarKeyPaths]) {
163    [self.collapsingToolbar removeObserver:self forKeyPath:keyPath];
164  }
165}
166
167#pragma mark - Private
168
169// Upates the collapsed and expanded heights from self.collapsingToolbar.
170- (void)updateCollapsingToolbarHeights {
171  self.collapsedToolbarHeight = self.collapsingToolbar.collapsedToolbarHeight;
172  self.expandedToolbarHeight = self.collapsingToolbar.expandedToolbarHeight;
173  [self updateToolbarHeightRange];
174}
175
176// Updates the height range using the current collapsing toolbar height values
177// and additional height behavior.
178- (void)updateToolbarHeightRange {
179  HeightRange oldHeightRange = self.heightRange;
180  CGFloat minHeight =
181      self.collapsedToolbarHeight +
182      (self.collapsesAdditionalHeight ? 0.0 : self.additionalHeight);
183  CGFloat maxHeight = self.expandedToolbarHeight + self.additionalHeight;
184  _heightRange = HeightRange(minHeight, maxHeight);
185  if (_heightRange == oldHeightRange)
186    return;
187  [self updateHeightConstant];
188  [self.delegate collapsingHeightConstraint:self
189                   didUpdateFromHeightRange:oldHeightRange];
190}
191
192// Updates the constraint's constant
193- (void)updateHeightConstant {
194  self.constant = self.heightRange.GetInterpolatedHeight(self.progress);
195}
196
197@end
198