| 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
|
|
|