blob: ad94baac8c5a43ea34b59cb1ef0d3e1e171421dc [file] [log] [blame]
// Copyright 2014 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/contextual_search/contextual_search_panel_view.h"
#import "base/ios/crb_protocol_observers.h"
#include "base/logging.h"
#import "ios/chrome/browser/procedural_block_types.h"
#import "ios/chrome/browser/ui/contextual_search/contextual_search_panel_protocols.h"
#import "ios/chrome/browser/ui/uikit_ui_util.h"
#import "ios/chrome/common/material_timing.h"
#import "ios/third_party/material_components_ios/src/components/ShadowElevations/src/MaterialShadowElevations.h"
#import "ios/third_party/material_components_ios/src/components/ShadowLayer/src/MaterialShadowLayer.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// Animation timings.
const NSTimeInterval kPanelAnimationDuration = ios::material::kDuration3;
const NSTimeInterval kDismissAnimationDuration = ios::material::kDuration1;
// Elevation (in MD vertical space) of the panel when dismissed and peeking.
const CGFloat kShadowElevation = MDCShadowElevationMenu;
} // namespace
@interface ContextualSearchPanelObservers
: CRBProtocolObservers<ContextualSearchPanelMotionObserver>
@end
@implementation ContextualSearchPanelObservers
@end
@interface ContextualSearchPanelView ()<UIGestureRecognizerDelegate,
ContextualSearchPanelMotionObserver>
// A subview whose content scrolls and whose scrolling is synchronized with
// panel dragging. This means that if the scrolling subview is being scrolled,
// that motion will not cause the panel to move, but if the scrolling reaches
// the end of its possible range, the gesture will then start dragging the
// panel.
@property(nonatomic, weak)
UIView<ContextualSearchPanelScrollSynchronizer>* scrollSynchronizer;
// Private readonly property to be used by weak pointers to |self| for non-
// retaining access to the underlying ivar in blocks.
@property(nonatomic, strong, readonly)
ContextualSearchPanelObservers* observers;
// Utility to generate a PanelMotion struct for the panel's current position.
- (ContextualSearch::PanelMotion)motion;
@end
@implementation ContextualSearchPanelView {
UIStackView* _contents;
// Constraints that define the size of this view. These will be cleared and
// regenerated when the horizontal size class changes.
NSArray* _sizingConstraints;
CGPoint _draggingStartPosition;
CGPoint _scrolledOffset;
UIPanGestureRecognizer* _dragRecognizer;
// Guide that's used to position this view.
__weak UILayoutGuide* _positioningGuide;
// Constraint that sets the size of |_positioningView| so this view is
// positioned correctly for its state.
__weak NSLayoutConstraint* _positioningViewConstraint;
// Other constraints that determine the position of this view.
NSArray* _positioningConstraints;
// Promotion state variables.
BOOL _resizingForPromotion;
CGFloat _promotionVerticalOffset;
// YES if dragging started inside the content view and scrolling is possible.
BOOL _maybeScrollContent;
// YES if the drag is happening along with scrolling the content view.
BOOL _isScrollingContent;
// YES if dragging upwards has occurred.
BOOL _hasDraggedUp;
}
@synthesize state = _state;
@synthesize scrollSynchronizer = _scrollSynchronizer;
@synthesize configuration = _configuration;
@synthesize observers = _observers;
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
#pragma mark - Initializers
- (instancetype)initWithConfiguration:(PanelConfiguration*)configuration {
if ((self = [super initWithFrame:CGRectZero])) {
_configuration = configuration;
_state = ContextualSearch::DISMISSED;
self.translatesAutoresizingMaskIntoConstraints = NO;
self.backgroundColor = [UIColor whiteColor];
self.accessibilityIdentifier = @"contextualSearchPanel";
_observers = [ContextualSearchPanelObservers
observersWithProtocol:@protocol(ContextualSearchPanelMotionObserver)];
[self addMotionObserver:self];
// Add gesture recognizer.
_dragRecognizer = [[UIPanGestureRecognizer alloc]
initWithTarget:self
action:@selector(handleDragFrom:)];
[self addGestureRecognizer:_dragRecognizer];
[_dragRecognizer setDelegate:self];
// Set up the stack view that holds the panel content
_contents = [[UIStackView alloc] initWithFrame:self.bounds];
[self addSubview:_contents];
_contents.translatesAutoresizingMaskIntoConstraints = NO;
_contents.accessibilityIdentifier = @"panelContents";
[NSLayoutConstraint activateConstraints:@[
[_contents.centerXAnchor constraintEqualToAnchor:self.centerXAnchor],
[_contents.centerYAnchor constraintEqualToAnchor:self.centerYAnchor],
[_contents.widthAnchor constraintEqualToAnchor:self.widthAnchor],
[_contents.heightAnchor constraintEqualToAnchor:self.heightAnchor]
]];
_contents.axis = UILayoutConstraintAxisVertical;
}
return self;
}
- (instancetype)initWithCoder:(NSCoder*)aDecoder NS_UNAVAILABLE {
NOTREACHED();
return nil;
}
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE {
NOTREACHED();
return nil;
}
#pragma mark - Public content views
- (void)addContentViews:(NSArray*)contentViews {
for (UIView* view in contentViews) {
if ([view
conformsToProtocol:@protocol(
ContextualSearchPanelScrollSynchronizer)]) {
self.scrollSynchronizer =
static_cast<UIView<ContextualSearchPanelScrollSynchronizer>*>(view);
}
if ([view conformsToProtocol:@protocol(
ContextualSearchPanelMotionObserver)]) {
[self
addMotionObserver:static_cast<
id<ContextualSearchPanelMotionObserver>>(view)];
}
[_contents addArrangedSubview:view];
}
}
#pragma mark - Public observer methods
- (void)addMotionObserver:(id<ContextualSearchPanelMotionObserver>)observer {
[_observers addObserver:observer];
}
- (void)removeMotionObserver:(id<ContextualSearchPanelMotionObserver>)observer {
[_observers removeObserver:observer];
}
- (void)prepareForPromotion {
self.scrollSynchronizer = nil;
[_observers panelWillPromote:self];
}
- (void)promoteToMatchSuperviewWithVerticalOffset:(CGFloat)offset {
_resizingForPromotion = YES;
_promotionVerticalOffset = offset;
[NSLayoutConstraint deactivateConstraints:_sizingConstraints];
[NSLayoutConstraint deactivateConstraints:_positioningConstraints];
[[_positioningGuide owningView] removeLayoutGuide:_positioningGuide];
[_observers panelIsPromoting:self];
[self setNeedsUpdateConstraints];
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
#pragma mark - Public property getters/setters
- (PanelConfiguration*)configuration {
return _configuration;
}
- (ContextualSearchPanelObservers*)observers {
return _observers;
}
- (void)setState:(ContextualSearch::PanelState)state {
if (state == _state)
return;
[_positioningViewConstraint setActive:NO];
_positioningViewConstraint = nil;
__weak ContextualSearchPanelView* weakSelf = self;
void (^transform)(void) = ^{
ContextualSearchPanelView* strongSelf = weakSelf;
if (strongSelf) {
[strongSelf setNeedsUpdateConstraints];
[[strongSelf superview] layoutIfNeeded];
[[strongSelf observers] panel:strongSelf
didMoveWithMotion:[strongSelf motion]];
}
};
ProceduralBlockWithBool completion;
NSTimeInterval animationDuration;
if (state == ContextualSearch::DISMISSED) {
animationDuration = kDismissAnimationDuration;
completion = [^(BOOL) {
[weakSelf setHidden:YES];
} copy];
} else {
self.hidden = NO;
animationDuration = kPanelAnimationDuration;
}
// Animations from a dismissed state are EaseOut, others are EaseInOut.
ios::material::Curve curve = _state == ContextualSearch::DISMISSED
? ios::material::CurveEaseOut
: ios::material::CurveEaseInOut;
ContextualSearch::PanelState previousState = _state;
_state = state;
[_observers panel:self didChangeToState:_state fromState:previousState];
[UIView cr_animateWithDuration:animationDuration
delay:0
curve:curve
options:UIViewAnimationOptionBeginFromCurrentState
animations:transform
completion:completion];
}
#pragma mark - UIView methods
- (void)updateConstraints {
if (_resizingForPromotion) {
[self.widthAnchor constraintEqualToAnchor:self.superview.widthAnchor]
.active = YES;
[self.centerXAnchor constraintEqualToAnchor:self.superview.centerXAnchor]
.active = YES;
[self.heightAnchor constraintEqualToAnchor:self.superview.heightAnchor
constant:-_promotionVerticalOffset]
.active = YES;
[self.topAnchor constraintEqualToAnchor:self.superview.topAnchor
constant:_promotionVerticalOffset]
.active = YES;
} else {
// Don't update sizing constraints if there isn't a defined horizontal size
// yet.
if (self.traitCollection.horizontalSizeClass !=
UIUserInterfaceSizeClassUnspecified &&
!_sizingConstraints) {
_sizingConstraints = [_configuration constraintsForSizingPanel:self];
[NSLayoutConstraint activateConstraints:_sizingConstraints];
}
// Update positioning constraints if they don't exist.
if (!_positioningConstraints) {
NSArray* positioningConstraints = @[
[[_positioningGuide topAnchor]
constraintEqualToAnchor:self.superview.topAnchor],
[[_positioningGuide bottomAnchor]
constraintEqualToAnchor:self.topAnchor]
];
[NSLayoutConstraint activateConstraints:positioningConstraints];
_positioningConstraints = positioningConstraints;
}
// Always update the positioning view constraint.
_positioningViewConstraint =
[self.configuration constraintForPositioningGuide:_positioningGuide
atState:self.state];
[_positioningViewConstraint setActive:YES];
}
[super updateConstraints];
}
- (void)didMoveToSuperview {
if (!self.superview)
return;
// Set up the invisible positioning view used to constrain this view's
// position.
UILayoutGuide* positioningGuide = [[UILayoutGuide alloc] init];
positioningGuide.identifier = @"contextualSearchPosition";
[self.superview addLayoutGuide:positioningGuide];
_positioningGuide = positioningGuide;
[self setNeedsUpdateConstraints];
}
- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
if (previousTraitCollection.horizontalSizeClass ==
self.traitCollection.horizontalSizeClass) {
return;
}
[self dismissPanel];
[_configuration
setHorizontalSizeClass:self.traitCollection.horizontalSizeClass];
[NSLayoutConstraint deactivateConstraints:_sizingConstraints];
_sizingConstraints = nil;
[self setNeedsUpdateConstraints];
}
- (void)layoutSubviews {
[super layoutSubviews];
self.configuration.containerSize = self.superview.bounds.size;
// Update the shadow path for this view.
// Consider switching to "full" MDCShadowLayer.
MDCShadowMetrics* metrics =
[MDCShadowMetrics metricsWithElevation:kShadowElevation];
UIBezierPath* shadowPath = [UIBezierPath bezierPathWithRect:self.bounds];
self.layer.shadowPath = shadowPath.CGPath;
self.layer.shadowOpacity = metrics.topShadowOpacity;
self.layer.shadowRadius = metrics.topShadowRadius;
}
- (void)dismissPanel {
ContextualSearch::PanelMotion motion;
motion.state = ContextualSearch::DISMISSED;
motion.nextState = ContextualSearch::DISMISSED;
motion.gradation = 0;
motion.position = 0;
[_observers panel:self didStopMovingWithMotion:motion];
}
- (void)dealloc {
[self removeMotionObserver:self];
[self removeGestureRecognizer:_dragRecognizer];
[[_positioningGuide owningView] removeLayoutGuide:_positioningGuide];
}
#pragma mark - Gesture recognizer callbacks
- (void)handleDragFrom:(UIGestureRecognizer*)gestureRecognizer {
UIPanGestureRecognizer* recognizer =
static_cast<UIPanGestureRecognizer*>(gestureRecognizer);
if ([recognizer state] == UIGestureRecognizerStateCancelled) {
recognizer.enabled = YES;
[self dismissPanel];
return;
}
CGPoint dragOffset = [recognizer translationInView:[self superview]];
BOOL isScrolling = NO;
if (_maybeScrollContent && self.scrollSynchronizer.scrolled) {
isScrolling = YES;
_scrolledOffset = dragOffset;
} else {
// Adjust drag offset for prior scrolling
dragOffset.y -= _scrolledOffset.y;
}
CGPoint newOrigin = _draggingStartPosition;
newOrigin.y += dragOffset.y;
// Clamp the drag to covering height.
CGFloat coveringY =
[self.configuration positionForPanelState:ContextualSearch::COVERING];
if (newOrigin.y < coveringY) {
newOrigin.y = coveringY;
dragOffset.y = coveringY - _draggingStartPosition.y;
[recognizer setTranslation:dragOffset inView:[self superview]];
}
// If the view hasn't moved up yet and it's moving down (dragOffset.y > 0)
// and it's moving from a peeking state, clamp the offset y to 0.
if (_state == ContextualSearch::PEEKING && !_hasDraggedUp &&
dragOffset.y > 0) {
dragOffset.y = 0;
[recognizer setTranslation:dragOffset inView:[self superview]];
}
switch ([recognizer state]) {
case UIGestureRecognizerStateBegan:
_draggingStartPosition = self.frame.origin;
_scrolledOffset = CGPointZero;
_hasDraggedUp = NO;
_maybeScrollContent = CGRectContainsPoint(
self.scrollSynchronizer.frame, [recognizer locationInView:self]);
break;
case UIGestureRecognizerStateEnded:
if (!CGPointEqualToPoint(self.frame.origin, _draggingStartPosition))
[_observers panel:self didStopMovingWithMotion:[self motion]];
break;
case UIGestureRecognizerStateChanged: {
if (!_hasDraggedUp && dragOffset.y < 0)
_hasDraggedUp = YES;
// Don't drag the pane if scrolling is happening.
if (isScrolling)
break;
CGRect frame = self.frame;
frame.origin.y = _draggingStartPosition.y + dragOffset.y;
self.frame = frame;
[_observers panel:self didMoveWithMotion:[self motion]];
} break;
default:
break;
}
}
- (ContextualSearch::PanelMotion)motion {
ContextualSearch::PanelMotion motion;
motion.position = self.frame.origin.y;
motion.state = [self.configuration panelStateForPosition:motion.position];
motion.nextState = static_cast<ContextualSearch::PanelState>(
MIN(motion.state + 1, ContextualSearch::COVERING));
motion.gradation = [_configuration gradationToState:motion.nextState
fromState:motion.state
atPosition:motion.position];
return motion;
}
#pragma mark - ContextualSearchPanelMotionDelegate methods
- (void)panel:(ContextualSearchPanelView*)panel
didMoveWithMotion:(ContextualSearch::PanelMotion)motion {
if (motion.state == ContextualSearch::PREVIEWING) {
MDCShadowMetrics* metrics =
[MDCShadowMetrics metricsWithElevation:kShadowElevation];
self.layer.shadowOpacity =
metrics.topShadowOpacity * (1.0 - motion.gradation);
}
}
#pragma mark - UIGestureRecognizerDelegate methods
- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:
(UIGestureRecognizer*)otherGestureRecognizer {
// Allow the drag recognizer and the panel content scroll recognizer to
// co-recognize.
if (gestureRecognizer == _dragRecognizer &&
otherGestureRecognizer == self.scrollSynchronizer.scrollRecognizer) {
return YES;
}
if (gestureRecognizer == _dragRecognizer &&
[_dragRecognizer state] == UIGestureRecognizerStateChanged) {
[gestureRecognizer setEnabled:NO];
}
return NO;
}
@end