OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 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/overscroll_actions/overscroll_actions_view.h" |
| 6 |
| 7 #import <QuartzCore/QuartzCore.h> |
| 8 |
| 9 #include "base/logging.h" |
| 10 #include "base/mac/objc_property_releaser.h" |
| 11 #include "base/mac/scoped_nsobject.h" |
| 12 #include "ios/chrome/browser/ui/rtl_geometry.h" |
| 13 #include "ios/chrome/grit/ios_theme_resources.h" |
| 14 #include "ui/base/resource/resource_bundle.h" |
| 15 |
| 16 namespace { |
| 17 // Actions images. |
| 18 NSString* const kNewTabActionImage = @"ptr_new_tab"; |
| 19 NSString* const kNewTabActionActiveImage = @"ptr_new_tab_active"; |
| 20 NSString* const kRefreshActionImage = @"ptr_reload"; |
| 21 NSString* const kRefreshActionActiveImage = @"ptr_reload_active"; |
| 22 NSString* const kCloseActionImage = @"ptr_close"; |
| 23 NSString* const kCloseActionActiveImage = @"ptr_close_active"; |
| 24 |
| 25 // Represents a simple min/max range. |
| 26 typedef struct { |
| 27 CGFloat min; |
| 28 CGFloat max; |
| 29 } FloatRange; |
| 30 |
| 31 // The threshold at which the refresh actions will start to be visible. |
| 32 const CGFloat kRefreshThreshold = 48.0; |
| 33 // The threshold at which the actions are fully visible and can be selected. |
| 34 const CGFloat kFullThreshold = 56.0; |
| 35 // The size in point of the edges of the action selection circle layer. |
| 36 const CGFloat kSelectionEdge = 64.0; |
| 37 // Initial start position in X of the left and right actions from the center. |
| 38 // Left actions will start at center.x - kActionsStartPositionMarginFromCenter |
| 39 // Right actions will start at center.x + kActionsStartPositionMarginFromCenter |
| 40 const CGFloat kActionsStartPositionMarginFromCenter = 80.0; |
| 41 // Ranges mapping the width of the screen to the margin of the left and right |
| 42 // actions images from the frame center. |
| 43 const FloatRange kActionsPositionMarginsFrom = {320.0, 736.0}; |
| 44 const FloatRange kActionsPositionMarginsTo = {100.0, 200.0}; |
| 45 // Horizontal threshold before visual feedback starts. Threshold applied on |
| 46 // values in between [-1,1], where -1 corresponds to the leftmost action, and 1 |
| 47 // corresponds to the rightmost action. |
| 48 const CGFloat kDistanceWhereMovementIsIgnored = 0.1; |
| 49 // Start scale of the action selection circle when no actions are displayed. |
| 50 const CGFloat kSelectionInitialDownScale = 0.1; |
| 51 // Start scale of the action selection circle when actions are displayed but |
| 52 // no action is selected. |
| 53 const CGFloat kSelectionDownScale = 0.1875; |
| 54 // The duration of the animations played when the actions are ready to |
| 55 // be triggered. |
| 56 const CGFloat kDisplayActionAnimationDuration = 0.5; |
| 57 // The final scale of the animation played when an action is triggered. |
| 58 const CGFloat kDisplayActionAnimationScale = 20; |
| 59 // The height of the shadow view. |
| 60 const CGFloat kShadowHeight = 2; |
| 61 // This controls how much the selection needs to be moved from the action center |
| 62 // in order to be snapped to the next action. |
| 63 // This value must stay in the interval [0,1]. |
| 64 const CGFloat kSelectionSnappingOffsetFromCenter = 0.15; |
| 65 // Duration of the snapping animation moving the selection circle to the |
| 66 // selected action. |
| 67 const CGFloat kSelectionSnappingAnimationDuration = 0.2; |
| 68 // Controls how much the bezier shape's front and back are deformed. |
| 69 CGFloat KBezierPathFrontDeformation = 5.0; |
| 70 CGFloat KBezierPathBackDeformation = 2.5; |
| 71 // Controls the amount of points the bezier path is made of. |
| 72 int kBezierPathPointCount = 40; |
| 73 // Minimum delay to perform the transition to the ready state. |
| 74 const CFTimeInterval kMinimumPullDurationToTransitionToReadyInSeconds = 0.25; |
| 75 // Value in point to which the action icon frame will be expanded to detect user |
| 76 // direct touches. |
| 77 const CGFloat kDirectTouchFrameExpansion = 20; |
| 78 |
| 79 // This function maps a value from a range to another. |
| 80 CGFloat MapValueToRange(FloatRange from, FloatRange to, CGFloat value) { |
| 81 DCHECK(from.min < from.max); |
| 82 if (value <= from.min) |
| 83 return to.min; |
| 84 if (value >= from.max) |
| 85 return to.max; |
| 86 const CGFloat fromDst = from.max - from.min; |
| 87 const CGFloat toDst = to.max - to.min; |
| 88 return to.min + ((value - from.min) / fromDst) * toDst; |
| 89 } |
| 90 |
| 91 // Used to set the X position of a CALayer. |
| 92 void SetLayerPositionX(CALayer* layer, CGFloat value) { |
| 93 CGPoint position = layer.position; |
| 94 position.x = value; |
| 95 layer.position = position; |
| 96 } |
| 97 |
| 98 // Describes the internal state of the OverscrollActionsView. |
| 99 enum class OverscrollViewState { |
| 100 NONE, // Initial state. |
| 101 PREPARE, // The actions are starting to be displayed. |
| 102 READY // Actions are fully displayed. |
| 103 }; |
| 104 } // namespace |
| 105 |
| 106 @interface OverscrollActionsView ()<UIGestureRecognizerDelegate> { |
| 107 // True when the first layout has been done. |
| 108 BOOL _initialLayoutDone; |
| 109 // True when an action trigger animation is currently playing. |
| 110 BOOL _animatingActionTrigger; |
| 111 // Whether the selection circle is deformed. |
| 112 BOOL _deformationBehaviorEnabled; |
| 113 // Whether the view already made the transition to the READY state at least |
| 114 // once. |
| 115 BOOL _didTransitionToReadyState; |
| 116 // True if the view is directly touched. |
| 117 BOOL _viewTouched; |
| 118 // An additionnal offset added to the horizontalOffset value in order to take |
| 119 // into account snapping. |
| 120 CGFloat _snappingOffset; |
| 121 // The offset of the currently snapped action. |
| 122 CGFloat _snappedActionOffset; |
| 123 // The value of the horizontalOffset when a snap animation has been triggered. |
| 124 CGFloat _horizontalOffsetOnAnimationStart; |
| 125 // The last vertical offset. |
| 126 CGFloat _lastVerticalOffset; |
| 127 // Last recorded pull start absolute time. |
| 128 // Unit is in seconds. |
| 129 CFTimeInterval _pullStartTimeInSeconds; |
| 130 // Tap gesture recognizer that allow the user to tap on an action to activate |
| 131 // it. |
| 132 base::scoped_nsobject<UITapGestureRecognizer> _tapGesture; |
| 133 // Array of layers that will be centered vertically. |
| 134 // The array is built the first time the method -layersToCenterVertically is |
| 135 // called. |
| 136 base::scoped_nsobject<NSArray> _layersToCenterVertically; |
| 137 base::mac::ObjCPropertyReleaser _propertyReleaser_OverscrollActionsView; |
| 138 } |
| 139 |
| 140 // Redefined to readwrite. |
| 141 @property(nonatomic, assign, readwrite) |
| 142 ios_internal::OverscrollAction selectedAction; |
| 143 |
| 144 // Actions image views. |
| 145 @property(nonatomic, retain) UIImageView* addTabActionImageView; |
| 146 @property(nonatomic, retain) UIImageView* refreshActionImageView; |
| 147 @property(nonatomic, retain) UIImageView* closeTabActionImageView; |
| 148 |
| 149 @property(nonatomic, retain) CALayer* highlightMaskLayer; |
| 150 |
| 151 @property(nonatomic, retain) UIImageView* addTabActionImageViewHighlighted; |
| 152 @property(nonatomic, retain) UIImageView* refreshActionImageViewHighlighted; |
| 153 @property(nonatomic, retain) UIImageView* closeTabActionImageViewHighlighted; |
| 154 |
| 155 // The layer displaying the selection circle. |
| 156 @property(nonatomic, retain) CAShapeLayer* selectionCircleLayer; |
| 157 // Mask layer used to display highlighted states when the selection circle is |
| 158 // above them. |
| 159 @property(nonatomic, retain) CAShapeLayer* selectionCircleMaskLayer; |
| 160 |
| 161 // The current vertical offset. |
| 162 @property(nonatomic, assign) CGFloat verticalOffset; |
| 163 // The current horizontal offset. |
| 164 @property(nonatomic, assign) CGFloat horizontalOffset; |
| 165 // The internal state of the OverscrollActionsView. |
| 166 @property(nonatomic, assign) OverscrollViewState overscrollState; |
| 167 // A shadow image view displayed at the bottom. |
| 168 @property(nonatomic, retain) UIImageView* shadowView; |
| 169 // Redefined to readwrite. |
| 170 @property(nonatomic, retain, readwrite) UIView* backgroundView; |
| 171 // Snapshot view added on top of the background image view. |
| 172 @property(nonatomic, retain, readwrite) UIView* snapshotView; |
| 173 // The parent layer on the selection circle used for cropping purpose. |
| 174 @property(nonatomic, retain, readwrite) CALayer* selectionCircleCroppingLayer; |
| 175 |
| 176 // An absolute horizontal offset that also takes into account snapping. |
| 177 - (CGFloat)absoluteHorizontalOffset; |
| 178 // Computes the margin of the actions image views using the screen's width. |
| 179 - (CGFloat)actionsPositionMarginFromCenter; |
| 180 // Performs the layout of the actions image views. |
| 181 - (void)layoutActions; |
| 182 // Absorbs the horizontal movement around the actions in intervals defined with |
| 183 // kDistanceWhereMovementIsIgnored. |
| 184 - (CGFloat)absorbsHorizontalMovementAroundActions:(CGFloat)x; |
| 185 // Computes the position of the selection circle layer based on the horizontal |
| 186 // offset. |
| 187 - (CGPoint)selectionCirclePosition; |
| 188 // Performs layout of the selection circle layer. |
| 189 - (void)layoutSelectionCircle; |
| 190 // Updates the selected action depending on the current internal state and |
| 191 // and the horizontal offset. |
| 192 - (void)updateSelectedAction; |
| 193 // Called when the selected action changes in order to perform animations that |
| 194 // depend on the currently selected action. |
| 195 - (void)onSelectedActionChange; |
| 196 // Layout method used to center subviews vertically. |
| 197 - (void)centerSubviewsVertically; |
| 198 // Updates the current internal state of the OverscrollActionsView depending |
| 199 // on vertical offset. |
| 200 - (void)updateState; |
| 201 // Called when the state changes in order to perform state dependent animations. |
| 202 - (void)onStateChange; |
| 203 // Resets values related to selection state. |
| 204 - (void)resetSelection; |
| 205 // Returns a newly allocated and configured selection circle shape. |
| 206 - (CAShapeLayer*)newSelectionCircleLayer; |
| 207 // Returns an autoreleased circular bezier path horizontally deformed according |
| 208 // to |dx|. |
| 209 - (UIBezierPath*)circlePath:(CGFloat)dx; |
| 210 // Returns the action at the given location in the view. |
| 211 - (ios_internal::OverscrollAction)actionAtLocation:(CGPoint)location; |
| 212 // Update the selection circle frame to select the given action. |
| 213 - (void)updateSelectionForTouchedAction:(ios_internal::OverscrollAction)action; |
| 214 // Clear the direct touch interaction after a small delay to prevent graphic |
| 215 // glitch with pan gesture selection deformation animations. |
| 216 - (void)clearDirectTouchInteraction; |
| 217 @end |
| 218 |
| 219 @implementation OverscrollActionsView |
| 220 |
| 221 @synthesize selectedAction = _selectedAction; |
| 222 @synthesize addTabActionImageView = _addTabActionImageView; |
| 223 @synthesize refreshActionImageView = _refreshActionImageView; |
| 224 @synthesize closeTabActionImageView = _closeTabActionImageView; |
| 225 @synthesize addTabActionImageViewHighlighted = |
| 226 _addTabActionImageViewHighlighted; |
| 227 @synthesize refreshActionImageViewHighlighted = |
| 228 _refreshActionImageViewHighlighted; |
| 229 @synthesize closeTabActionImageViewHighlighted = |
| 230 _closeTabActionImageViewHighlighted; |
| 231 @synthesize highlightMaskLayer = _highlightMaskLayer; |
| 232 @synthesize selectionCircleLayer = _selectionCircleLayer; |
| 233 @synthesize selectionCircleMaskLayer = _selectionCircleMaskLayer; |
| 234 @synthesize verticalOffset = _verticalOffset; |
| 235 @synthesize horizontalOffset = _horizontalOffset; |
| 236 @synthesize overscrollState = _overscrollState; |
| 237 @synthesize shadowView = _shadowView; |
| 238 @synthesize backgroundView = _backgroundView; |
| 239 @synthesize snapshotView = _snapshotView; |
| 240 @synthesize selectionCircleCroppingLayer = _selectionCircleCroppingLayer; |
| 241 @synthesize delegate = _delegate; |
| 242 |
| 243 - (instancetype)initWithFrame:(CGRect)frame { |
| 244 self = [super initWithFrame:frame]; |
| 245 if (self) { |
| 246 _propertyReleaser_OverscrollActionsView.Init(self, |
| 247 [OverscrollActionsView class]); |
| 248 _deformationBehaviorEnabled = YES; |
| 249 self.autoresizingMask = |
| 250 UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; |
| 251 _selectionCircleLayer = [self newSelectionCircleLayer]; |
| 252 _selectionCircleMaskLayer = [self newSelectionCircleLayer]; |
| 253 _selectionCircleMaskLayer.contentsGravity = kCAGravityCenter; |
| 254 _selectionCircleCroppingLayer = [[CALayer alloc] init]; |
| 255 _selectionCircleCroppingLayer.frame = self.bounds; |
| 256 [_selectionCircleCroppingLayer setMasksToBounds:YES]; |
| 257 |
| 258 [self.layer addSublayer:_selectionCircleCroppingLayer]; |
| 259 [_selectionCircleCroppingLayer addSublayer:_selectionCircleLayer]; |
| 260 |
| 261 _addTabActionImageView = [[UIImageView alloc] init]; |
| 262 [self addSubview:_addTabActionImageView]; |
| 263 _refreshActionImageView = [[UIImageView alloc] init]; |
| 264 if (UseRTLLayout()) |
| 265 [_refreshActionImageView setTransform:CGAffineTransformMakeScale(-1, 1)]; |
| 266 [self addSubview:_refreshActionImageView]; |
| 267 _closeTabActionImageView = [[UIImageView alloc] init]; |
| 268 [self addSubview:_closeTabActionImageView]; |
| 269 |
| 270 _highlightMaskLayer = [[CALayer alloc] init]; |
| 271 _highlightMaskLayer.frame = self.bounds; |
| 272 _highlightMaskLayer.contentsGravity = kCAGravityCenter; |
| 273 _highlightMaskLayer.backgroundColor = [[UIColor clearColor] CGColor]; |
| 274 [_highlightMaskLayer setMask:_selectionCircleMaskLayer]; |
| 275 [self.layer addSublayer:_highlightMaskLayer]; |
| 276 |
| 277 _addTabActionImageViewHighlighted = [[UIImageView alloc] init]; |
| 278 _refreshActionImageViewHighlighted = [[UIImageView alloc] init]; |
| 279 if (UseRTLLayout()) { |
| 280 [_refreshActionImageViewHighlighted |
| 281 setTransform:CGAffineTransformMakeScale(-1, 1)]; |
| 282 } |
| 283 _closeTabActionImageViewHighlighted = [[UIImageView alloc] init]; |
| 284 [_highlightMaskLayer addSublayer:_addTabActionImageViewHighlighted.layer]; |
| 285 [_highlightMaskLayer addSublayer:_refreshActionImageViewHighlighted.layer]; |
| 286 [_highlightMaskLayer addSublayer:_closeTabActionImageViewHighlighted.layer]; |
| 287 |
| 288 _shadowView = [[UIImageView alloc] initWithFrame:CGRectZero]; |
| 289 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); |
| 290 gfx::Image shadow = rb.GetNativeImageNamed(IDR_IOS_TOOLBAR_SHADOW); |
| 291 [_shadowView setImage:shadow.ToUIImage()]; |
| 292 [self addSubview:_shadowView]; |
| 293 |
| 294 _backgroundView = [[UIView alloc] initWithFrame:CGRectZero]; |
| 295 [self addSubview:_backgroundView]; |
| 296 |
| 297 if (UseRTLLayout()) |
| 298 [self setTransform:CGAffineTransformMakeScale(-1, 1)]; |
| 299 |
| 300 _tapGesture.reset([[UITapGestureRecognizer alloc] |
| 301 initWithTarget:self |
| 302 action:@selector(tapGesture:)]); |
| 303 [_tapGesture setDelegate:self]; |
| 304 [self addGestureRecognizer:_tapGesture]; |
| 305 } |
| 306 return self; |
| 307 } |
| 308 |
| 309 - (void)dealloc { |
| 310 [self.snapshotView removeFromSuperview]; |
| 311 [super dealloc]; |
| 312 } |
| 313 |
| 314 - (BOOL)selectionCroppingEnabled { |
| 315 return [_selectionCircleCroppingLayer masksToBounds]; |
| 316 } |
| 317 |
| 318 - (void)setSelectionCroppingEnabled:(BOOL)enableSelectionCropping { |
| 319 [_selectionCircleCroppingLayer setMasksToBounds:enableSelectionCropping]; |
| 320 } |
| 321 |
| 322 - (void)addSnapshotView:(UIView*)view { |
| 323 if (UseRTLLayout()) { |
| 324 [CATransaction begin]; |
| 325 [CATransaction setDisableActions:YES]; |
| 326 [view setTransform:CGAffineTransformMakeScale(-1, 1)]; |
| 327 [CATransaction commit]; |
| 328 } |
| 329 [self.snapshotView removeFromSuperview]; |
| 330 self.snapshotView = view; |
| 331 [self.backgroundView addSubview:self.snapshotView]; |
| 332 } |
| 333 |
| 334 - (void)pullStarted { |
| 335 _didTransitionToReadyState = NO; |
| 336 _pullStartTimeInSeconds = CACurrentMediaTime(); |
| 337 // Ensure we will update the state after time threshold even without offset |
| 338 // change. |
| 339 dispatch_after( |
| 340 dispatch_time(DISPATCH_TIME_NOW, |
| 341 (kMinimumPullDurationToTransitionToReadyInSeconds + 0.01) * |
| 342 NSEC_PER_SEC), |
| 343 dispatch_get_main_queue(), ^{ |
| 344 [self updateState]; |
| 345 }); |
| 346 } |
| 347 |
| 348 - (void)updateWithVerticalOffset:(CGFloat)offset { |
| 349 _lastVerticalOffset = self.verticalOffset; |
| 350 self.verticalOffset = offset; |
| 351 [self updateState]; |
| 352 } |
| 353 |
| 354 - (void)updateWithHorizontalOffset:(CGFloat)offset { |
| 355 if (_animatingActionTrigger || _viewTouched) |
| 356 return; |
| 357 self.horizontalOffset = offset; |
| 358 // Absorb out of range offset values so that the user doesn't need to |
| 359 // compensate in order to move the cursor in the other direction. |
| 360 if ([self absoluteHorizontalOffset] < -1) |
| 361 _snappingOffset = -self.horizontalOffset - 1; |
| 362 if ([self absoluteHorizontalOffset] > 1) |
| 363 _snappingOffset = 1 - self.horizontalOffset; |
| 364 [self setNeedsLayout]; |
| 365 } |
| 366 |
| 367 - (void)displayActionAnimation { |
| 368 _animatingActionTrigger = YES; |
| 369 [CATransaction begin]; |
| 370 [CATransaction setCompletionBlock:^{ |
| 371 _animatingActionTrigger = NO; |
| 372 [CATransaction begin]; |
| 373 [CATransaction setDisableActions:YES]; |
| 374 // See comment below for why we manually set opacity to 0 and remove |
| 375 // the animation. |
| 376 self.selectionCircleLayer.opacity = 0; |
| 377 [self.selectionCircleLayer removeAnimationForKey:@"opacity"]; |
| 378 [self onStateChange]; |
| 379 [CATransaction commit]; |
| 380 }]; |
| 381 |
| 382 CABasicAnimation* scaleAnimation = |
| 383 [CABasicAnimation animationWithKeyPath:@"transform"]; |
| 384 scaleAnimation.fromValue = |
| 385 [NSValue valueWithCATransform3D:CATransform3DIdentity]; |
| 386 scaleAnimation.toValue = |
| 387 [NSValue valueWithCATransform3D:CATransform3DMakeScale( |
| 388 kDisplayActionAnimationScale, |
| 389 kDisplayActionAnimationScale, 1)]; |
| 390 scaleAnimation.duration = kDisplayActionAnimationDuration; |
| 391 [self.selectionCircleLayer addAnimation:scaleAnimation forKey:@"transform"]; |
| 392 |
| 393 CABasicAnimation* opacityAnimation = |
| 394 [CABasicAnimation animationWithKeyPath:@"opacity"]; |
| 395 opacityAnimation.fromValue = @(1); |
| 396 opacityAnimation.toValue = @(0); |
| 397 opacityAnimation.duration = kDisplayActionAnimationDuration; |
| 398 // A fillMode forward and manual removal of the animation is needed because |
| 399 // the completion handler can be called one frame earlier for the first |
| 400 // animation (transform) causing the opacity animation to be removed and show |
| 401 // an opacity of 1 for one or two frames. |
| 402 opacityAnimation.fillMode = kCAFillModeForwards; |
| 403 opacityAnimation.removedOnCompletion = NO; |
| 404 [self.selectionCircleLayer addAnimation:opacityAnimation forKey:@"opacity"]; |
| 405 |
| 406 [CATransaction commit]; |
| 407 } |
| 408 |
| 409 - (void)layoutSubviews { |
| 410 [super layoutSubviews]; |
| 411 |
| 412 [CATransaction begin]; |
| 413 [CATransaction setDisableActions:YES]; |
| 414 if (self.snapshotView) |
| 415 self.backgroundView.frame = self.snapshotView.bounds; |
| 416 _selectionCircleCroppingLayer.frame = self.bounds; |
| 417 _highlightMaskLayer.frame = self.bounds; |
| 418 |
| 419 CGRect shadowFrame = self.bounds; |
| 420 shadowFrame.origin.y = self.bounds.size.height; |
| 421 shadowFrame.size.height = kShadowHeight; |
| 422 self.shadowView.frame = shadowFrame; |
| 423 [CATransaction commit]; |
| 424 |
| 425 const BOOL disableActionsOnInitialLayout = |
| 426 !CGRectEqualToRect(CGRectZero, self.frame) && !_initialLayoutDone; |
| 427 if (disableActionsOnInitialLayout) { |
| 428 [CATransaction begin]; |
| 429 [CATransaction setDisableActions:YES]; |
| 430 _initialLayoutDone = YES; |
| 431 } |
| 432 [self centerSubviewsVertically]; |
| 433 [self layoutActions]; |
| 434 if (_deformationBehaviorEnabled) |
| 435 [self layoutSelectionCircleWithDeformation]; |
| 436 else |
| 437 [self layoutSelectionCircle]; |
| 438 [self updateSelectedAction]; |
| 439 if (disableActionsOnInitialLayout) |
| 440 [CATransaction commit]; |
| 441 } |
| 442 |
| 443 #pragma mark - Private |
| 444 |
| 445 - (CGFloat)absoluteHorizontalOffset { |
| 446 return self.horizontalOffset + _snappingOffset; |
| 447 } |
| 448 |
| 449 - (CGFloat)actionsPositionMarginFromCenter { |
| 450 return MapValueToRange(kActionsPositionMarginsFrom, kActionsPositionMarginsTo, |
| 451 self.bounds.size.width); |
| 452 } |
| 453 |
| 454 - (void)layoutActions { |
| 455 const CGFloat width = self.bounds.size.width; |
| 456 const CGFloat centerX = width / 2.0; |
| 457 const CGFloat actionsPositionMargin = [self actionsPositionMarginFromCenter]; |
| 458 |
| 459 [UIView beginAnimations:@"position" context:NULL]; |
| 460 [UIView setAnimationDuration:0.1]; |
| 461 SetLayerPositionX(self.refreshActionImageView.layer, centerX); |
| 462 SetLayerPositionX(self.refreshActionImageViewHighlighted.layer, centerX); |
| 463 |
| 464 const CGFloat addTabPositionX = |
| 465 MapValueToRange({kRefreshThreshold, kFullThreshold}, |
| 466 {centerX - kActionsStartPositionMarginFromCenter, |
| 467 centerX - actionsPositionMargin}, |
| 468 self.verticalOffset); |
| 469 SetLayerPositionX(self.addTabActionImageView.layer, addTabPositionX); |
| 470 SetLayerPositionX(self.addTabActionImageViewHighlighted.layer, |
| 471 addTabPositionX); |
| 472 |
| 473 const CGFloat closeTabPositionX = |
| 474 MapValueToRange({kRefreshThreshold, kFullThreshold}, |
| 475 {centerX + kActionsStartPositionMarginFromCenter, |
| 476 centerX + actionsPositionMargin}, |
| 477 self.verticalOffset); |
| 478 SetLayerPositionX(self.closeTabActionImageView.layer, closeTabPositionX); |
| 479 SetLayerPositionX(self.closeTabActionImageViewHighlighted.layer, |
| 480 closeTabPositionX); |
| 481 |
| 482 [UIView commitAnimations]; |
| 483 |
| 484 [UIView beginAnimations:@"opacity" context:NULL]; |
| 485 [UIView setAnimationDuration:0.1]; |
| 486 self.refreshActionImageView.layer.opacity = MapValueToRange( |
| 487 {kFullThreshold / 2.0, kFullThreshold}, {0, 1}, self.verticalOffset); |
| 488 self.refreshActionImageViewHighlighted.layer.opacity = |
| 489 self.refreshActionImageView.layer.opacity; |
| 490 self.addTabActionImageView.layer.opacity = MapValueToRange( |
| 491 {kRefreshThreshold, kFullThreshold}, {0, 1}, self.verticalOffset); |
| 492 self.addTabActionImageViewHighlighted.layer.opacity = |
| 493 self.addTabActionImageView.layer.opacity; |
| 494 self.closeTabActionImageView.layer.opacity = MapValueToRange( |
| 495 {kRefreshThreshold, kFullThreshold}, {0, 1}, self.verticalOffset); |
| 496 self.closeTabActionImageViewHighlighted.layer.opacity = |
| 497 self.closeTabActionImageView.layer.opacity; |
| 498 [UIView commitAnimations]; |
| 499 |
| 500 [UIView beginAnimations:@"transform" context:NULL]; |
| 501 [UIView setAnimationDuration:0.1]; |
| 502 CATransform3D rotation = CATransform3DMakeRotation( |
| 503 MapValueToRange({kFullThreshold / 2.0, kFullThreshold}, {-M_PI_2, M_PI_4}, |
| 504 self.verticalOffset), |
| 505 0, 0, 1); |
| 506 self.refreshActionImageView.layer.transform = rotation; |
| 507 self.refreshActionImageViewHighlighted.layer.transform = rotation; |
| 508 [UIView commitAnimations]; |
| 509 } |
| 510 |
| 511 - (CGFloat)absorbsHorizontalMovementAroundActions:(CGFloat)x { |
| 512 // The limits of the intervals where x is constant. |
| 513 const CGFloat kLeftActionAbsorptionLimit = |
| 514 -1 + kDistanceWhereMovementIsIgnored; |
| 515 const CGFloat kCenterActionLeftAbsorptionLimit = |
| 516 -kDistanceWhereMovementIsIgnored; |
| 517 const CGFloat kCenterActionRightAbsorptionLimit = |
| 518 kDistanceWhereMovementIsIgnored; |
| 519 const CGFloat kRightActionAbsorptionLimit = |
| 520 1 - kDistanceWhereMovementIsIgnored; |
| 521 if (x < kLeftActionAbsorptionLimit) { |
| 522 return -1; |
| 523 } |
| 524 if (x < kCenterActionLeftAbsorptionLimit) { |
| 525 return MapValueToRange( |
| 526 {kLeftActionAbsorptionLimit, kCenterActionLeftAbsorptionLimit}, {-1, 0}, |
| 527 x); |
| 528 } |
| 529 if (x < kCenterActionRightAbsorptionLimit) { |
| 530 return 0; |
| 531 } |
| 532 if (x < kRightActionAbsorptionLimit) { |
| 533 return MapValueToRange( |
| 534 {kCenterActionRightAbsorptionLimit, kRightActionAbsorptionLimit}, |
| 535 {0, 1}, x); |
| 536 } |
| 537 return 1; |
| 538 } |
| 539 |
| 540 - (CGPoint)selectionCirclePosition { |
| 541 const CGFloat centerX = self.bounds.size.width / 2.0; |
| 542 const CGFloat actionsPositionMargin = [self actionsPositionMarginFromCenter]; |
| 543 const CGFloat transformedOffset = [self |
| 544 absorbsHorizontalMovementAroundActions:[self absoluteHorizontalOffset]]; |
| 545 return CGPointMake(MapValueToRange({-1, 1}, {centerX - actionsPositionMargin, |
| 546 centerX + actionsPositionMargin}, |
| 547 transformedOffset), |
| 548 self.bounds.size.height / 2.0); |
| 549 } |
| 550 |
| 551 - (void)layoutSelectionCircle { |
| 552 if (self.overscrollState == OverscrollViewState::READY) { |
| 553 [CATransaction begin]; |
| 554 [CATransaction setDisableActions:YES]; |
| 555 self.selectionCircleLayer.position = [self selectionCirclePosition]; |
| 556 self.selectionCircleMaskLayer.position = self.selectionCircleLayer.position; |
| 557 [CATransaction commit]; |
| 558 } |
| 559 } |
| 560 |
| 561 - (void)layoutSelectionCircleWithDeformation { |
| 562 if (self.overscrollState == OverscrollViewState::READY) { |
| 563 BOOL animate = NO; |
| 564 CGFloat snapDistance = |
| 565 [self absoluteHorizontalOffset] - _snappedActionOffset; |
| 566 // Cancel out deformation for small movements. |
| 567 if (fabs(snapDistance) < kDistanceWhereMovementIsIgnored) { |
| 568 snapDistance = 0; |
| 569 } else { |
| 570 snapDistance -= snapDistance > 0 ? kDistanceWhereMovementIsIgnored |
| 571 : -kDistanceWhereMovementIsIgnored; |
| 572 } |
| 573 |
| 574 [self.selectionCircleLayer removeAnimationForKey:@"path"]; |
| 575 [self.selectionCircleMaskLayer removeAnimationForKey:@"path"]; |
| 576 self.selectionCircleLayer.path = [self circlePath:snapDistance].CGPath; |
| 577 self.selectionCircleMaskLayer.path = self.selectionCircleLayer.path; |
| 578 |
| 579 if (fabs(snapDistance) > kSelectionSnappingOffsetFromCenter) { |
| 580 animate = YES; |
| 581 _snappedActionOffset += (snapDistance < 0 ? -1 : 1); |
| 582 _snappingOffset = _snappedActionOffset - self.horizontalOffset; |
| 583 _horizontalOffsetOnAnimationStart = self.horizontalOffset; |
| 584 const CGFloat finalSnapDistance = |
| 585 [self absoluteHorizontalOffset] - _snappedActionOffset; |
| 586 |
| 587 UIBezierPath* finalPath = [self circlePath:finalSnapDistance]; |
| 588 [CATransaction begin]; |
| 589 [CATransaction setCompletionBlock:^{ |
| 590 self.selectionCircleLayer.path = finalPath.CGPath; |
| 591 [self.selectionCircleLayer removeAnimationForKey:@"path"]; |
| 592 self.selectionCircleMaskLayer.path = finalPath.CGPath; |
| 593 [self.selectionCircleMaskLayer removeAnimationForKey:@"path"]; |
| 594 }]; |
| 595 CABasicAnimation* (^pathAnimation)(void) = ^{ |
| 596 CABasicAnimation* pathAnim = |
| 597 [CABasicAnimation animationWithKeyPath:@"path"]; |
| 598 pathAnim.removedOnCompletion = NO; |
| 599 pathAnim.fillMode = kCAFillModeForwards; |
| 600 pathAnim.duration = kSelectionSnappingAnimationDuration; |
| 601 pathAnim.toValue = (__bridge id)finalPath.CGPath; |
| 602 return pathAnim; |
| 603 }; |
| 604 [self.selectionCircleLayer addAnimation:pathAnimation() forKey:@"path"]; |
| 605 [self.selectionCircleMaskLayer addAnimation:pathAnimation() |
| 606 forKey:@"path"]; |
| 607 [CATransaction commit]; |
| 608 } |
| 609 [CATransaction begin]; |
| 610 if (!animate) |
| 611 [CATransaction setDisableActions:YES]; |
| 612 else |
| 613 [CATransaction setAnimationDuration:kSelectionSnappingAnimationDuration]; |
| 614 self.selectionCircleLayer.position = [self selectionCirclePosition]; |
| 615 self.selectionCircleMaskLayer.position = self.selectionCircleLayer.position; |
| 616 [CATransaction commit]; |
| 617 } |
| 618 } |
| 619 |
| 620 - (void)updateSelectedAction { |
| 621 if (self.overscrollState != OverscrollViewState::READY) { |
| 622 self.selectedAction = ios_internal::OverscrollAction::NONE; |
| 623 return; |
| 624 } |
| 625 |
| 626 // Update action index by checking that the action image layer is included |
| 627 // inside the selection layer. |
| 628 const CGPoint selectionPosition = [self selectionCirclePosition]; |
| 629 if (_deformationBehaviorEnabled) { |
| 630 const CGFloat distanceBetweenTwoActions = |
| 631 (self.refreshActionImageView.frame.origin.x - |
| 632 self.addTabActionImageView.frame.origin.x) / |
| 633 2; |
| 634 if (fabs(self.addTabActionImageView.center.x - selectionPosition.x) < |
| 635 distanceBetweenTwoActions) { |
| 636 self.selectedAction = ios_internal::OverscrollAction::NEW_TAB; |
| 637 } |
| 638 if (fabs(self.refreshActionImageView.center.x - selectionPosition.x) < |
| 639 distanceBetweenTwoActions) { |
| 640 self.selectedAction = ios_internal::OverscrollAction::REFRESH; |
| 641 } |
| 642 if (fabs(self.closeTabActionImageView.center.x - selectionPosition.x) < |
| 643 distanceBetweenTwoActions) { |
| 644 self.selectedAction = ios_internal::OverscrollAction::CLOSE_TAB; |
| 645 } |
| 646 } else { |
| 647 const CGRect selectionRect = |
| 648 CGRectMake(selectionPosition.x - kSelectionEdge / 2.0, |
| 649 selectionPosition.y - kSelectionEdge / 2.0, kSelectionEdge, |
| 650 kSelectionEdge); |
| 651 const CGRect addTabRect = self.addTabActionImageView.frame; |
| 652 const CGRect closeTabRect = self.closeTabActionImageView.frame; |
| 653 const CGRect refreshRect = self.refreshActionImageView.frame; |
| 654 |
| 655 if (CGRectContainsRect(selectionRect, addTabRect)) { |
| 656 self.selectedAction = ios_internal::OverscrollAction::NEW_TAB; |
| 657 } else if (CGRectContainsRect(selectionRect, refreshRect)) { |
| 658 self.selectedAction = ios_internal::OverscrollAction::REFRESH; |
| 659 } else if (CGRectContainsRect(selectionRect, closeTabRect)) { |
| 660 self.selectedAction = ios_internal::OverscrollAction::CLOSE_TAB; |
| 661 } else { |
| 662 self.selectedAction = ios_internal::OverscrollAction::NONE; |
| 663 } |
| 664 } |
| 665 } |
| 666 |
| 667 - (void)setSelectedAction:(ios_internal::OverscrollAction)action { |
| 668 if (_selectedAction != action) { |
| 669 _selectedAction = action; |
| 670 [self onSelectedActionChange]; |
| 671 } |
| 672 } |
| 673 |
| 674 - (void)onSelectedActionChange { |
| 675 if (self.overscrollState == OverscrollViewState::PREPARE || |
| 676 _animatingActionTrigger) |
| 677 return; |
| 678 |
| 679 [UIView beginAnimations:@"transform" context:NULL]; |
| 680 [UIView setAnimationDuration:kSelectionSnappingAnimationDuration]; |
| 681 if (self.selectedAction == ios_internal::OverscrollAction::NONE) { |
| 682 if (!_deformationBehaviorEnabled) { |
| 683 // Scale selection down. |
| 684 self.selectionCircleLayer.transform = |
| 685 CATransform3DMakeScale(kSelectionDownScale, kSelectionDownScale, 1); |
| 686 self.selectionCircleMaskLayer.transform = |
| 687 self.selectionCircleLayer.transform; |
| 688 } |
| 689 } else { |
| 690 // Scale selection up. |
| 691 self.selectionCircleLayer.transform = CATransform3DMakeScale(1, 1, 1); |
| 692 self.selectionCircleMaskLayer.transform = |
| 693 self.selectionCircleLayer.transform; |
| 694 } |
| 695 [UIView commitAnimations]; |
| 696 } |
| 697 |
| 698 - (base::scoped_nsobject<NSArray>&)layersToCenterVertically { |
| 699 if (!_layersToCenterVertically) { |
| 700 NSArray* layersToCenterVertically = @[ |
| 701 _selectionCircleLayer, _selectionCircleMaskLayer, |
| 702 _addTabActionImageView.layer, _refreshActionImageView.layer, |
| 703 _closeTabActionImageView.layer, _addTabActionImageViewHighlighted.layer, |
| 704 _refreshActionImageViewHighlighted.layer, |
| 705 _closeTabActionImageViewHighlighted.layer, _backgroundView.layer |
| 706 ]; |
| 707 _layersToCenterVertically.reset([layersToCenterVertically retain]); |
| 708 } |
| 709 return _layersToCenterVertically; |
| 710 } |
| 711 |
| 712 - (void)centerSubviewsVertically { |
| 713 [CATransaction begin]; |
| 714 [CATransaction setDisableActions:YES]; |
| 715 for (CALayer* layer in [self layersToCenterVertically].get()) { |
| 716 CGPoint position = layer.position; |
| 717 position.y = self.bounds.size.height / 2; |
| 718 layer.position = position; |
| 719 } |
| 720 [CATransaction commit]; |
| 721 } |
| 722 |
| 723 - (void)updateState { |
| 724 if (self.verticalOffset > 1) { |
| 725 const CFTimeInterval elapsedTime = |
| 726 CACurrentMediaTime() - _pullStartTimeInSeconds; |
| 727 const BOOL isMinimumTimeElapsed = |
| 728 elapsedTime >= kMinimumPullDurationToTransitionToReadyInSeconds; |
| 729 const BOOL isPullingDownOrAlreadyTriggeredOnce = |
| 730 _lastVerticalOffset <= self.verticalOffset || |
| 731 _didTransitionToReadyState; |
| 732 const BOOL isVerticalThresholdSatisfied = |
| 733 self.verticalOffset >= kFullThreshold; |
| 734 if (isPullingDownOrAlreadyTriggeredOnce && isVerticalThresholdSatisfied && |
| 735 isMinimumTimeElapsed) { |
| 736 self.overscrollState = OverscrollViewState::READY; |
| 737 } else { |
| 738 self.overscrollState = OverscrollViewState::PREPARE; |
| 739 } |
| 740 } else { |
| 741 self.overscrollState = OverscrollViewState::NONE; |
| 742 } |
| 743 [self setNeedsLayout]; |
| 744 } |
| 745 |
| 746 - (void)setOverscrollState:(OverscrollViewState)state { |
| 747 if (_overscrollState != state) { |
| 748 _overscrollState = state; |
| 749 [self onStateChange]; |
| 750 } |
| 751 } |
| 752 |
| 753 - (void)onStateChange { |
| 754 if (_animatingActionTrigger) |
| 755 return; |
| 756 |
| 757 if (self.overscrollState != OverscrollViewState::NONE) { |
| 758 [UIView beginAnimations:@"opacity" context:NULL]; |
| 759 [UIView setAnimationDuration:kSelectionSnappingAnimationDuration]; |
| 760 self.selectionCircleLayer.opacity = |
| 761 self.overscrollState == OverscrollViewState::READY ? 1.0 : 0.0; |
| 762 self.selectionCircleMaskLayer.opacity = self.selectionCircleLayer.opacity; |
| 763 [UIView commitAnimations]; |
| 764 if (self.overscrollState == OverscrollViewState::PREPARE) { |
| 765 [UIView beginAnimations:@"transform" context:NULL]; |
| 766 [UIView setAnimationDuration:kSelectionSnappingAnimationDuration]; |
| 767 [self resetSelection]; |
| 768 [UIView commitAnimations]; |
| 769 } else { |
| 770 _didTransitionToReadyState = YES; |
| 771 } |
| 772 } else { |
| 773 [CATransaction begin]; |
| 774 [CATransaction setDisableActions:YES]; |
| 775 [self resetSelection]; |
| 776 [CATransaction commit]; |
| 777 } |
| 778 } |
| 779 |
| 780 - (void)resetSelection { |
| 781 _didTransitionToReadyState = NO; |
| 782 _snappingOffset = 0; |
| 783 _snappedActionOffset = 0; |
| 784 _horizontalOffsetOnAnimationStart = 0; |
| 785 self.selectionCircleLayer.transform = CATransform3DMakeScale( |
| 786 kSelectionInitialDownScale, kSelectionInitialDownScale, 1); |
| 787 self.selectionCircleMaskLayer.transform = self.selectionCircleLayer.transform; |
| 788 } |
| 789 |
| 790 - (CAShapeLayer*)newSelectionCircleLayer { |
| 791 const CGRect bounds = CGRectMake(0, 0, kSelectionEdge, kSelectionEdge); |
| 792 CAShapeLayer* selectionCircleLayer = [[CAShapeLayer alloc] init]; |
| 793 selectionCircleLayer.bounds = bounds; |
| 794 selectionCircleLayer.backgroundColor = [[UIColor clearColor] CGColor]; |
| 795 selectionCircleLayer.opacity = 0; |
| 796 selectionCircleLayer.transform = CATransform3DMakeScale( |
| 797 kSelectionInitialDownScale, kSelectionInitialDownScale, 1); |
| 798 selectionCircleLayer.path = |
| 799 [[UIBezierPath bezierPathWithOvalInRect:bounds] CGPath]; |
| 800 |
| 801 return selectionCircleLayer; |
| 802 } |
| 803 |
| 804 - (UIBezierPath*)circlePath:(CGFloat)dx { |
| 805 UIBezierPath* path = [UIBezierPath bezierPath]; |
| 806 |
| 807 CGFloat radius = kSelectionEdge * 0.5; |
| 808 CGFloat deformationDirection = dx > 0 ? 1 : -1; |
| 809 for (int i = 0; i < kBezierPathPointCount; i++) { |
| 810 CGPoint p; |
| 811 float angle = i * 2 * M_PI / kBezierPathPointCount; |
| 812 |
| 813 // Circle centered on 0. |
| 814 p.x = cos(angle) * radius; |
| 815 p.y = sin(angle) * radius; |
| 816 |
| 817 // Horizontal deformation. The further the points are from the center, the |
| 818 // larger the deformation is. |
| 819 if (p.x * deformationDirection > 0) { |
| 820 p.x += p.x * dx * KBezierPathFrontDeformation * deformationDirection; |
| 821 } else { |
| 822 p.x += p.x * dx * KBezierPathBackDeformation * deformationDirection; |
| 823 } |
| 824 |
| 825 // Translate center of circle. |
| 826 p.x += radius; |
| 827 p.y += radius; |
| 828 |
| 829 if (i == 0) { |
| 830 [path moveToPoint:p]; |
| 831 } else { |
| 832 [path addLineToPoint:p]; |
| 833 } |
| 834 } |
| 835 |
| 836 [path closePath]; |
| 837 return path; |
| 838 } |
| 839 |
| 840 - (void)setStyle:(ios_internal::OverscrollStyle)style { |
| 841 switch (style) { |
| 842 case ios_internal::OverscrollStyle::NTP_NON_INCOGNITO: |
| 843 [self.shadowView setHidden:YES]; |
| 844 self.backgroundColor = [UIColor whiteColor]; |
| 845 break; |
| 846 case ios_internal::OverscrollStyle::NTP_INCOGNITO: |
| 847 [self.shadowView setHidden:YES]; |
| 848 self.backgroundColor = [UIColor colorWithWhite:0 alpha:0]; |
| 849 break; |
| 850 case ios_internal::OverscrollStyle::REGULAR_PAGE_NON_INCOGNITO: |
| 851 [self.shadowView setHidden:NO]; |
| 852 self.backgroundColor = [UIColor colorWithRed:242.0 / 256 |
| 853 green:242.0 / 256 |
| 854 blue:242.0 / 256 |
| 855 alpha:1.0]; |
| 856 break; |
| 857 case ios_internal::OverscrollStyle::REGULAR_PAGE_INCOGNITO: |
| 858 [self.shadowView setHidden:NO]; |
| 859 self.backgroundColor = [UIColor colorWithRed:80.0 / 256 |
| 860 green:80.0 / 256 |
| 861 blue:80.0 / 256 |
| 862 alpha:1.0]; |
| 863 break; |
| 864 } |
| 865 |
| 866 BOOL incognito = |
| 867 style == ios_internal::OverscrollStyle::NTP_INCOGNITO || |
| 868 style == ios_internal::OverscrollStyle::REGULAR_PAGE_INCOGNITO; |
| 869 if (incognito) { |
| 870 [_addTabActionImageView |
| 871 setImage:[UIImage imageNamed:kNewTabActionActiveImage]]; |
| 872 [_refreshActionImageView |
| 873 setImage:[UIImage imageNamed:kRefreshActionActiveImage]]; |
| 874 [_closeTabActionImageView |
| 875 setImage:[UIImage imageNamed:kCloseActionActiveImage]]; |
| 876 _selectionCircleLayer.fillColor = |
| 877 [[UIColor colorWithRed:1 green:1 blue:1 alpha:0.2] CGColor]; |
| 878 _selectionCircleMaskLayer.fillColor = [[UIColor clearColor] CGColor]; |
| 879 } else { |
| 880 [_addTabActionImageView setImage:[UIImage imageNamed:kNewTabActionImage]]; |
| 881 [_refreshActionImageView setImage:[UIImage imageNamed:kRefreshActionImage]]; |
| 882 [_closeTabActionImageView setImage:[UIImage imageNamed:kCloseActionImage]]; |
| 883 |
| 884 [_addTabActionImageViewHighlighted |
| 885 setImage:[UIImage imageNamed:kNewTabActionActiveImage]]; |
| 886 [_refreshActionImageViewHighlighted |
| 887 setImage:[UIImage imageNamed:kRefreshActionActiveImage]]; |
| 888 [_closeTabActionImageViewHighlighted |
| 889 setImage:[UIImage imageNamed:kCloseActionActiveImage]]; |
| 890 |
| 891 _selectionCircleLayer.fillColor = [[UIColor colorWithRed:66.0 / 256 |
| 892 green:133.0 / 256 |
| 893 blue:244.0 / 256 |
| 894 alpha:1] CGColor]; |
| 895 _selectionCircleMaskLayer.fillColor = [[UIColor blackColor] CGColor]; |
| 896 } |
| 897 [_addTabActionImageView sizeToFit]; |
| 898 [_refreshActionImageView sizeToFit]; |
| 899 [_closeTabActionImageView sizeToFit]; |
| 900 [_addTabActionImageViewHighlighted sizeToFit]; |
| 901 [_refreshActionImageViewHighlighted sizeToFit]; |
| 902 [_closeTabActionImageViewHighlighted sizeToFit]; |
| 903 } |
| 904 |
| 905 - (ios_internal::OverscrollAction)actionAtLocation:(CGPoint)location { |
| 906 ios_internal::OverscrollAction action = ios_internal::OverscrollAction::NONE; |
| 907 if (CGRectContainsPoint( |
| 908 CGRectInset([_addTabActionImageView frame], |
| 909 -kDirectTouchFrameExpansion, -kDirectTouchFrameExpansion), |
| 910 location)) { |
| 911 action = ios_internal::OverscrollAction::NEW_TAB; |
| 912 } else if (CGRectContainsPoint(CGRectInset([_refreshActionImageView frame], |
| 913 -kDirectTouchFrameExpansion, |
| 914 -kDirectTouchFrameExpansion), |
| 915 location)) { |
| 916 action = ios_internal::OverscrollAction::REFRESH; |
| 917 } else if (CGRectContainsPoint(CGRectInset([_closeTabActionImageView frame], |
| 918 -kDirectTouchFrameExpansion, |
| 919 -kDirectTouchFrameExpansion), |
| 920 location)) { |
| 921 action = ios_internal::OverscrollAction::CLOSE_TAB; |
| 922 } |
| 923 return action; |
| 924 } |
| 925 |
| 926 - (void)updateSelectionForTouchedAction:(ios_internal::OverscrollAction)action { |
| 927 switch (action) { |
| 928 case ios_internal::OverscrollAction::NEW_TAB: |
| 929 [self updateWithHorizontalOffset:-1]; |
| 930 break; |
| 931 case ios_internal::OverscrollAction::REFRESH: |
| 932 [self updateWithHorizontalOffset:0]; |
| 933 break; |
| 934 case ios_internal::OverscrollAction::CLOSE_TAB: |
| 935 [self updateWithHorizontalOffset:1]; |
| 936 break; |
| 937 case ios_internal::OverscrollAction::NONE: |
| 938 return; |
| 939 break; |
| 940 } |
| 941 } |
| 942 |
| 943 // Clear the direct touch interaction after a small delay to prevent graphic |
| 944 // glitch with pan gesture selection deformation animations. |
| 945 - (void)clearDirectTouchInteraction { |
| 946 if (!_viewTouched) |
| 947 return; |
| 948 dispatch_after( |
| 949 dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), |
| 950 dispatch_get_main_queue(), ^{ |
| 951 _deformationBehaviorEnabled = YES; |
| 952 _viewTouched = NO; |
| 953 }); |
| 954 } |
| 955 |
| 956 #pragma mark - UIResponder |
| 957 |
| 958 - (void)touchesBegan:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event { |
| 959 [super touchesBegan:touches withEvent:event]; |
| 960 if (_viewTouched) |
| 961 return; |
| 962 |
| 963 _deformationBehaviorEnabled = NO; |
| 964 _snappingOffset = 0; |
| 965 CGPoint tapLocation = [[touches anyObject] locationInView:self]; |
| 966 [self updateSelectionForTouchedAction:[self actionAtLocation:tapLocation]]; |
| 967 [self layoutSubviews]; |
| 968 _viewTouched = YES; |
| 969 } |
| 970 |
| 971 - (void)touchesCancelled:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event { |
| 972 [super touchesCancelled:touches withEvent:event]; |
| 973 [self clearDirectTouchInteraction]; |
| 974 } |
| 975 |
| 976 - (void)touchesEnded:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event { |
| 977 [super touchesEnded:touches withEvent:event]; |
| 978 [self clearDirectTouchInteraction]; |
| 979 } |
| 980 |
| 981 #pragma mark - UIGestureRecognizerDelegate |
| 982 |
| 983 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer |
| 984 shouldRecognizeSimultaneouslyWithGestureRecognizer: |
| 985 (UIGestureRecognizer*)otherGestureRecognizer { |
| 986 return YES; |
| 987 } |
| 988 |
| 989 #pragma mark - Tap gesture action |
| 990 |
| 991 - (void)tapGesture:(UITapGestureRecognizer*)tapRecognizer { |
| 992 CGPoint tapLocation = [tapRecognizer locationInView:self]; |
| 993 ios_internal::OverscrollAction action = [self actionAtLocation:tapLocation]; |
| 994 if (action != ios_internal::OverscrollAction::NONE) { |
| 995 [self updateSelectionForTouchedAction:action]; |
| 996 [self setSelectedAction:action]; |
| 997 [self.delegate overscrollActionsViewDidTapTriggerAction:self]; |
| 998 } |
| 999 } |
| 1000 |
| 1001 @end |
OLD | NEW |