blob: 13ab0462ba96c295b23a1450f08e17aef0631211 [file] [log] [blame]
// Copyright 2017 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/location_bar/location_bar_legacy_view.h"
#import "ios/chrome/browser/ui/animation_util.h"
#import "ios/chrome/browser/ui/omnibox/clipping_textfield_container.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_text_field_ios.h"
#include "ios/chrome/browser/ui/rtl_geometry.h"
#include "ios/chrome/browser/ui/ui_util.h"
#import "ios/chrome/browser/ui/uikit_ui_util.h"
#import "ios/chrome/common/material_timing.h"
#include "ios/chrome/grit/ios_strings.h"
#include "ios/chrome/grit/ios_theme_resources.h"
#include "skia/ext/skia_utils_ios.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/image/image.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
const CGFloat kLeadingButtonEdgeOffset = 9;
// Offset from the leading edge to the textfield when no image is shown.
const CGFloat kTextFieldLeadingOffsetNoImage = 16;
// Space between the leading button and the textfield when a button is shown.
const CGFloat kTextFieldLeadingOffsetImage = 6;
// The default position animation is 10 pixels toward the trailing side, so
// that's a negative leading offset.
const LayoutOffset kPositionAnimationLeadingOffset = -10;
} // namespace
@interface OmniboxTextFieldIOS ()
// Gets the bounds of the rect covering the URL.
- (CGRect)preEditLabelRectForBounds:(CGRect)bounds;
// Creates the UILabel if it doesn't already exist and adds it as a
// subview.
- (void)createSelectionViewIfNecessary;
// Helper method used to set the text of this field. Updates the selection view
// to contain the correct inline autocomplete text.
- (void)setTextInternal:(NSAttributedString*)text
autocompleteLength:(NSUInteger)autocompleteLength;
// Override deleteBackward so that backspace can clear query refinement chips.
- (void)deleteBackward;
// Returns the layers affected by animations added by |-animateFadeWithStyle:|.
- (NSArray*)fadeAnimationLayers;
// Returns the text that is displayed in the field, including any inline
// autocomplete text that may be present as an NSString. Returns the same
// value as -|displayedText| but prefer to use this to avoid unnecessary
// conversion from NSString to base::string16 if possible.
- (NSString*)nsDisplayedText;
@end
#pragma mark - LocationBarEditView
@interface LocationBarLegacyView ()
// Constraints the leading textfield side to the leading of |self|.
// Active when the |leadingView| is nil or hidden.
@property(nonatomic, strong) NSLayoutConstraint* leadingTextfieldConstraint;
// When the |leadingButton| is not hidden, this is a constraint that links the
// leading edge of the button to self leading edge. Used for animations.
@property(nonatomic, strong) NSLayoutConstraint* leadingButtonLeadingConstraint;
// The textfield container. The |textField| is contained in it, and its frame
// should not be managed directly, instead the location bar uses this container.
// This is required to achieve desired text clipping of long URLs.
@property(nonatomic, strong) ClippingTextFieldContainer* textFieldContainer;
@end
@implementation LocationBarLegacyView
@synthesize textField = _textField;
@synthesize leadingButton = _leadingButton;
@synthesize leadingTextfieldConstraint = _leadingTextfieldConstraint;
@synthesize incognito = _incognito;
@synthesize leadingButtonLeadingConstraint = _leadingButtonLeadingConstraint;
@synthesize textFieldContainer = _textFieldContainer;
#pragma mark - Public properties
- (void)setLeadingButton:(UIButton*)leadingButton {
_leadingButton = leadingButton;
_leadingButton.translatesAutoresizingMaskIntoConstraints = NO;
[_leadingButton
setContentCompressionResistancePriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisHorizontal];
[_leadingButton
setContentCompressionResistancePriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisVertical];
[_leadingButton setContentHuggingPriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisHorizontal];
[_leadingButton setContentHuggingPriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisVertical];
}
#pragma mark - Public methods
- (instancetype)initWithFrame:(CGRect)frame
font:(UIFont*)font
textColor:(UIColor*)textColor
tintColor:(UIColor*)tintColor {
self = [super initWithFrame:frame];
if (self) {
_textField = [[OmniboxTextFieldIOS alloc] initWithFrame:frame
font:font
textColor:textColor
tintColor:tintColor];
// The text field is put into a container.
// TODO(crbug.com/789968): remove these insets when the location bar
// background is managed by this view and not toolbar controller. These
// insets allow the gradient masking of the omnibox to not extend beyond
// the omnibox background's visible frame.
self.layoutMargins = UIEdgeInsetsMake(3, 3, 3, 3);
_textFieldContainer = [[ClippingTextFieldContainer alloc]
initWithClippingTextField:_textField];
[self addSubview:_textFieldContainer];
_leadingTextfieldConstraint = [_textFieldContainer.leadingAnchor
constraintEqualToAnchor:self.leadingAnchor
constant:kTextFieldLeadingOffsetNoImage];
[NSLayoutConstraint activateConstraints:@[
[_textFieldContainer.trailingAnchor
constraintEqualToAnchor:self.layoutMarginsGuide.trailingAnchor],
[_textFieldContainer.topAnchor
constraintEqualToAnchor:self.layoutMarginsGuide.topAnchor],
[_textFieldContainer.bottomAnchor
constraintEqualToAnchor:self.layoutMarginsGuide.bottomAnchor],
_leadingTextfieldConstraint,
]];
_textFieldContainer.translatesAutoresizingMaskIntoConstraints = NO;
[_textFieldContainer
setContentCompressionResistancePriority:UILayoutPriorityDefaultLow
forAxis:
UILayoutConstraintAxisHorizontal];
}
return self;
}
- (void)setLeadingButtonHidden:(BOOL)hidden {
if (!_leadingButton) {
return;
}
if (hidden) {
[_leadingButton removeFromSuperview];
self.leadingTextfieldConstraint.active = YES;
} else {
[self addSubview:_leadingButton];
self.leadingTextfieldConstraint.active = NO;
self.leadingButtonLeadingConstraint = [self.layoutMarginsGuide.leadingAnchor
constraintEqualToAnchor:self.leadingButton.leadingAnchor
constant:-kLeadingButtonEdgeOffset];
NSLayoutConstraint* leadingButtonToTextField = nil;
leadingButtonToTextField = [self.leadingButton.trailingAnchor
constraintEqualToAnchor:self.textFieldContainer.leadingAnchor
constant:-kTextFieldLeadingOffsetImage];
[NSLayoutConstraint activateConstraints:@[
[_leadingButton.centerYAnchor
constraintEqualToAnchor:self.layoutMarginsGuide.centerYAnchor],
self.leadingButtonLeadingConstraint,
leadingButtonToTextField,
]];
}
}
- (void)setLeadingButtonEnabled:(BOOL)enabled {
_leadingButton.enabled = enabled;
}
- (void)setPlaceholderImage:(int)imageID {
[self.leadingButton setImage:[self placeholderImageWithId:imageID]
forState:UIControlStateNormal];
[self.leadingButton setTintColor:[self tintColorForLeftImageWithID:imageID]];
// TODO(crbug.com/774121): This should not be done like this; instead the
// responder status of the textfield should be broadcasted and observed
// by the mediator of location bar, that would then show/hide the
// leading button.
BOOL hidden = (!IsIPadIdiom() && [self.textField isFirstResponder]);
[self setLeadingButtonHidden:hidden];
}
- (void)fadeInLeadingButton {
self.leadingButton.alpha = 0;
// Instead of passing a delay into -fadeInView:, wait to call -fadeInView:.
// The CABasicAnimation's start and end positions are calculated immediately
// instead of after the animation's delay, but the omnibox's layer isn't set
// yet to its final state and as a result the start and end positions will not
// be correct.
dispatch_time_t delay = dispatch_time(
DISPATCH_TIME_NOW, ios::material::kDuration2 * NSEC_PER_SEC);
dispatch_after(delay, dispatch_get_main_queue(), ^(void) {
UIView* view = self.leadingButton;
LayoutOffset leadingOffset = kPositionAnimationLeadingOffset;
NSTimeInterval duration = ios::material::kDuration1;
NSTimeInterval delay = 0;
[CATransaction begin];
[CATransaction setDisableActions:YES];
[CATransaction setCompletionBlock:^{
[view.layer removeAnimationForKey:@"fadeIn"];
}];
view.alpha = 1.0;
// Animate the position of |view| |leadingOffset| pixels after |delay|.
CGRect shiftedFrame = CGRectLayoutOffset(view.frame, leadingOffset);
CAAnimation* shiftAnimation =
FrameAnimationMake(view.layer, shiftedFrame, view.frame);
shiftAnimation.duration = duration;
shiftAnimation.beginTime = delay;
shiftAnimation.timingFunction =
TimingFunction(ios::material::CurveEaseInOut);
// Animate the opacity of |view| to 1 after |delay|.
CAAnimation* fadeAnimation = OpacityAnimationMake(0.0, 1.0);
fadeAnimation.duration = duration;
fadeAnimation.beginTime = delay;
shiftAnimation.timingFunction =
TimingFunction(ios::material::CurveEaseInOut);
// Add group animation to layer.
CAAnimation* group = AnimationGroupMake(@[ shiftAnimation, fadeAnimation ]);
[view.layer addAnimation:group forKey:@"fadeIn"];
[CATransaction commit];
});
}
- (void)fadeOutLeadingButton {
[self setLeadingButtonHidden:NO];
UIView* leadingView = [self leadingButton];
// Move the leadingButton outside of the bounds; this constraint will be
// created from scratch when the button is shown.
self.leadingButtonLeadingConstraint.constant = leadingView.frame.size.width;
[UIView animateWithDuration:ios::material::kDuration2
delay:0
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
// Fade out the alpha and apply the constraint change above.
leadingView.alpha = 0;
[self setNeedsLayout];
[self layoutIfNeeded];
}
completion:^(BOOL finished) {
// Restore alpha and update the hidden state.
leadingView.alpha = 1;
[self setLeadingButtonHidden:YES];
}];
}
- (void)addExpandOmniboxAnimations:(UIViewPropertyAnimator*)animator
completionAnimator:(UIViewPropertyAnimator*)completionAnimator {
// TODO(crbug.com/791455): Due to crbug.com/774121 |self.leadingButton| is
// hidden in line 151 before the animation starts. For this reason any
// animation we try doing on |self.leadingButton| will not be visible.
[self.textField addExpandOmniboxAnimations:animator
completionAnimator:completionAnimator];
}
- (void)addContractOmniboxAnimations:(UIViewPropertyAnimator*)animator {
[self setLeadingButtonHidden:NO];
self.leadingButton.alpha = 0;
[animator addAnimations:^{
self.leadingButton.alpha = 1;
[self setLeadingButtonHidden:YES];
[self layoutIfNeeded];
}];
[self.textField addContractOmniboxAnimations:animator];
}
#pragma mark - Private methods
// Retrieves a resource image by ID and returns it as UIImage.
- (UIImage*)placeholderImageWithId:(int)imageID {
return [NativeImage(imageID)
imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
}
// Returns the tint color for the left image. This is necessary because the
// resource images are stored as templates, but in non-incognito the securtiy
// indicator needs to have a color depending on security status.
- (UIColor*)tintColorForLeftImageWithID:(int)imageID {
UIColor* tint = [UIColor whiteColor];
if (!self.incognito) {
switch (imageID) {
case IDR_IOS_LOCATION_BAR_HTTP:
tint = [UIColor darkGrayColor];
break;
case IDR_IOS_OMNIBOX_HTTPS_VALID:
tint = skia::UIColorFromSkColor(gfx::kGoogleGreen700);
break;
case IDR_IOS_OMNIBOX_HTTPS_POLICY_WARNING:
tint = skia::UIColorFromSkColor(gfx::kGoogleYellow700);
break;
case IDR_IOS_OMNIBOX_HTTPS_INVALID:
tint = skia::UIColorFromSkColor(gfx::kGoogleRed700);
break;
default:
tint = [UIColor darkGrayColor];
}
}
return tint;
}
#pragma mark - OmniboxLeftImageConsumer
- (void)setLeftImageId:(int)imageId {
[self setPlaceholderImage:imageId];
}
@end