| Index: ios/chrome/browser/ui/contextual_search/contextual_search_panel_view.mm
|
| diff --git a/ios/chrome/browser/ui/contextual_search/contextual_search_panel_view.mm b/ios/chrome/browser/ui/contextual_search/contextual_search_panel_view.mm
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..1b556aa9945c38874c29a1d9778e1f4f5fe39f12
|
| --- /dev/null
|
| +++ b/ios/chrome/browser/ui/contextual_search/contextual_search_panel_view.mm
|
| @@ -0,0 +1,480 @@
|
| +// 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/ios/weak_nsobject.h"
|
| +#include "base/logging.h"
|
| +#include "base/mac/scoped_block.h"
|
| +#include "base/mac/scoped_nsobject.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"
|
| +
|
| +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, assign)
|
| + 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, 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.
|
| + base::scoped_nsobject<NSArray> _sizingConstraints;
|
| +
|
| + CGPoint _draggingStartPosition;
|
| + CGPoint _scrolledOffset;
|
| + base::scoped_nsobject<UIPanGestureRecognizer> _dragRecognizer;
|
| +
|
| + base::scoped_nsobject<ContextualSearchPanelObservers> _observers;
|
| +
|
| + base::scoped_nsobject<PanelConfiguration> _configuration;
|
| +
|
| + base::WeakNSProtocol<id<ContextualSearchPanelScrollSynchronizer>>
|
| + _scrollSynchronizer;
|
| +
|
| + // Guide that's used to position this view.
|
| + base::WeakNSObject<UILayoutGuide> _positioningGuide;
|
| + // Constraint that sets the size of |_positioningView| so this view is
|
| + // positioned correctly for its state.
|
| + base::WeakNSObject<NSLayoutConstraint> _positioningViewConstraint;
|
| + // Other constraints that determine the position of this view.
|
| + base::scoped_nsobject<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;
|
| +
|
| ++ (BOOL)requiresConstraintBasedLayout {
|
| + return YES;
|
| +}
|
| +
|
| +#pragma mark - Initializers
|
| +
|
| +- (instancetype)initWithConfiguration:(PanelConfiguration*)configuration {
|
| + if ((self = [super initWithFrame:CGRectZero])) {
|
| + _configuration.reset([configuration retain]);
|
| + _state = ContextualSearch::DISMISSED;
|
| +
|
| + self.translatesAutoresizingMaskIntoConstraints = NO;
|
| + self.backgroundColor = [UIColor whiteColor];
|
| + self.accessibilityIdentifier = @"contextualSearchPanel";
|
| +
|
| + _observers.reset([[ContextualSearchPanelObservers
|
| + observersWithProtocol:@protocol(ContextualSearchPanelMotionObserver)]
|
| + retain]);
|
| + [self addMotionObserver:self];
|
| +
|
| + // Add gesture recognizer.
|
| + _dragRecognizer.reset([[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] autorelease];
|
| + [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.get();
|
| +}
|
| +
|
| +- (void)setScrollSynchronizer:
|
| + (id<ContextualSearchPanelScrollSynchronizer>)scrollSynchronizer {
|
| + _scrollSynchronizer.reset(scrollSynchronizer);
|
| +}
|
| +
|
| +- (id<ContextualSearchPanelScrollSynchronizer>)scrollSynchronizer {
|
| + return _scrollSynchronizer;
|
| +}
|
| +
|
| +- (ContextualSearchPanelObservers*)observers {
|
| + return _observers;
|
| +}
|
| +
|
| +- (void)setState:(ContextualSearch::PanelState)state {
|
| + if (state == _state)
|
| + return;
|
| +
|
| + [_positioningViewConstraint setActive:NO];
|
| + _positioningViewConstraint.reset();
|
| + base::WeakNSObject<ContextualSearchPanelView> weakSelf(self);
|
| + void (^transform)(void) = ^{
|
| + base::scoped_nsobject<ContextualSearchPanelView> strongSelf(
|
| + [weakSelf retain]);
|
| + if (strongSelf) {
|
| + [strongSelf setNeedsUpdateConstraints];
|
| + [[strongSelf superview] layoutIfNeeded];
|
| + [[strongSelf observers] panel:strongSelf
|
| + didMoveWithMotion:[strongSelf motion]];
|
| + }
|
| + };
|
| +
|
| + base::mac::ScopedBlock<ProceduralBlockWithBool> completion;
|
| + NSTimeInterval animationDuration;
|
| + if (state == ContextualSearch::DISMISSED) {
|
| + animationDuration = kDismissAnimationDuration;
|
| + completion.reset(
|
| + ^(BOOL) {
|
| + [weakSelf setHidden:YES];
|
| + },
|
| + base::scoped_policy::RETAIN);
|
| + } 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.reset(
|
| + [[_configuration constraintsForSizingPanel:self] retain]);
|
| + [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.reset([positioningConstraints retain]);
|
| + }
|
| + // Always update the positioning view constraint.
|
| + _positioningViewConstraint.reset([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] autorelease];
|
| + positioningGuide.identifier = @"contextualSearchPosition";
|
| + [self.superview addLayoutGuide:positioningGuide];
|
| + _positioningGuide.reset(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.reset();
|
| + [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];
|
| + [super dealloc];
|
| +}
|
| +
|
| +#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.get() &&
|
| + otherGestureRecognizer == self.scrollSynchronizer.scrollRecognizer) {
|
| + return YES;
|
| + }
|
| +
|
| + if (gestureRecognizer == _dragRecognizer.get() &&
|
| + [_dragRecognizer state] == UIGestureRecognizerStateChanged) {
|
| + [gestureRecognizer setEnabled:NO];
|
| + }
|
| + return NO;
|
| +}
|
| +
|
| +@end
|
|
|