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

Unified 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 side-by-side diff with in-line comments
Download patch
Index: ios/chrome/browser/ui/overscroll_actions/overscroll_actions_view.mm
diff --git a/ios/chrome/browser/ui/overscroll_actions/overscroll_actions_view.mm b/ios/chrome/browser/ui/overscroll_actions/overscroll_actions_view.mm
new file mode 100644
index 0000000000000000000000000000000000000000..44aa1ecad5405d95099ff83939dde9a5bdbb1630
--- /dev/null
+++ b/ios/chrome/browser/ui/overscroll_actions/overscroll_actions_view.mm
@@ -0,0 +1,1001 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ios/chrome/browser/ui/overscroll_actions/overscroll_actions_view.h"
+
+#import <QuartzCore/QuartzCore.h>
+
+#include "base/logging.h"
+#include "base/mac/objc_property_releaser.h"
+#include "base/mac/scoped_nsobject.h"
+#include "ios/chrome/browser/ui/rtl_geometry.h"
+#include "ios/chrome/grit/ios_theme_resources.h"
+#include "ui/base/resource/resource_bundle.h"
+
+namespace {
+// Actions images.
+NSString* const kNewTabActionImage = @"ptr_new_tab";
+NSString* const kNewTabActionActiveImage = @"ptr_new_tab_active";
+NSString* const kRefreshActionImage = @"ptr_reload";
+NSString* const kRefreshActionActiveImage = @"ptr_reload_active";
+NSString* const kCloseActionImage = @"ptr_close";
+NSString* const kCloseActionActiveImage = @"ptr_close_active";
+
+// Represents a simple min/max range.
+typedef struct {
+ CGFloat min;
+ CGFloat max;
+} FloatRange;
+
+// The threshold at which the refresh actions will start to be visible.
+const CGFloat kRefreshThreshold = 48.0;
+// The threshold at which the actions are fully visible and can be selected.
+const CGFloat kFullThreshold = 56.0;
+// The size in point of the edges of the action selection circle layer.
+const CGFloat kSelectionEdge = 64.0;
+// Initial start position in X of the left and right actions from the center.
+// Left actions will start at center.x - kActionsStartPositionMarginFromCenter
+// Right actions will start at center.x + kActionsStartPositionMarginFromCenter
+const CGFloat kActionsStartPositionMarginFromCenter = 80.0;
+// Ranges mapping the width of the screen to the margin of the left and right
+// actions images from the frame center.
+const FloatRange kActionsPositionMarginsFrom = {320.0, 736.0};
+const FloatRange kActionsPositionMarginsTo = {100.0, 200.0};
+// Horizontal threshold before visual feedback starts. Threshold applied on
+// values in between [-1,1], where -1 corresponds to the leftmost action, and 1
+// corresponds to the rightmost action.
+const CGFloat kDistanceWhereMovementIsIgnored = 0.1;
+// Start scale of the action selection circle when no actions are displayed.
+const CGFloat kSelectionInitialDownScale = 0.1;
+// Start scale of the action selection circle when actions are displayed but
+// no action is selected.
+const CGFloat kSelectionDownScale = 0.1875;
+// The duration of the animations played when the actions are ready to
+// be triggered.
+const CGFloat kDisplayActionAnimationDuration = 0.5;
+// The final scale of the animation played when an action is triggered.
+const CGFloat kDisplayActionAnimationScale = 20;
+// The height of the shadow view.
+const CGFloat kShadowHeight = 2;
+// This controls how much the selection needs to be moved from the action center
+// in order to be snapped to the next action.
+// This value must stay in the interval [0,1].
+const CGFloat kSelectionSnappingOffsetFromCenter = 0.15;
+// Duration of the snapping animation moving the selection circle to the
+// selected action.
+const CGFloat kSelectionSnappingAnimationDuration = 0.2;
+// Controls how much the bezier shape's front and back are deformed.
+CGFloat KBezierPathFrontDeformation = 5.0;
+CGFloat KBezierPathBackDeformation = 2.5;
+// Controls the amount of points the bezier path is made of.
+int kBezierPathPointCount = 40;
+// Minimum delay to perform the transition to the ready state.
+const CFTimeInterval kMinimumPullDurationToTransitionToReadyInSeconds = 0.25;
+// Value in point to which the action icon frame will be expanded to detect user
+// direct touches.
+const CGFloat kDirectTouchFrameExpansion = 20;
+
+// This function maps a value from a range to another.
+CGFloat MapValueToRange(FloatRange from, FloatRange to, CGFloat value) {
+ DCHECK(from.min < from.max);
+ if (value <= from.min)
+ return to.min;
+ if (value >= from.max)
+ return to.max;
+ const CGFloat fromDst = from.max - from.min;
+ const CGFloat toDst = to.max - to.min;
+ return to.min + ((value - from.min) / fromDst) * toDst;
+}
+
+// Used to set the X position of a CALayer.
+void SetLayerPositionX(CALayer* layer, CGFloat value) {
+ CGPoint position = layer.position;
+ position.x = value;
+ layer.position = position;
+}
+
+// Describes the internal state of the OverscrollActionsView.
+enum class OverscrollViewState {
+ NONE, // Initial state.
+ PREPARE, // The actions are starting to be displayed.
+ READY // Actions are fully displayed.
+};
+} // namespace
+
+@interface OverscrollActionsView ()<UIGestureRecognizerDelegate> {
+ // True when the first layout has been done.
+ BOOL _initialLayoutDone;
+ // True when an action trigger animation is currently playing.
+ BOOL _animatingActionTrigger;
+ // Whether the selection circle is deformed.
+ BOOL _deformationBehaviorEnabled;
+ // Whether the view already made the transition to the READY state at least
+ // once.
+ BOOL _didTransitionToReadyState;
+ // True if the view is directly touched.
+ BOOL _viewTouched;
+ // An additionnal offset added to the horizontalOffset value in order to take
+ // into account snapping.
+ CGFloat _snappingOffset;
+ // The offset of the currently snapped action.
+ CGFloat _snappedActionOffset;
+ // The value of the horizontalOffset when a snap animation has been triggered.
+ CGFloat _horizontalOffsetOnAnimationStart;
+ // The last vertical offset.
+ CGFloat _lastVerticalOffset;
+ // Last recorded pull start absolute time.
+ // Unit is in seconds.
+ CFTimeInterval _pullStartTimeInSeconds;
+ // Tap gesture recognizer that allow the user to tap on an action to activate
+ // it.
+ base::scoped_nsobject<UITapGestureRecognizer> _tapGesture;
+ // Array of layers that will be centered vertically.
+ // The array is built the first time the method -layersToCenterVertically is
+ // called.
+ base::scoped_nsobject<NSArray> _layersToCenterVertically;
+ base::mac::ObjCPropertyReleaser _propertyReleaser_OverscrollActionsView;
+}
+
+// Redefined to readwrite.
+@property(nonatomic, assign, readwrite)
+ ios_internal::OverscrollAction selectedAction;
+
+// Actions image views.
+@property(nonatomic, retain) UIImageView* addTabActionImageView;
+@property(nonatomic, retain) UIImageView* refreshActionImageView;
+@property(nonatomic, retain) UIImageView* closeTabActionImageView;
+
+@property(nonatomic, retain) CALayer* highlightMaskLayer;
+
+@property(nonatomic, retain) UIImageView* addTabActionImageViewHighlighted;
+@property(nonatomic, retain) UIImageView* refreshActionImageViewHighlighted;
+@property(nonatomic, retain) UIImageView* closeTabActionImageViewHighlighted;
+
+// The layer displaying the selection circle.
+@property(nonatomic, retain) CAShapeLayer* selectionCircleLayer;
+// Mask layer used to display highlighted states when the selection circle is
+// above them.
+@property(nonatomic, retain) CAShapeLayer* selectionCircleMaskLayer;
+
+// The current vertical offset.
+@property(nonatomic, assign) CGFloat verticalOffset;
+// The current horizontal offset.
+@property(nonatomic, assign) CGFloat horizontalOffset;
+// The internal state of the OverscrollActionsView.
+@property(nonatomic, assign) OverscrollViewState overscrollState;
+// A shadow image view displayed at the bottom.
+@property(nonatomic, retain) UIImageView* shadowView;
+// Redefined to readwrite.
+@property(nonatomic, retain, readwrite) UIView* backgroundView;
+// Snapshot view added on top of the background image view.
+@property(nonatomic, retain, readwrite) UIView* snapshotView;
+// The parent layer on the selection circle used for cropping purpose.
+@property(nonatomic, retain, readwrite) CALayer* selectionCircleCroppingLayer;
+
+// An absolute horizontal offset that also takes into account snapping.
+- (CGFloat)absoluteHorizontalOffset;
+// Computes the margin of the actions image views using the screen's width.
+- (CGFloat)actionsPositionMarginFromCenter;
+// Performs the layout of the actions image views.
+- (void)layoutActions;
+// Absorbs the horizontal movement around the actions in intervals defined with
+// kDistanceWhereMovementIsIgnored.
+- (CGFloat)absorbsHorizontalMovementAroundActions:(CGFloat)x;
+// Computes the position of the selection circle layer based on the horizontal
+// offset.
+- (CGPoint)selectionCirclePosition;
+// Performs layout of the selection circle layer.
+- (void)layoutSelectionCircle;
+// Updates the selected action depending on the current internal state and
+// and the horizontal offset.
+- (void)updateSelectedAction;
+// Called when the selected action changes in order to perform animations that
+// depend on the currently selected action.
+- (void)onSelectedActionChange;
+// Layout method used to center subviews vertically.
+- (void)centerSubviewsVertically;
+// Updates the current internal state of the OverscrollActionsView depending
+// on vertical offset.
+- (void)updateState;
+// Called when the state changes in order to perform state dependent animations.
+- (void)onStateChange;
+// Resets values related to selection state.
+- (void)resetSelection;
+// Returns a newly allocated and configured selection circle shape.
+- (CAShapeLayer*)newSelectionCircleLayer;
+// Returns an autoreleased circular bezier path horizontally deformed according
+// to |dx|.
+- (UIBezierPath*)circlePath:(CGFloat)dx;
+// Returns the action at the given location in the view.
+- (ios_internal::OverscrollAction)actionAtLocation:(CGPoint)location;
+// Update the selection circle frame to select the given action.
+- (void)updateSelectionForTouchedAction:(ios_internal::OverscrollAction)action;
+// Clear the direct touch interaction after a small delay to prevent graphic
+// glitch with pan gesture selection deformation animations.
+- (void)clearDirectTouchInteraction;
+@end
+
+@implementation OverscrollActionsView
+
+@synthesize selectedAction = _selectedAction;
+@synthesize addTabActionImageView = _addTabActionImageView;
+@synthesize refreshActionImageView = _refreshActionImageView;
+@synthesize closeTabActionImageView = _closeTabActionImageView;
+@synthesize addTabActionImageViewHighlighted =
+ _addTabActionImageViewHighlighted;
+@synthesize refreshActionImageViewHighlighted =
+ _refreshActionImageViewHighlighted;
+@synthesize closeTabActionImageViewHighlighted =
+ _closeTabActionImageViewHighlighted;
+@synthesize highlightMaskLayer = _highlightMaskLayer;
+@synthesize selectionCircleLayer = _selectionCircleLayer;
+@synthesize selectionCircleMaskLayer = _selectionCircleMaskLayer;
+@synthesize verticalOffset = _verticalOffset;
+@synthesize horizontalOffset = _horizontalOffset;
+@synthesize overscrollState = _overscrollState;
+@synthesize shadowView = _shadowView;
+@synthesize backgroundView = _backgroundView;
+@synthesize snapshotView = _snapshotView;
+@synthesize selectionCircleCroppingLayer = _selectionCircleCroppingLayer;
+@synthesize delegate = _delegate;
+
+- (instancetype)initWithFrame:(CGRect)frame {
+ self = [super initWithFrame:frame];
+ if (self) {
+ _propertyReleaser_OverscrollActionsView.Init(self,
+ [OverscrollActionsView class]);
+ _deformationBehaviorEnabled = YES;
+ self.autoresizingMask =
+ UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+ _selectionCircleLayer = [self newSelectionCircleLayer];
+ _selectionCircleMaskLayer = [self newSelectionCircleLayer];
+ _selectionCircleMaskLayer.contentsGravity = kCAGravityCenter;
+ _selectionCircleCroppingLayer = [[CALayer alloc] init];
+ _selectionCircleCroppingLayer.frame = self.bounds;
+ [_selectionCircleCroppingLayer setMasksToBounds:YES];
+
+ [self.layer addSublayer:_selectionCircleCroppingLayer];
+ [_selectionCircleCroppingLayer addSublayer:_selectionCircleLayer];
+
+ _addTabActionImageView = [[UIImageView alloc] init];
+ [self addSubview:_addTabActionImageView];
+ _refreshActionImageView = [[UIImageView alloc] init];
+ if (UseRTLLayout())
+ [_refreshActionImageView setTransform:CGAffineTransformMakeScale(-1, 1)];
+ [self addSubview:_refreshActionImageView];
+ _closeTabActionImageView = [[UIImageView alloc] init];
+ [self addSubview:_closeTabActionImageView];
+
+ _highlightMaskLayer = [[CALayer alloc] init];
+ _highlightMaskLayer.frame = self.bounds;
+ _highlightMaskLayer.contentsGravity = kCAGravityCenter;
+ _highlightMaskLayer.backgroundColor = [[UIColor clearColor] CGColor];
+ [_highlightMaskLayer setMask:_selectionCircleMaskLayer];
+ [self.layer addSublayer:_highlightMaskLayer];
+
+ _addTabActionImageViewHighlighted = [[UIImageView alloc] init];
+ _refreshActionImageViewHighlighted = [[UIImageView alloc] init];
+ if (UseRTLLayout()) {
+ [_refreshActionImageViewHighlighted
+ setTransform:CGAffineTransformMakeScale(-1, 1)];
+ }
+ _closeTabActionImageViewHighlighted = [[UIImageView alloc] init];
+ [_highlightMaskLayer addSublayer:_addTabActionImageViewHighlighted.layer];
+ [_highlightMaskLayer addSublayer:_refreshActionImageViewHighlighted.layer];
+ [_highlightMaskLayer addSublayer:_closeTabActionImageViewHighlighted.layer];
+
+ _shadowView = [[UIImageView alloc] initWithFrame:CGRectZero];
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ gfx::Image shadow = rb.GetNativeImageNamed(IDR_IOS_TOOLBAR_SHADOW);
+ [_shadowView setImage:shadow.ToUIImage()];
+ [self addSubview:_shadowView];
+
+ _backgroundView = [[UIView alloc] initWithFrame:CGRectZero];
+ [self addSubview:_backgroundView];
+
+ if (UseRTLLayout())
+ [self setTransform:CGAffineTransformMakeScale(-1, 1)];
+
+ _tapGesture.reset([[UITapGestureRecognizer alloc]
+ initWithTarget:self
+ action:@selector(tapGesture:)]);
+ [_tapGesture setDelegate:self];
+ [self addGestureRecognizer:_tapGesture];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [self.snapshotView removeFromSuperview];
+ [super dealloc];
+}
+
+- (BOOL)selectionCroppingEnabled {
+ return [_selectionCircleCroppingLayer masksToBounds];
+}
+
+- (void)setSelectionCroppingEnabled:(BOOL)enableSelectionCropping {
+ [_selectionCircleCroppingLayer setMasksToBounds:enableSelectionCropping];
+}
+
+- (void)addSnapshotView:(UIView*)view {
+ if (UseRTLLayout()) {
+ [CATransaction begin];
+ [CATransaction setDisableActions:YES];
+ [view setTransform:CGAffineTransformMakeScale(-1, 1)];
+ [CATransaction commit];
+ }
+ [self.snapshotView removeFromSuperview];
+ self.snapshotView = view;
+ [self.backgroundView addSubview:self.snapshotView];
+}
+
+- (void)pullStarted {
+ _didTransitionToReadyState = NO;
+ _pullStartTimeInSeconds = CACurrentMediaTime();
+ // Ensure we will update the state after time threshold even without offset
+ // change.
+ dispatch_after(
+ dispatch_time(DISPATCH_TIME_NOW,
+ (kMinimumPullDurationToTransitionToReadyInSeconds + 0.01) *
+ NSEC_PER_SEC),
+ dispatch_get_main_queue(), ^{
+ [self updateState];
+ });
+}
+
+- (void)updateWithVerticalOffset:(CGFloat)offset {
+ _lastVerticalOffset = self.verticalOffset;
+ self.verticalOffset = offset;
+ [self updateState];
+}
+
+- (void)updateWithHorizontalOffset:(CGFloat)offset {
+ if (_animatingActionTrigger || _viewTouched)
+ return;
+ self.horizontalOffset = offset;
+ // Absorb out of range offset values so that the user doesn't need to
+ // compensate in order to move the cursor in the other direction.
+ if ([self absoluteHorizontalOffset] < -1)
+ _snappingOffset = -self.horizontalOffset - 1;
+ if ([self absoluteHorizontalOffset] > 1)
+ _snappingOffset = 1 - self.horizontalOffset;
+ [self setNeedsLayout];
+}
+
+- (void)displayActionAnimation {
+ _animatingActionTrigger = YES;
+ [CATransaction begin];
+ [CATransaction setCompletionBlock:^{
+ _animatingActionTrigger = NO;
+ [CATransaction begin];
+ [CATransaction setDisableActions:YES];
+ // See comment below for why we manually set opacity to 0 and remove
+ // the animation.
+ self.selectionCircleLayer.opacity = 0;
+ [self.selectionCircleLayer removeAnimationForKey:@"opacity"];
+ [self onStateChange];
+ [CATransaction commit];
+ }];
+
+ CABasicAnimation* scaleAnimation =
+ [CABasicAnimation animationWithKeyPath:@"transform"];
+ scaleAnimation.fromValue =
+ [NSValue valueWithCATransform3D:CATransform3DIdentity];
+ scaleAnimation.toValue =
+ [NSValue valueWithCATransform3D:CATransform3DMakeScale(
+ kDisplayActionAnimationScale,
+ kDisplayActionAnimationScale, 1)];
+ scaleAnimation.duration = kDisplayActionAnimationDuration;
+ [self.selectionCircleLayer addAnimation:scaleAnimation forKey:@"transform"];
+
+ CABasicAnimation* opacityAnimation =
+ [CABasicAnimation animationWithKeyPath:@"opacity"];
+ opacityAnimation.fromValue = @(1);
+ opacityAnimation.toValue = @(0);
+ opacityAnimation.duration = kDisplayActionAnimationDuration;
+ // A fillMode forward and manual removal of the animation is needed because
+ // the completion handler can be called one frame earlier for the first
+ // animation (transform) causing the opacity animation to be removed and show
+ // an opacity of 1 for one or two frames.
+ opacityAnimation.fillMode = kCAFillModeForwards;
+ opacityAnimation.removedOnCompletion = NO;
+ [self.selectionCircleLayer addAnimation:opacityAnimation forKey:@"opacity"];
+
+ [CATransaction commit];
+}
+
+- (void)layoutSubviews {
+ [super layoutSubviews];
+
+ [CATransaction begin];
+ [CATransaction setDisableActions:YES];
+ if (self.snapshotView)
+ self.backgroundView.frame = self.snapshotView.bounds;
+ _selectionCircleCroppingLayer.frame = self.bounds;
+ _highlightMaskLayer.frame = self.bounds;
+
+ CGRect shadowFrame = self.bounds;
+ shadowFrame.origin.y = self.bounds.size.height;
+ shadowFrame.size.height = kShadowHeight;
+ self.shadowView.frame = shadowFrame;
+ [CATransaction commit];
+
+ const BOOL disableActionsOnInitialLayout =
+ !CGRectEqualToRect(CGRectZero, self.frame) && !_initialLayoutDone;
+ if (disableActionsOnInitialLayout) {
+ [CATransaction begin];
+ [CATransaction setDisableActions:YES];
+ _initialLayoutDone = YES;
+ }
+ [self centerSubviewsVertically];
+ [self layoutActions];
+ if (_deformationBehaviorEnabled)
+ [self layoutSelectionCircleWithDeformation];
+ else
+ [self layoutSelectionCircle];
+ [self updateSelectedAction];
+ if (disableActionsOnInitialLayout)
+ [CATransaction commit];
+}
+
+#pragma mark - Private
+
+- (CGFloat)absoluteHorizontalOffset {
+ return self.horizontalOffset + _snappingOffset;
+}
+
+- (CGFloat)actionsPositionMarginFromCenter {
+ return MapValueToRange(kActionsPositionMarginsFrom, kActionsPositionMarginsTo,
+ self.bounds.size.width);
+}
+
+- (void)layoutActions {
+ const CGFloat width = self.bounds.size.width;
+ const CGFloat centerX = width / 2.0;
+ const CGFloat actionsPositionMargin = [self actionsPositionMarginFromCenter];
+
+ [UIView beginAnimations:@"position" context:NULL];
+ [UIView setAnimationDuration:0.1];
+ SetLayerPositionX(self.refreshActionImageView.layer, centerX);
+ SetLayerPositionX(self.refreshActionImageViewHighlighted.layer, centerX);
+
+ const CGFloat addTabPositionX =
+ MapValueToRange({kRefreshThreshold, kFullThreshold},
+ {centerX - kActionsStartPositionMarginFromCenter,
+ centerX - actionsPositionMargin},
+ self.verticalOffset);
+ SetLayerPositionX(self.addTabActionImageView.layer, addTabPositionX);
+ SetLayerPositionX(self.addTabActionImageViewHighlighted.layer,
+ addTabPositionX);
+
+ const CGFloat closeTabPositionX =
+ MapValueToRange({kRefreshThreshold, kFullThreshold},
+ {centerX + kActionsStartPositionMarginFromCenter,
+ centerX + actionsPositionMargin},
+ self.verticalOffset);
+ SetLayerPositionX(self.closeTabActionImageView.layer, closeTabPositionX);
+ SetLayerPositionX(self.closeTabActionImageViewHighlighted.layer,
+ closeTabPositionX);
+
+ [UIView commitAnimations];
+
+ [UIView beginAnimations:@"opacity" context:NULL];
+ [UIView setAnimationDuration:0.1];
+ self.refreshActionImageView.layer.opacity = MapValueToRange(
+ {kFullThreshold / 2.0, kFullThreshold}, {0, 1}, self.verticalOffset);
+ self.refreshActionImageViewHighlighted.layer.opacity =
+ self.refreshActionImageView.layer.opacity;
+ self.addTabActionImageView.layer.opacity = MapValueToRange(
+ {kRefreshThreshold, kFullThreshold}, {0, 1}, self.verticalOffset);
+ self.addTabActionImageViewHighlighted.layer.opacity =
+ self.addTabActionImageView.layer.opacity;
+ self.closeTabActionImageView.layer.opacity = MapValueToRange(
+ {kRefreshThreshold, kFullThreshold}, {0, 1}, self.verticalOffset);
+ self.closeTabActionImageViewHighlighted.layer.opacity =
+ self.closeTabActionImageView.layer.opacity;
+ [UIView commitAnimations];
+
+ [UIView beginAnimations:@"transform" context:NULL];
+ [UIView setAnimationDuration:0.1];
+ CATransform3D rotation = CATransform3DMakeRotation(
+ MapValueToRange({kFullThreshold / 2.0, kFullThreshold}, {-M_PI_2, M_PI_4},
+ self.verticalOffset),
+ 0, 0, 1);
+ self.refreshActionImageView.layer.transform = rotation;
+ self.refreshActionImageViewHighlighted.layer.transform = rotation;
+ [UIView commitAnimations];
+}
+
+- (CGFloat)absorbsHorizontalMovementAroundActions:(CGFloat)x {
+ // The limits of the intervals where x is constant.
+ const CGFloat kLeftActionAbsorptionLimit =
+ -1 + kDistanceWhereMovementIsIgnored;
+ const CGFloat kCenterActionLeftAbsorptionLimit =
+ -kDistanceWhereMovementIsIgnored;
+ const CGFloat kCenterActionRightAbsorptionLimit =
+ kDistanceWhereMovementIsIgnored;
+ const CGFloat kRightActionAbsorptionLimit =
+ 1 - kDistanceWhereMovementIsIgnored;
+ if (x < kLeftActionAbsorptionLimit) {
+ return -1;
+ }
+ if (x < kCenterActionLeftAbsorptionLimit) {
+ return MapValueToRange(
+ {kLeftActionAbsorptionLimit, kCenterActionLeftAbsorptionLimit}, {-1, 0},
+ x);
+ }
+ if (x < kCenterActionRightAbsorptionLimit) {
+ return 0;
+ }
+ if (x < kRightActionAbsorptionLimit) {
+ return MapValueToRange(
+ {kCenterActionRightAbsorptionLimit, kRightActionAbsorptionLimit},
+ {0, 1}, x);
+ }
+ return 1;
+}
+
+- (CGPoint)selectionCirclePosition {
+ const CGFloat centerX = self.bounds.size.width / 2.0;
+ const CGFloat actionsPositionMargin = [self actionsPositionMarginFromCenter];
+ const CGFloat transformedOffset = [self
+ absorbsHorizontalMovementAroundActions:[self absoluteHorizontalOffset]];
+ return CGPointMake(MapValueToRange({-1, 1}, {centerX - actionsPositionMargin,
+ centerX + actionsPositionMargin},
+ transformedOffset),
+ self.bounds.size.height / 2.0);
+}
+
+- (void)layoutSelectionCircle {
+ if (self.overscrollState == OverscrollViewState::READY) {
+ [CATransaction begin];
+ [CATransaction setDisableActions:YES];
+ self.selectionCircleLayer.position = [self selectionCirclePosition];
+ self.selectionCircleMaskLayer.position = self.selectionCircleLayer.position;
+ [CATransaction commit];
+ }
+}
+
+- (void)layoutSelectionCircleWithDeformation {
+ if (self.overscrollState == OverscrollViewState::READY) {
+ BOOL animate = NO;
+ CGFloat snapDistance =
+ [self absoluteHorizontalOffset] - _snappedActionOffset;
+ // Cancel out deformation for small movements.
+ if (fabs(snapDistance) < kDistanceWhereMovementIsIgnored) {
+ snapDistance = 0;
+ } else {
+ snapDistance -= snapDistance > 0 ? kDistanceWhereMovementIsIgnored
+ : -kDistanceWhereMovementIsIgnored;
+ }
+
+ [self.selectionCircleLayer removeAnimationForKey:@"path"];
+ [self.selectionCircleMaskLayer removeAnimationForKey:@"path"];
+ self.selectionCircleLayer.path = [self circlePath:snapDistance].CGPath;
+ self.selectionCircleMaskLayer.path = self.selectionCircleLayer.path;
+
+ if (fabs(snapDistance) > kSelectionSnappingOffsetFromCenter) {
+ animate = YES;
+ _snappedActionOffset += (snapDistance < 0 ? -1 : 1);
+ _snappingOffset = _snappedActionOffset - self.horizontalOffset;
+ _horizontalOffsetOnAnimationStart = self.horizontalOffset;
+ const CGFloat finalSnapDistance =
+ [self absoluteHorizontalOffset] - _snappedActionOffset;
+
+ UIBezierPath* finalPath = [self circlePath:finalSnapDistance];
+ [CATransaction begin];
+ [CATransaction setCompletionBlock:^{
+ self.selectionCircleLayer.path = finalPath.CGPath;
+ [self.selectionCircleLayer removeAnimationForKey:@"path"];
+ self.selectionCircleMaskLayer.path = finalPath.CGPath;
+ [self.selectionCircleMaskLayer removeAnimationForKey:@"path"];
+ }];
+ CABasicAnimation* (^pathAnimation)(void) = ^{
+ CABasicAnimation* pathAnim =
+ [CABasicAnimation animationWithKeyPath:@"path"];
+ pathAnim.removedOnCompletion = NO;
+ pathAnim.fillMode = kCAFillModeForwards;
+ pathAnim.duration = kSelectionSnappingAnimationDuration;
+ pathAnim.toValue = (__bridge id)finalPath.CGPath;
+ return pathAnim;
+ };
+ [self.selectionCircleLayer addAnimation:pathAnimation() forKey:@"path"];
+ [self.selectionCircleMaskLayer addAnimation:pathAnimation()
+ forKey:@"path"];
+ [CATransaction commit];
+ }
+ [CATransaction begin];
+ if (!animate)
+ [CATransaction setDisableActions:YES];
+ else
+ [CATransaction setAnimationDuration:kSelectionSnappingAnimationDuration];
+ self.selectionCircleLayer.position = [self selectionCirclePosition];
+ self.selectionCircleMaskLayer.position = self.selectionCircleLayer.position;
+ [CATransaction commit];
+ }
+}
+
+- (void)updateSelectedAction {
+ if (self.overscrollState != OverscrollViewState::READY) {
+ self.selectedAction = ios_internal::OverscrollAction::NONE;
+ return;
+ }
+
+ // Update action index by checking that the action image layer is included
+ // inside the selection layer.
+ const CGPoint selectionPosition = [self selectionCirclePosition];
+ if (_deformationBehaviorEnabled) {
+ const CGFloat distanceBetweenTwoActions =
+ (self.refreshActionImageView.frame.origin.x -
+ self.addTabActionImageView.frame.origin.x) /
+ 2;
+ if (fabs(self.addTabActionImageView.center.x - selectionPosition.x) <
+ distanceBetweenTwoActions) {
+ self.selectedAction = ios_internal::OverscrollAction::NEW_TAB;
+ }
+ if (fabs(self.refreshActionImageView.center.x - selectionPosition.x) <
+ distanceBetweenTwoActions) {
+ self.selectedAction = ios_internal::OverscrollAction::REFRESH;
+ }
+ if (fabs(self.closeTabActionImageView.center.x - selectionPosition.x) <
+ distanceBetweenTwoActions) {
+ self.selectedAction = ios_internal::OverscrollAction::CLOSE_TAB;
+ }
+ } else {
+ const CGRect selectionRect =
+ CGRectMake(selectionPosition.x - kSelectionEdge / 2.0,
+ selectionPosition.y - kSelectionEdge / 2.0, kSelectionEdge,
+ kSelectionEdge);
+ const CGRect addTabRect = self.addTabActionImageView.frame;
+ const CGRect closeTabRect = self.closeTabActionImageView.frame;
+ const CGRect refreshRect = self.refreshActionImageView.frame;
+
+ if (CGRectContainsRect(selectionRect, addTabRect)) {
+ self.selectedAction = ios_internal::OverscrollAction::NEW_TAB;
+ } else if (CGRectContainsRect(selectionRect, refreshRect)) {
+ self.selectedAction = ios_internal::OverscrollAction::REFRESH;
+ } else if (CGRectContainsRect(selectionRect, closeTabRect)) {
+ self.selectedAction = ios_internal::OverscrollAction::CLOSE_TAB;
+ } else {
+ self.selectedAction = ios_internal::OverscrollAction::NONE;
+ }
+ }
+}
+
+- (void)setSelectedAction:(ios_internal::OverscrollAction)action {
+ if (_selectedAction != action) {
+ _selectedAction = action;
+ [self onSelectedActionChange];
+ }
+}
+
+- (void)onSelectedActionChange {
+ if (self.overscrollState == OverscrollViewState::PREPARE ||
+ _animatingActionTrigger)
+ return;
+
+ [UIView beginAnimations:@"transform" context:NULL];
+ [UIView setAnimationDuration:kSelectionSnappingAnimationDuration];
+ if (self.selectedAction == ios_internal::OverscrollAction::NONE) {
+ if (!_deformationBehaviorEnabled) {
+ // Scale selection down.
+ self.selectionCircleLayer.transform =
+ CATransform3DMakeScale(kSelectionDownScale, kSelectionDownScale, 1);
+ self.selectionCircleMaskLayer.transform =
+ self.selectionCircleLayer.transform;
+ }
+ } else {
+ // Scale selection up.
+ self.selectionCircleLayer.transform = CATransform3DMakeScale(1, 1, 1);
+ self.selectionCircleMaskLayer.transform =
+ self.selectionCircleLayer.transform;
+ }
+ [UIView commitAnimations];
+}
+
+- (base::scoped_nsobject<NSArray>&)layersToCenterVertically {
+ if (!_layersToCenterVertically) {
+ NSArray* layersToCenterVertically = @[
+ _selectionCircleLayer, _selectionCircleMaskLayer,
+ _addTabActionImageView.layer, _refreshActionImageView.layer,
+ _closeTabActionImageView.layer, _addTabActionImageViewHighlighted.layer,
+ _refreshActionImageViewHighlighted.layer,
+ _closeTabActionImageViewHighlighted.layer, _backgroundView.layer
+ ];
+ _layersToCenterVertically.reset([layersToCenterVertically retain]);
+ }
+ return _layersToCenterVertically;
+}
+
+- (void)centerSubviewsVertically {
+ [CATransaction begin];
+ [CATransaction setDisableActions:YES];
+ for (CALayer* layer in [self layersToCenterVertically].get()) {
+ CGPoint position = layer.position;
+ position.y = self.bounds.size.height / 2;
+ layer.position = position;
+ }
+ [CATransaction commit];
+}
+
+- (void)updateState {
+ if (self.verticalOffset > 1) {
+ const CFTimeInterval elapsedTime =
+ CACurrentMediaTime() - _pullStartTimeInSeconds;
+ const BOOL isMinimumTimeElapsed =
+ elapsedTime >= kMinimumPullDurationToTransitionToReadyInSeconds;
+ const BOOL isPullingDownOrAlreadyTriggeredOnce =
+ _lastVerticalOffset <= self.verticalOffset ||
+ _didTransitionToReadyState;
+ const BOOL isVerticalThresholdSatisfied =
+ self.verticalOffset >= kFullThreshold;
+ if (isPullingDownOrAlreadyTriggeredOnce && isVerticalThresholdSatisfied &&
+ isMinimumTimeElapsed) {
+ self.overscrollState = OverscrollViewState::READY;
+ } else {
+ self.overscrollState = OverscrollViewState::PREPARE;
+ }
+ } else {
+ self.overscrollState = OverscrollViewState::NONE;
+ }
+ [self setNeedsLayout];
+}
+
+- (void)setOverscrollState:(OverscrollViewState)state {
+ if (_overscrollState != state) {
+ _overscrollState = state;
+ [self onStateChange];
+ }
+}
+
+- (void)onStateChange {
+ if (_animatingActionTrigger)
+ return;
+
+ if (self.overscrollState != OverscrollViewState::NONE) {
+ [UIView beginAnimations:@"opacity" context:NULL];
+ [UIView setAnimationDuration:kSelectionSnappingAnimationDuration];
+ self.selectionCircleLayer.opacity =
+ self.overscrollState == OverscrollViewState::READY ? 1.0 : 0.0;
+ self.selectionCircleMaskLayer.opacity = self.selectionCircleLayer.opacity;
+ [UIView commitAnimations];
+ if (self.overscrollState == OverscrollViewState::PREPARE) {
+ [UIView beginAnimations:@"transform" context:NULL];
+ [UIView setAnimationDuration:kSelectionSnappingAnimationDuration];
+ [self resetSelection];
+ [UIView commitAnimations];
+ } else {
+ _didTransitionToReadyState = YES;
+ }
+ } else {
+ [CATransaction begin];
+ [CATransaction setDisableActions:YES];
+ [self resetSelection];
+ [CATransaction commit];
+ }
+}
+
+- (void)resetSelection {
+ _didTransitionToReadyState = NO;
+ _snappingOffset = 0;
+ _snappedActionOffset = 0;
+ _horizontalOffsetOnAnimationStart = 0;
+ self.selectionCircleLayer.transform = CATransform3DMakeScale(
+ kSelectionInitialDownScale, kSelectionInitialDownScale, 1);
+ self.selectionCircleMaskLayer.transform = self.selectionCircleLayer.transform;
+}
+
+- (CAShapeLayer*)newSelectionCircleLayer {
+ const CGRect bounds = CGRectMake(0, 0, kSelectionEdge, kSelectionEdge);
+ CAShapeLayer* selectionCircleLayer = [[CAShapeLayer alloc] init];
+ selectionCircleLayer.bounds = bounds;
+ selectionCircleLayer.backgroundColor = [[UIColor clearColor] CGColor];
+ selectionCircleLayer.opacity = 0;
+ selectionCircleLayer.transform = CATransform3DMakeScale(
+ kSelectionInitialDownScale, kSelectionInitialDownScale, 1);
+ selectionCircleLayer.path =
+ [[UIBezierPath bezierPathWithOvalInRect:bounds] CGPath];
+
+ return selectionCircleLayer;
+}
+
+- (UIBezierPath*)circlePath:(CGFloat)dx {
+ UIBezierPath* path = [UIBezierPath bezierPath];
+
+ CGFloat radius = kSelectionEdge * 0.5;
+ CGFloat deformationDirection = dx > 0 ? 1 : -1;
+ for (int i = 0; i < kBezierPathPointCount; i++) {
+ CGPoint p;
+ float angle = i * 2 * M_PI / kBezierPathPointCount;
+
+ // Circle centered on 0.
+ p.x = cos(angle) * radius;
+ p.y = sin(angle) * radius;
+
+ // Horizontal deformation. The further the points are from the center, the
+ // larger the deformation is.
+ if (p.x * deformationDirection > 0) {
+ p.x += p.x * dx * KBezierPathFrontDeformation * deformationDirection;
+ } else {
+ p.x += p.x * dx * KBezierPathBackDeformation * deformationDirection;
+ }
+
+ // Translate center of circle.
+ p.x += radius;
+ p.y += radius;
+
+ if (i == 0) {
+ [path moveToPoint:p];
+ } else {
+ [path addLineToPoint:p];
+ }
+ }
+
+ [path closePath];
+ return path;
+}
+
+- (void)setStyle:(ios_internal::OverscrollStyle)style {
+ switch (style) {
+ case ios_internal::OverscrollStyle::NTP_NON_INCOGNITO:
+ [self.shadowView setHidden:YES];
+ self.backgroundColor = [UIColor whiteColor];
+ break;
+ case ios_internal::OverscrollStyle::NTP_INCOGNITO:
+ [self.shadowView setHidden:YES];
+ self.backgroundColor = [UIColor colorWithWhite:0 alpha:0];
+ break;
+ case ios_internal::OverscrollStyle::REGULAR_PAGE_NON_INCOGNITO:
+ [self.shadowView setHidden:NO];
+ self.backgroundColor = [UIColor colorWithRed:242.0 / 256
+ green:242.0 / 256
+ blue:242.0 / 256
+ alpha:1.0];
+ break;
+ case ios_internal::OverscrollStyle::REGULAR_PAGE_INCOGNITO:
+ [self.shadowView setHidden:NO];
+ self.backgroundColor = [UIColor colorWithRed:80.0 / 256
+ green:80.0 / 256
+ blue:80.0 / 256
+ alpha:1.0];
+ break;
+ }
+
+ BOOL incognito =
+ style == ios_internal::OverscrollStyle::NTP_INCOGNITO ||
+ style == ios_internal::OverscrollStyle::REGULAR_PAGE_INCOGNITO;
+ if (incognito) {
+ [_addTabActionImageView
+ setImage:[UIImage imageNamed:kNewTabActionActiveImage]];
+ [_refreshActionImageView
+ setImage:[UIImage imageNamed:kRefreshActionActiveImage]];
+ [_closeTabActionImageView
+ setImage:[UIImage imageNamed:kCloseActionActiveImage]];
+ _selectionCircleLayer.fillColor =
+ [[UIColor colorWithRed:1 green:1 blue:1 alpha:0.2] CGColor];
+ _selectionCircleMaskLayer.fillColor = [[UIColor clearColor] CGColor];
+ } else {
+ [_addTabActionImageView setImage:[UIImage imageNamed:kNewTabActionImage]];
+ [_refreshActionImageView setImage:[UIImage imageNamed:kRefreshActionImage]];
+ [_closeTabActionImageView setImage:[UIImage imageNamed:kCloseActionImage]];
+
+ [_addTabActionImageViewHighlighted
+ setImage:[UIImage imageNamed:kNewTabActionActiveImage]];
+ [_refreshActionImageViewHighlighted
+ setImage:[UIImage imageNamed:kRefreshActionActiveImage]];
+ [_closeTabActionImageViewHighlighted
+ setImage:[UIImage imageNamed:kCloseActionActiveImage]];
+
+ _selectionCircleLayer.fillColor = [[UIColor colorWithRed:66.0 / 256
+ green:133.0 / 256
+ blue:244.0 / 256
+ alpha:1] CGColor];
+ _selectionCircleMaskLayer.fillColor = [[UIColor blackColor] CGColor];
+ }
+ [_addTabActionImageView sizeToFit];
+ [_refreshActionImageView sizeToFit];
+ [_closeTabActionImageView sizeToFit];
+ [_addTabActionImageViewHighlighted sizeToFit];
+ [_refreshActionImageViewHighlighted sizeToFit];
+ [_closeTabActionImageViewHighlighted sizeToFit];
+}
+
+- (ios_internal::OverscrollAction)actionAtLocation:(CGPoint)location {
+ ios_internal::OverscrollAction action = ios_internal::OverscrollAction::NONE;
+ if (CGRectContainsPoint(
+ CGRectInset([_addTabActionImageView frame],
+ -kDirectTouchFrameExpansion, -kDirectTouchFrameExpansion),
+ location)) {
+ action = ios_internal::OverscrollAction::NEW_TAB;
+ } else if (CGRectContainsPoint(CGRectInset([_refreshActionImageView frame],
+ -kDirectTouchFrameExpansion,
+ -kDirectTouchFrameExpansion),
+ location)) {
+ action = ios_internal::OverscrollAction::REFRESH;
+ } else if (CGRectContainsPoint(CGRectInset([_closeTabActionImageView frame],
+ -kDirectTouchFrameExpansion,
+ -kDirectTouchFrameExpansion),
+ location)) {
+ action = ios_internal::OverscrollAction::CLOSE_TAB;
+ }
+ return action;
+}
+
+- (void)updateSelectionForTouchedAction:(ios_internal::OverscrollAction)action {
+ switch (action) {
+ case ios_internal::OverscrollAction::NEW_TAB:
+ [self updateWithHorizontalOffset:-1];
+ break;
+ case ios_internal::OverscrollAction::REFRESH:
+ [self updateWithHorizontalOffset:0];
+ break;
+ case ios_internal::OverscrollAction::CLOSE_TAB:
+ [self updateWithHorizontalOffset:1];
+ break;
+ case ios_internal::OverscrollAction::NONE:
+ return;
+ break;
+ }
+}
+
+// Clear the direct touch interaction after a small delay to prevent graphic
+// glitch with pan gesture selection deformation animations.
+- (void)clearDirectTouchInteraction {
+ if (!_viewTouched)
+ return;
+ dispatch_after(
+ dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)),
+ dispatch_get_main_queue(), ^{
+ _deformationBehaviorEnabled = YES;
+ _viewTouched = NO;
+ });
+}
+
+#pragma mark - UIResponder
+
+- (void)touchesBegan:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
+ [super touchesBegan:touches withEvent:event];
+ if (_viewTouched)
+ return;
+
+ _deformationBehaviorEnabled = NO;
+ _snappingOffset = 0;
+ CGPoint tapLocation = [[touches anyObject] locationInView:self];
+ [self updateSelectionForTouchedAction:[self actionAtLocation:tapLocation]];
+ [self layoutSubviews];
+ _viewTouched = YES;
+}
+
+- (void)touchesCancelled:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
+ [super touchesCancelled:touches withEvent:event];
+ [self clearDirectTouchInteraction];
+}
+
+- (void)touchesEnded:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
+ [super touchesEnded:touches withEvent:event];
+ [self clearDirectTouchInteraction];
+}
+
+#pragma mark - UIGestureRecognizerDelegate
+
+- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
+ shouldRecognizeSimultaneouslyWithGestureRecognizer:
+ (UIGestureRecognizer*)otherGestureRecognizer {
+ return YES;
+}
+
+#pragma mark - Tap gesture action
+
+- (void)tapGesture:(UITapGestureRecognizer*)tapRecognizer {
+ CGPoint tapLocation = [tapRecognizer locationInView:self];
+ ios_internal::OverscrollAction action = [self actionAtLocation:tapLocation];
+ if (action != ios_internal::OverscrollAction::NONE) {
+ [self updateSelectionForTouchedAction:action];
+ [self setSelectedAction:action];
+ [self.delegate overscrollActionsViewDidTapTriggerAction:self];
+ }
+}
+
+@end

Powered by Google App Engine
This is Rietveld 408576698