Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(276)

Side by Side Diff: ios/chrome/browser/ui/overscroll_actions/overscroll_actions_view.mm

Issue 2589803002: Upstream Chrome on iOS source code [6/11]. (Closed)
Patch Set: Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698