OLD | NEW |
(Empty) | |
| 1 // Copyright 2014 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/contextual_search/contextual_search_panel_view.h" |
| 6 |
| 7 #import "base/ios/crb_protocol_observers.h" |
| 8 #include "base/ios/weak_nsobject.h" |
| 9 #include "base/logging.h" |
| 10 #include "base/mac/scoped_block.h" |
| 11 #include "base/mac/scoped_nsobject.h" |
| 12 #import "ios/chrome/browser/procedural_block_types.h" |
| 13 #import "ios/chrome/browser/ui/contextual_search/contextual_search_panel_protoco
ls.h" |
| 14 #import "ios/chrome/browser/ui/uikit_ui_util.h" |
| 15 #import "ios/chrome/common/material_timing.h" |
| 16 #import "ios/third_party/material_components_ios/src/components/ShadowElevations
/src/MaterialShadowElevations.h" |
| 17 #import "ios/third_party/material_components_ios/src/components/ShadowLayer/src/
MaterialShadowLayer.h" |
| 18 |
| 19 namespace { |
| 20 |
| 21 // Animation timings. |
| 22 const NSTimeInterval kPanelAnimationDuration = ios::material::kDuration3; |
| 23 const NSTimeInterval kDismissAnimationDuration = ios::material::kDuration1; |
| 24 |
| 25 // Elevation (in MD vertical space) of the panel when dismissed and peeking. |
| 26 const CGFloat kShadowElevation = MDCShadowElevationMenu; |
| 27 |
| 28 } // namespace |
| 29 |
| 30 @interface ContextualSearchPanelObservers |
| 31 : CRBProtocolObservers<ContextualSearchPanelMotionObserver> |
| 32 @end |
| 33 @implementation ContextualSearchPanelObservers |
| 34 |
| 35 @end |
| 36 |
| 37 @interface ContextualSearchPanelView ()<UIGestureRecognizerDelegate, |
| 38 ContextualSearchPanelMotionObserver> |
| 39 |
| 40 // A subview whose content scrolls and whose scrolling is synchronized with |
| 41 // panel dragging. This means that if the scrolling subview is being scrolled, |
| 42 // that motion will not cause the panel to move, but if the scrolling reaches |
| 43 // the end of its possible range, the gesture will then start dragging the |
| 44 // panel. |
| 45 @property(nonatomic, assign) |
| 46 UIView<ContextualSearchPanelScrollSynchronizer>* scrollSynchronizer; |
| 47 |
| 48 // Private readonly property to be used by weak pointers to |self| for non- |
| 49 // retaining access to the underlying ivar in blocks. |
| 50 @property(nonatomic, readonly) ContextualSearchPanelObservers* observers; |
| 51 |
| 52 // Utility to generate a PanelMotion struct for the panel's current position. |
| 53 - (ContextualSearch::PanelMotion)motion; |
| 54 @end |
| 55 |
| 56 @implementation ContextualSearchPanelView { |
| 57 UIStackView* _contents; |
| 58 |
| 59 // Constraints that define the size of this view. These will be cleared and |
| 60 // regenerated when the horizontal size class changes. |
| 61 base::scoped_nsobject<NSArray> _sizingConstraints; |
| 62 |
| 63 CGPoint _draggingStartPosition; |
| 64 CGPoint _scrolledOffset; |
| 65 base::scoped_nsobject<UIPanGestureRecognizer> _dragRecognizer; |
| 66 |
| 67 base::scoped_nsobject<ContextualSearchPanelObservers> _observers; |
| 68 |
| 69 base::scoped_nsobject<PanelConfiguration> _configuration; |
| 70 |
| 71 base::WeakNSProtocol<id<ContextualSearchPanelScrollSynchronizer>> |
| 72 _scrollSynchronizer; |
| 73 |
| 74 // Guide that's used to position this view. |
| 75 base::WeakNSObject<UILayoutGuide> _positioningGuide; |
| 76 // Constraint that sets the size of |_positioningView| so this view is |
| 77 // positioned correctly for its state. |
| 78 base::WeakNSObject<NSLayoutConstraint> _positioningViewConstraint; |
| 79 // Other constraints that determine the position of this view. |
| 80 base::scoped_nsobject<NSArray> _positioningConstraints; |
| 81 |
| 82 // Promotion state variables. |
| 83 BOOL _resizingForPromotion; |
| 84 CGFloat _promotionVerticalOffset; |
| 85 |
| 86 // YES if dragging started inside the content view and scrolling is possible. |
| 87 BOOL _maybeScrollContent; |
| 88 // YES if the drag is happening along with scrolling the content view. |
| 89 BOOL _isScrollingContent; |
| 90 |
| 91 // YES if dragging upwards has occurred. |
| 92 BOOL _hasDraggedUp; |
| 93 } |
| 94 |
| 95 @synthesize state = _state; |
| 96 |
| 97 + (BOOL)requiresConstraintBasedLayout { |
| 98 return YES; |
| 99 } |
| 100 |
| 101 #pragma mark - Initializers |
| 102 |
| 103 - (instancetype)initWithConfiguration:(PanelConfiguration*)configuration { |
| 104 if ((self = [super initWithFrame:CGRectZero])) { |
| 105 _configuration.reset([configuration retain]); |
| 106 _state = ContextualSearch::DISMISSED; |
| 107 |
| 108 self.translatesAutoresizingMaskIntoConstraints = NO; |
| 109 self.backgroundColor = [UIColor whiteColor]; |
| 110 self.accessibilityIdentifier = @"contextualSearchPanel"; |
| 111 |
| 112 _observers.reset([[ContextualSearchPanelObservers |
| 113 observersWithProtocol:@protocol(ContextualSearchPanelMotionObserver)] |
| 114 retain]); |
| 115 [self addMotionObserver:self]; |
| 116 |
| 117 // Add gesture recognizer. |
| 118 _dragRecognizer.reset([[UIPanGestureRecognizer alloc] |
| 119 initWithTarget:self |
| 120 action:@selector(handleDragFrom:)]); |
| 121 [self addGestureRecognizer:_dragRecognizer]; |
| 122 [_dragRecognizer setDelegate:self]; |
| 123 |
| 124 // Set up the stack view that holds the panel content |
| 125 _contents = [[[UIStackView alloc] initWithFrame:self.bounds] autorelease]; |
| 126 [self addSubview:_contents]; |
| 127 _contents.translatesAutoresizingMaskIntoConstraints = NO; |
| 128 _contents.accessibilityIdentifier = @"panelContents"; |
| 129 [NSLayoutConstraint activateConstraints:@[ |
| 130 [_contents.centerXAnchor constraintEqualToAnchor:self.centerXAnchor], |
| 131 [_contents.centerYAnchor constraintEqualToAnchor:self.centerYAnchor], |
| 132 [_contents.widthAnchor constraintEqualToAnchor:self.widthAnchor], |
| 133 [_contents.heightAnchor constraintEqualToAnchor:self.heightAnchor] |
| 134 ]]; |
| 135 _contents.axis = UILayoutConstraintAxisVertical; |
| 136 } |
| 137 return self; |
| 138 } |
| 139 |
| 140 - (instancetype)initWithCoder:(NSCoder*)aDecoder NS_UNAVAILABLE { |
| 141 NOTREACHED(); |
| 142 return nil; |
| 143 } |
| 144 |
| 145 - (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE { |
| 146 NOTREACHED(); |
| 147 return nil; |
| 148 } |
| 149 |
| 150 #pragma mark - Public content views |
| 151 |
| 152 - (void)addContentViews:(NSArray*)contentViews { |
| 153 for (UIView* view in contentViews) { |
| 154 if ([view |
| 155 conformsToProtocol:@protocol( |
| 156 ContextualSearchPanelScrollSynchronizer)]) { |
| 157 self.scrollSynchronizer = |
| 158 static_cast<UIView<ContextualSearchPanelScrollSynchronizer>*>(view); |
| 159 } |
| 160 if ([view conformsToProtocol:@protocol( |
| 161 ContextualSearchPanelMotionObserver)]) { |
| 162 [self |
| 163 addMotionObserver:static_cast< |
| 164 id<ContextualSearchPanelMotionObserver>>(view)]; |
| 165 } |
| 166 [_contents addArrangedSubview:view]; |
| 167 } |
| 168 } |
| 169 |
| 170 #pragma mark - Public observer methods |
| 171 |
| 172 - (void)addMotionObserver:(id<ContextualSearchPanelMotionObserver>)observer { |
| 173 [_observers addObserver:observer]; |
| 174 } |
| 175 |
| 176 - (void)removeMotionObserver:(id<ContextualSearchPanelMotionObserver>)observer { |
| 177 [_observers removeObserver:observer]; |
| 178 } |
| 179 |
| 180 - (void)prepareForPromotion { |
| 181 self.scrollSynchronizer = nil; |
| 182 [_observers panelWillPromote:self]; |
| 183 } |
| 184 |
| 185 - (void)promoteToMatchSuperviewWithVerticalOffset:(CGFloat)offset { |
| 186 _resizingForPromotion = YES; |
| 187 _promotionVerticalOffset = offset; |
| 188 [NSLayoutConstraint deactivateConstraints:_sizingConstraints]; |
| 189 [NSLayoutConstraint deactivateConstraints:_positioningConstraints]; |
| 190 [[_positioningGuide owningView] removeLayoutGuide:_positioningGuide]; |
| 191 [_observers panelIsPromoting:self]; |
| 192 [self setNeedsUpdateConstraints]; |
| 193 [self updateConstraintsIfNeeded]; |
| 194 [self layoutIfNeeded]; |
| 195 } |
| 196 |
| 197 #pragma mark - Public property getters/setters |
| 198 |
| 199 - (PanelConfiguration*)configuration { |
| 200 return _configuration.get(); |
| 201 } |
| 202 |
| 203 - (void)setScrollSynchronizer: |
| 204 (id<ContextualSearchPanelScrollSynchronizer>)scrollSynchronizer { |
| 205 _scrollSynchronizer.reset(scrollSynchronizer); |
| 206 } |
| 207 |
| 208 - (id<ContextualSearchPanelScrollSynchronizer>)scrollSynchronizer { |
| 209 return _scrollSynchronizer; |
| 210 } |
| 211 |
| 212 - (ContextualSearchPanelObservers*)observers { |
| 213 return _observers; |
| 214 } |
| 215 |
| 216 - (void)setState:(ContextualSearch::PanelState)state { |
| 217 if (state == _state) |
| 218 return; |
| 219 |
| 220 [_positioningViewConstraint setActive:NO]; |
| 221 _positioningViewConstraint.reset(); |
| 222 base::WeakNSObject<ContextualSearchPanelView> weakSelf(self); |
| 223 void (^transform)(void) = ^{ |
| 224 base::scoped_nsobject<ContextualSearchPanelView> strongSelf( |
| 225 [weakSelf retain]); |
| 226 if (strongSelf) { |
| 227 [strongSelf setNeedsUpdateConstraints]; |
| 228 [[strongSelf superview] layoutIfNeeded]; |
| 229 [[strongSelf observers] panel:strongSelf |
| 230 didMoveWithMotion:[strongSelf motion]]; |
| 231 } |
| 232 }; |
| 233 |
| 234 base::mac::ScopedBlock<ProceduralBlockWithBool> completion; |
| 235 NSTimeInterval animationDuration; |
| 236 if (state == ContextualSearch::DISMISSED) { |
| 237 animationDuration = kDismissAnimationDuration; |
| 238 completion.reset( |
| 239 ^(BOOL) { |
| 240 [weakSelf setHidden:YES]; |
| 241 }, |
| 242 base::scoped_policy::RETAIN); |
| 243 } else { |
| 244 self.hidden = NO; |
| 245 animationDuration = kPanelAnimationDuration; |
| 246 } |
| 247 |
| 248 // Animations from a dismissed state are EaseOut, others are EaseInOut. |
| 249 ios::material::Curve curve = _state == ContextualSearch::DISMISSED |
| 250 ? ios::material::CurveEaseOut |
| 251 : ios::material::CurveEaseInOut; |
| 252 |
| 253 ContextualSearch::PanelState previousState = _state; |
| 254 _state = state; |
| 255 [_observers panel:self didChangeToState:_state fromState:previousState]; |
| 256 |
| 257 [UIView cr_animateWithDuration:animationDuration |
| 258 delay:0 |
| 259 curve:curve |
| 260 options:UIViewAnimationOptionBeginFromCurrentState |
| 261 animations:transform |
| 262 completion:completion]; |
| 263 } |
| 264 |
| 265 #pragma mark - UIView methods |
| 266 |
| 267 - (void)updateConstraints { |
| 268 if (_resizingForPromotion) { |
| 269 [self.widthAnchor constraintEqualToAnchor:self.superview.widthAnchor] |
| 270 .active = YES; |
| 271 [self.centerXAnchor constraintEqualToAnchor:self.superview.centerXAnchor] |
| 272 .active = YES; |
| 273 [self.heightAnchor constraintEqualToAnchor:self.superview.heightAnchor |
| 274 constant:-_promotionVerticalOffset] |
| 275 .active = YES; |
| 276 [self.topAnchor constraintEqualToAnchor:self.superview.topAnchor |
| 277 constant:_promotionVerticalOffset] |
| 278 .active = YES; |
| 279 } else { |
| 280 // Don't update sizing constraints if there isn't a defined horizontal size |
| 281 // yet. |
| 282 if (self.traitCollection.horizontalSizeClass != |
| 283 UIUserInterfaceSizeClassUnspecified && |
| 284 !_sizingConstraints) { |
| 285 _sizingConstraints.reset( |
| 286 [[_configuration constraintsForSizingPanel:self] retain]); |
| 287 [NSLayoutConstraint activateConstraints:_sizingConstraints]; |
| 288 } |
| 289 // Update positioning constraints if they don't exist. |
| 290 if (!_positioningConstraints) { |
| 291 NSArray* positioningConstraints = @[ |
| 292 [[_positioningGuide topAnchor] |
| 293 constraintEqualToAnchor:self.superview.topAnchor], |
| 294 [[_positioningGuide bottomAnchor] |
| 295 constraintEqualToAnchor:self.topAnchor] |
| 296 ]; |
| 297 [NSLayoutConstraint activateConstraints:positioningConstraints]; |
| 298 |
| 299 _positioningConstraints.reset([positioningConstraints retain]); |
| 300 } |
| 301 // Always update the positioning view constraint. |
| 302 _positioningViewConstraint.reset([self.configuration |
| 303 constraintForPositioningGuide:_positioningGuide |
| 304 atState:self.state]); |
| 305 [_positioningViewConstraint setActive:YES]; |
| 306 } |
| 307 [super updateConstraints]; |
| 308 } |
| 309 |
| 310 - (void)didMoveToSuperview { |
| 311 if (!self.superview) |
| 312 return; |
| 313 // Set up the invisible positioning view used to constrain this view's |
| 314 // position. |
| 315 UILayoutGuide* positioningGuide = [[[UILayoutGuide alloc] init] autorelease]; |
| 316 positioningGuide.identifier = @"contextualSearchPosition"; |
| 317 [self.superview addLayoutGuide:positioningGuide]; |
| 318 _positioningGuide.reset(positioningGuide); |
| 319 [self setNeedsUpdateConstraints]; |
| 320 } |
| 321 |
| 322 - (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection { |
| 323 if (previousTraitCollection.horizontalSizeClass == |
| 324 self.traitCollection.horizontalSizeClass) { |
| 325 return; |
| 326 } |
| 327 |
| 328 [self dismissPanel]; |
| 329 |
| 330 [_configuration |
| 331 setHorizontalSizeClass:self.traitCollection.horizontalSizeClass]; |
| 332 [NSLayoutConstraint deactivateConstraints:_sizingConstraints]; |
| 333 _sizingConstraints.reset(); |
| 334 [self setNeedsUpdateConstraints]; |
| 335 } |
| 336 |
| 337 - (void)layoutSubviews { |
| 338 [super layoutSubviews]; |
| 339 self.configuration.containerSize = self.superview.bounds.size; |
| 340 // Update the shadow path for this view. |
| 341 // Consider switching to "full" MDCShadowLayer. |
| 342 MDCShadowMetrics* metrics = |
| 343 [MDCShadowMetrics metricsWithElevation:kShadowElevation]; |
| 344 UIBezierPath* shadowPath = [UIBezierPath bezierPathWithRect:self.bounds]; |
| 345 self.layer.shadowPath = shadowPath.CGPath; |
| 346 self.layer.shadowOpacity = metrics.topShadowOpacity; |
| 347 self.layer.shadowRadius = metrics.topShadowRadius; |
| 348 } |
| 349 |
| 350 - (void)dismissPanel { |
| 351 ContextualSearch::PanelMotion motion; |
| 352 motion.state = ContextualSearch::DISMISSED; |
| 353 motion.nextState = ContextualSearch::DISMISSED; |
| 354 motion.gradation = 0; |
| 355 motion.position = 0; |
| 356 [_observers panel:self didStopMovingWithMotion:motion]; |
| 357 } |
| 358 |
| 359 - (void)dealloc { |
| 360 [self removeMotionObserver:self]; |
| 361 [self removeGestureRecognizer:_dragRecognizer]; |
| 362 [[_positioningGuide owningView] removeLayoutGuide:_positioningGuide]; |
| 363 [super dealloc]; |
| 364 } |
| 365 |
| 366 #pragma mark - Gesture recognizer callbacks |
| 367 |
| 368 - (void)handleDragFrom:(UIGestureRecognizer*)gestureRecognizer { |
| 369 UIPanGestureRecognizer* recognizer = |
| 370 static_cast<UIPanGestureRecognizer*>(gestureRecognizer); |
| 371 if ([recognizer state] == UIGestureRecognizerStateCancelled) { |
| 372 recognizer.enabled = YES; |
| 373 [self dismissPanel]; |
| 374 return; |
| 375 } |
| 376 |
| 377 CGPoint dragOffset = [recognizer translationInView:[self superview]]; |
| 378 BOOL isScrolling = NO; |
| 379 if (_maybeScrollContent && self.scrollSynchronizer.scrolled) { |
| 380 isScrolling = YES; |
| 381 _scrolledOffset = dragOffset; |
| 382 } else { |
| 383 // Adjust drag offset for prior scrolling |
| 384 dragOffset.y -= _scrolledOffset.y; |
| 385 } |
| 386 |
| 387 CGPoint newOrigin = _draggingStartPosition; |
| 388 newOrigin.y += dragOffset.y; |
| 389 |
| 390 // Clamp the drag to covering height. |
| 391 CGFloat coveringY = |
| 392 [self.configuration positionForPanelState:ContextualSearch::COVERING]; |
| 393 if (newOrigin.y < coveringY) { |
| 394 newOrigin.y = coveringY; |
| 395 dragOffset.y = coveringY - _draggingStartPosition.y; |
| 396 [recognizer setTranslation:dragOffset inView:[self superview]]; |
| 397 } |
| 398 |
| 399 // If the view hasn't moved up yet and it's moving down (dragOffset.y > 0) |
| 400 // and it's moving from a peeking state, clamp the offset y to 0. |
| 401 if (_state == ContextualSearch::PEEKING && !_hasDraggedUp && |
| 402 dragOffset.y > 0) { |
| 403 dragOffset.y = 0; |
| 404 [recognizer setTranslation:dragOffset inView:[self superview]]; |
| 405 } |
| 406 |
| 407 switch ([recognizer state]) { |
| 408 case UIGestureRecognizerStateBegan: |
| 409 _draggingStartPosition = self.frame.origin; |
| 410 _scrolledOffset = CGPointZero; |
| 411 _hasDraggedUp = NO; |
| 412 _maybeScrollContent = CGRectContainsPoint( |
| 413 self.scrollSynchronizer.frame, [recognizer locationInView:self]); |
| 414 break; |
| 415 case UIGestureRecognizerStateEnded: |
| 416 if (!CGPointEqualToPoint(self.frame.origin, _draggingStartPosition)) |
| 417 [_observers panel:self didStopMovingWithMotion:[self motion]]; |
| 418 break; |
| 419 case UIGestureRecognizerStateChanged: { |
| 420 if (!_hasDraggedUp && dragOffset.y < 0) |
| 421 _hasDraggedUp = YES; |
| 422 |
| 423 // Don't drag the pane if scrolling is happening. |
| 424 if (isScrolling) |
| 425 break; |
| 426 |
| 427 CGRect frame = self.frame; |
| 428 frame.origin.y = _draggingStartPosition.y + dragOffset.y; |
| 429 self.frame = frame; |
| 430 [_observers panel:self didMoveWithMotion:[self motion]]; |
| 431 } break; |
| 432 default: |
| 433 break; |
| 434 } |
| 435 } |
| 436 |
| 437 - (ContextualSearch::PanelMotion)motion { |
| 438 ContextualSearch::PanelMotion motion; |
| 439 motion.position = self.frame.origin.y; |
| 440 motion.state = [self.configuration panelStateForPosition:motion.position]; |
| 441 motion.nextState = static_cast<ContextualSearch::PanelState>( |
| 442 MIN(motion.state + 1, ContextualSearch::COVERING)); |
| 443 motion.gradation = [_configuration gradationToState:motion.nextState |
| 444 fromState:motion.state |
| 445 atPosition:motion.position]; |
| 446 return motion; |
| 447 } |
| 448 |
| 449 #pragma mark - ContextualSearchPanelMotionDelegate methods |
| 450 |
| 451 - (void)panel:(ContextualSearchPanelView*)panel |
| 452 didMoveWithMotion:(ContextualSearch::PanelMotion)motion { |
| 453 if (motion.state == ContextualSearch::PREVIEWING) { |
| 454 MDCShadowMetrics* metrics = |
| 455 [MDCShadowMetrics metricsWithElevation:kShadowElevation]; |
| 456 self.layer.shadowOpacity = |
| 457 metrics.topShadowOpacity * (1.0 - motion.gradation); |
| 458 } |
| 459 } |
| 460 |
| 461 #pragma mark - UIGestureRecognizerDelegate methods |
| 462 |
| 463 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer |
| 464 shouldRecognizeSimultaneouslyWithGestureRecognizer: |
| 465 (UIGestureRecognizer*)otherGestureRecognizer { |
| 466 // Allow the drag recognizer and the panel content scroll recognizer to |
| 467 // co-recognize. |
| 468 if (gestureRecognizer == _dragRecognizer.get() && |
| 469 otherGestureRecognizer == self.scrollSynchronizer.scrollRecognizer) { |
| 470 return YES; |
| 471 } |
| 472 |
| 473 if (gestureRecognizer == _dragRecognizer.get() && |
| 474 [_dragRecognizer state] == UIGestureRecognizerStateChanged) { |
| 475 [gestureRecognizer setEnabled:NO]; |
| 476 } |
| 477 return NO; |
| 478 } |
| 479 |
| 480 @end |
OLD | NEW |