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_header_view.h
" |
| 6 |
| 7 #import "base/ios/weak_nsobject.h" |
| 8 #include "base/logging.h" |
| 9 #include "base/mac/scoped_cftyperef.h" |
| 10 #include "base/mac/scoped_nsobject.h" |
| 11 #import "ios/chrome/browser/ui/contextual_search/contextual_search_panel_view.h" |
| 12 #import "ios/chrome/browser/ui/uikit_ui_util.h" |
| 13 #import "ios/chrome/common/material_timing.h" |
| 14 #include "ios/chrome/common/string_util.h" |
| 15 #include "ios/public/provider/chrome/browser/chrome_browser_provider.h" |
| 16 #include "ios/public/provider/chrome/browser/images/branded_image_provider.h" |
| 17 #import "ios/third_party/material_components_ios/src/components/Typography/src/M
aterialTypography.h" |
| 18 |
| 19 namespace { |
| 20 const CGFloat kHorizontalMargin = 24.0; |
| 21 const CGFloat kHorizontalLayoutGap = 16.0; |
| 22 |
| 23 const NSTimeInterval kTextTransformAnimationDuration = |
| 24 ios::material::kDuration1; |
| 25 const NSTimeInterval kLogoIrisAnimationDuration = ios::material::kDuration1; |
| 26 } // namespace |
| 27 |
| 28 // An image that can "iris" in/out. Assumes a square image and will do a stupid- |
| 29 // looking eliptical iris otherwise. |
| 30 @interface IrisingImageView : UIImageView |
| 31 // |iris| is the degree that the logo is irised; a value of 0.0 indicates |
| 32 // the logo is completly invisible, a 1.0 indicates it is completely visible, |
| 33 // and 0.5 indicates the iris is open to show a diameter half of the image size. |
| 34 // |iris| has an initial value of 1.0. |
| 35 // |iris| is animatable, in that setting in inside an animation block will |
| 36 // cause the transition to be animated. |
| 37 @property(nonatomic, assign) CGFloat iris; |
| 38 @end |
| 39 |
| 40 @implementation IrisingImageView { |
| 41 CGFloat _iris; |
| 42 } |
| 43 |
| 44 @synthesize iris = _iris; |
| 45 |
| 46 // Create a mask layer for the iris effect |
| 47 - (instancetype)initWithImage:(UIImage*)image { |
| 48 if ((self = [super initWithImage:image])) { |
| 49 CAShapeLayer* maskLayer = [CAShapeLayer layer]; |
| 50 maskLayer.bounds = self.bounds; |
| 51 base::ScopedCFTypeRef<CGPathRef> path( |
| 52 CGPathCreateWithEllipseInRect(maskLayer.bounds, NULL)); |
| 53 maskLayer.path = path; |
| 54 maskLayer.fillColor = [UIColor whiteColor].CGColor; |
| 55 maskLayer.position = |
| 56 CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); |
| 57 self.layer.mask = maskLayer; |
| 58 self.iris = 1.0; |
| 59 [self setContentHuggingPriority:UILayoutPriorityDefaultHigh |
| 60 forAxis:UILayoutConstraintAxisVertical]; |
| 61 [self setContentHuggingPriority:UILayoutPriorityDefaultHigh |
| 62 forAxis:UILayoutConstraintAxisHorizontal]; |
| 63 } |
| 64 return self; |
| 65 } |
| 66 |
| 67 - (void)setIris:(CGFloat)iris { |
| 68 _iris = iris; |
| 69 // Transform the (0.0 ... 1.0) iris value so that the area covered appears |
| 70 // to change linearly. A value of 0.5 should cover half the area of the image, |
| 71 // so the radius of the circle should be sqrt(0.5); so, use sqrt(_iris). |
| 72 // At a scale of 1.0, the iris should totally expose the image, so the iris |
| 73 // diameter must be enough to encompass a square the size of the image; for a |
| 74 // unit square, that's sqrt(2). |
| 75 CGFloat scale = sqrt(_iris) * sqrt(2.0); |
| 76 [self.layer.mask setAffineTransform:CGAffineTransformMakeScale(scale, scale)]; |
| 77 } |
| 78 |
| 79 @end |
| 80 |
| 81 // Button subclass whose intrinsic content size is always large enough to be |
| 82 // easily tappable. |
| 83 @interface TappableButton : UIButton |
| 84 @end |
| 85 |
| 86 @implementation TappableButton |
| 87 |
| 88 - (CGSize)intrinsicContentSize { |
| 89 CGSize contentSize = [super intrinsicContentSize]; |
| 90 contentSize.height = MAX(contentSize.height, 44.0); |
| 91 contentSize.width = MAX(contentSize.width, 44.0); |
| 92 return contentSize; |
| 93 } |
| 94 |
| 95 @end |
| 96 |
| 97 @implementation ContextualSearchHeaderView { |
| 98 CGFloat _height; |
| 99 // Circular logo positioned leading side. |
| 100 __unsafe_unretained IrisingImageView* _logo; |
| 101 // Up/down caret positioned trailing side. |
| 102 __unsafe_unretained UIImageView* _caret; |
| 103 // Close control position identically to the caret. |
| 104 __unsafe_unretained TappableButton* _closeButton; |
| 105 // Label showing the text the user tapped on in the web page, and any |
| 106 // additional context that will be displayed. |
| 107 __unsafe_unretained UILabel* _textLabel; |
| 108 base::WeakNSProtocol<id<ContextualSearchPanelTapHandler>> _tapHandler; |
| 109 base::scoped_nsobject<UIGestureRecognizer> _tapRecognizer; |
| 110 } |
| 111 |
| 112 + (BOOL)requiresConstraintBasedLayout { |
| 113 return YES; |
| 114 } |
| 115 |
| 116 - (instancetype)initWithHeight:(CGFloat)height { |
| 117 if (!(self = [super initWithFrame:CGRectZero])) |
| 118 return nil; |
| 119 |
| 120 DCHECK(height > 0); |
| 121 _height = height; |
| 122 |
| 123 self.translatesAutoresizingMaskIntoConstraints = NO; |
| 124 self.backgroundColor = [UIColor whiteColor]; |
| 125 _tapRecognizer.reset([[UITapGestureRecognizer alloc] init]); |
| 126 [self addGestureRecognizer:_tapRecognizer]; |
| 127 [_tapRecognizer addTarget:self action:@selector(panelWasTapped:)]; |
| 128 |
| 129 UIImage* logoImage = ios::GetChromeBrowserProvider() |
| 130 ->GetBrandedImageProvider() |
| 131 ->GetContextualSearchHeaderImage(); |
| 132 _logo = [[[IrisingImageView alloc] initWithImage:logoImage] autorelease]; |
| 133 _logo.translatesAutoresizingMaskIntoConstraints = NO; |
| 134 _logo.iris = 0.0; |
| 135 |
| 136 _caret = [[[UIImageView alloc] |
| 137 initWithImage:[UIImage imageNamed:@"expand_less"]] autorelease]; |
| 138 _caret.translatesAutoresizingMaskIntoConstraints = NO; |
| 139 [_caret setContentHuggingPriority:UILayoutPriorityDefaultHigh |
| 140 forAxis:UILayoutConstraintAxisVertical]; |
| 141 [_caret setContentHuggingPriority:UILayoutPriorityDefaultHigh |
| 142 forAxis:UILayoutConstraintAxisHorizontal]; |
| 143 |
| 144 _closeButton = |
| 145 [[[TappableButton alloc] initWithFrame:CGRectZero] autorelease]; |
| 146 _closeButton.translatesAutoresizingMaskIntoConstraints = NO; |
| 147 [_closeButton setImage:[UIImage imageNamed:@"card_close_button"] |
| 148 forState:UIControlStateNormal]; |
| 149 [_closeButton setImage:[UIImage imageNamed:@"card_close_button_pressed"] |
| 150 forState:UIControlStateHighlighted]; |
| 151 [_closeButton setContentHuggingPriority:UILayoutPriorityDefaultHigh |
| 152 forAxis:UILayoutConstraintAxisVertical]; |
| 153 [_closeButton setContentHuggingPriority:UILayoutPriorityDefaultHigh |
| 154 forAxis:UILayoutConstraintAxisHorizontal]; |
| 155 _closeButton.alpha = 0; |
| 156 |
| 157 _textLabel = [[[UILabel alloc] initWithFrame:CGRectZero] autorelease]; |
| 158 _textLabel.translatesAutoresizingMaskIntoConstraints = NO; |
| 159 _textLabel.font = [MDCTypography subheadFont]; |
| 160 _textLabel.textAlignment = NSTextAlignmentNatural; |
| 161 _textLabel.lineBreakMode = NSLineBreakByCharWrapping; |
| 162 // Ensure that |_textLabel| doesn't expand past the space defined for it |
| 163 // regardless of how long its text is. |
| 164 [_textLabel setContentHuggingPriority:UILayoutPriorityDefaultLow |
| 165 forAxis:UILayoutConstraintAxisHorizontal]; |
| 166 |
| 167 [self setAccessibilityIdentifier:@"header"]; |
| 168 [_logo setAccessibilityIdentifier:@"logo"]; |
| 169 [_caret setAccessibilityIdentifier:@"caret"]; |
| 170 [_closeButton setAccessibilityIdentifier:@"close"]; |
| 171 [_textLabel setAccessibilityIdentifier:@"selectedText"]; |
| 172 |
| 173 [self addSubview:_logo]; |
| 174 [self addSubview:_caret]; |
| 175 [self addSubview:_textLabel]; |
| 176 [self addSubview:_closeButton]; |
| 177 |
| 178 [self setLayoutMargins:UIEdgeInsetsMake(0, kHorizontalMargin, 0, |
| 179 kHorizontalMargin)]; |
| 180 |
| 181 [NSLayoutConstraint activateConstraints:@[ |
| 182 // Horizontal layout: |
| 183 // Logo is at the leading margin: |
| 184 [_logo.leadingAnchor |
| 185 constraintEqualToAnchor:self.layoutMarginsGuide.leadingAnchor], |
| 186 // Caret is at the trailing margin: |
| 187 [_caret.trailingAnchor |
| 188 constraintEqualToAnchor:self.layoutMarginsGuide.trailingAnchor], |
| 189 // Close button is centered over the caret: |
| 190 [_closeButton.centerXAnchor constraintEqualToAnchor:_caret.centerXAnchor], |
| 191 // The available space for the text label is the space (minus |
| 192 // |kHorizontalLayoutGap| on each side) between the logo and the caret: |
| 193 [_textLabel.leadingAnchor constraintEqualToAnchor:_logo.trailingAnchor |
| 194 constant:kHorizontalLayoutGap], |
| 195 [_textLabel.trailingAnchor |
| 196 constraintLessThanOrEqualToAnchor:_caret.leadingAnchor |
| 197 constant:-kHorizontalLayoutGap], |
| 198 // Vertical layout: |
| 199 // Everything is center-aligned to |self|. |
| 200 [_logo.centerYAnchor |
| 201 constraintEqualToAnchor:self.layoutMarginsGuide.centerYAnchor], |
| 202 [_textLabel.centerYAnchor |
| 203 constraintEqualToAnchor:self.layoutMarginsGuide.centerYAnchor], |
| 204 [_caret.centerYAnchor |
| 205 constraintEqualToAnchor:self.layoutMarginsGuide.centerYAnchor], |
| 206 [_closeButton.centerYAnchor constraintEqualToAnchor:_caret.centerYAnchor], |
| 207 ]]; |
| 208 |
| 209 return self; |
| 210 } |
| 211 |
| 212 - (instancetype)initWithCoder:(NSCoder*)aDecoder NS_UNAVAILABLE { |
| 213 NOTREACHED(); |
| 214 return nil; |
| 215 } |
| 216 |
| 217 - (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE { |
| 218 NOTREACHED(); |
| 219 return nil; |
| 220 } |
| 221 |
| 222 #pragma mark - property implementation. |
| 223 |
| 224 - (void)setTapHandler:(id<ContextualSearchPanelTapHandler>)tapHandler { |
| 225 if (_tapHandler) { |
| 226 [_closeButton removeTarget:_tapHandler |
| 227 action:@selector(closePanel) |
| 228 forControlEvents:UIControlEventTouchUpInside]; |
| 229 } |
| 230 _tapHandler.reset(tapHandler); |
| 231 if (_tapHandler) { |
| 232 [_closeButton addTarget:_tapHandler |
| 233 action:@selector(closePanel) |
| 234 forControlEvents:UIControlEventTouchUpInside]; |
| 235 } |
| 236 } |
| 237 |
| 238 - (id<ContextualSearchPanelTapHandler>)tapHandler { |
| 239 return _tapHandler; |
| 240 } |
| 241 |
| 242 - (void)panelWasTapped:(UIGestureRecognizer*)gestureRecognizer { |
| 243 for (NSUInteger touchIndex = 0; |
| 244 touchIndex < gestureRecognizer.numberOfTouches; touchIndex++) { |
| 245 if (!CGRectContainsPoint( |
| 246 self.frame, |
| 247 [gestureRecognizer locationOfTouch:touchIndex inView:self])) { |
| 248 return; |
| 249 } |
| 250 } |
| 251 [_tapHandler panelWasTapped:gestureRecognizer]; |
| 252 } |
| 253 |
| 254 #pragma mark - UIView layout methods |
| 255 |
| 256 - (CGSize)intrinsicContentSize { |
| 257 // This view's height is always |_height|. |
| 258 return CGSizeMake(UIViewNoIntrinsicMetric, _height); |
| 259 } |
| 260 |
| 261 #pragma mark - ContextualSearchPanelMotionObserver |
| 262 |
| 263 - (void)panel:(ContextualSearchPanelView*)panel |
| 264 didMoveWithMotion:(ContextualSearch::PanelMotion)motion { |
| 265 if (motion.state == ContextualSearch::PREVIEWING) { |
| 266 [self setCloseButtonTransition:1.0]; |
| 267 } |
| 268 if (motion.state == ContextualSearch::PEEKING) { |
| 269 _caret.alpha = 1.0; |
| 270 [self setCloseButtonTransition:motion.gradation]; |
| 271 } |
| 272 } |
| 273 |
| 274 - (void)panelWillPromote:(ContextualSearchPanelView*)panel { |
| 275 // Disable tap handling. |
| 276 self.tapHandler = nil; |
| 277 } |
| 278 |
| 279 - (void)panelIsPromoting:(ContextualSearchPanelView*)panel { |
| 280 self.alpha = 0.0; |
| 281 [panel removeMotionObserver:self]; |
| 282 } |
| 283 |
| 284 #pragma mark - Subview update |
| 285 |
| 286 - (void)setCloseButtonTransition:(CGFloat)gradation { |
| 287 // Crossfade the caret into the close button by fading the caret all the way |
| 288 // out, and then fading the close button in. |
| 289 // As the overall gradation moves from 0 to 0.5, the caret's alpha moves |
| 290 // from 1.0 to 0, and as the gradation continues from 0.5 to 1.0, the |
| 291 // close button's alpha moves from 0 to 1.0. |
| 292 CGFloat scaledGradation = 1 - (2 * gradation); // [0, 1] -> [1, -1] |
| 293 CGFloat caretGradation = MAX(scaledGradation, 0); // [1.0 .. 0.0 .. 0.0] |
| 294 CGFloat closeGradation = MAX(-scaledGradation, 0); // [0.0 .. 0.0 .. 1.0] |
| 295 _caret.alpha = caretGradation * caretGradation; |
| 296 _closeButton.alpha = closeGradation * closeGradation; |
| 297 } |
| 298 |
| 299 #pragma mark - Animated transitions |
| 300 |
| 301 - (void)setText:(NSString*)text |
| 302 followingTextRange:(NSRange)followingTextRange |
| 303 animated:(BOOL)animated { |
| 304 NSMutableAttributedString* styledText = |
| 305 [[[NSMutableAttributedString alloc] initWithString:text] autorelease]; |
| 306 [styledText addAttribute:NSForegroundColorAttributeName |
| 307 value:[UIColor colorWithWhite:0 alpha:0.71f] |
| 308 range:followingTextRange]; |
| 309 |
| 310 void (^transform)(void) = ^{ |
| 311 _textLabel.attributedText = styledText; |
| 312 }; |
| 313 void (^complete)(BOOL) = ^(BOOL finished) { |
| 314 [self showLogoAnimated:animated]; |
| 315 }; |
| 316 |
| 317 if (animated) { |
| 318 UIViewAnimationOptions options = |
| 319 UIViewAnimationOptionTransitionCrossDissolve; |
| 320 [UIView cr_transitionWithView:self |
| 321 duration:kTextTransformAnimationDuration |
| 322 curve:ios::material::CurveEaseOut |
| 323 options:options |
| 324 animations:transform |
| 325 completion:complete]; |
| 326 } else { |
| 327 transform(); |
| 328 complete(NO); |
| 329 } |
| 330 } |
| 331 |
| 332 - (void)setSearchTerm:(NSString*)searchTerm animated:(BOOL)animated { |
| 333 void (^transform)(void) = ^{ |
| 334 _textLabel.text = searchTerm; |
| 335 }; |
| 336 void (^complete)(BOOL) = ^(BOOL finished) { |
| 337 [self showLogoAnimated:animated]; |
| 338 }; |
| 339 |
| 340 if (animated) { |
| 341 UIViewAnimationOptions options = |
| 342 UIViewAnimationOptionTransitionCrossDissolve; |
| 343 [UIView cr_transitionWithView:self |
| 344 duration:kTextTransformAnimationDuration |
| 345 curve:ios::material::CurveEaseInOut |
| 346 options:options |
| 347 animations:transform |
| 348 completion:complete]; |
| 349 } else { |
| 350 transform(); |
| 351 complete(NO); |
| 352 } |
| 353 } |
| 354 |
| 355 - (void)showLogoAnimated:(BOOL)animated { |
| 356 // Since the logo is round, we only need to animate to 1/sqrt(2) to display |
| 357 // the whole thing. |
| 358 if ([_logo iris] > 0.0) |
| 359 return; |
| 360 |
| 361 void (^transform)(void) = ^{ |
| 362 [_logo setIris:(1.0 / sqrt(2.0))]; |
| 363 }; |
| 364 if (animated) { |
| 365 [UIView cr_animateWithDuration:kLogoIrisAnimationDuration |
| 366 delay:0 |
| 367 curve:ios::material::CurveEaseIn |
| 368 options:0 |
| 369 animations:transform |
| 370 completion:nil]; |
| 371 } else { |
| 372 transform(); |
| 373 } |
| 374 } |
| 375 |
| 376 - (void)hideLogoAnimated:(BOOL)animated { |
| 377 if ([_logo iris] == 0.0) |
| 378 return; |
| 379 |
| 380 void (^transform)(void) = ^{ |
| 381 [_logo setIris:0.0]; |
| 382 }; |
| 383 if (animated) { |
| 384 [UIView cr_animateWithDuration:kLogoIrisAnimationDuration |
| 385 delay:0 |
| 386 curve:ios::material::CurveEaseOut |
| 387 options:0 |
| 388 animations:transform |
| 389 completion:nil]; |
| 390 } else { |
| 391 transform(); |
| 392 } |
| 393 } |
| 394 |
| 395 @end |
OLD | NEW |