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

Unified Diff: ios/chrome/browser/ui/side_swipe/side_swipe_navigation_view.mm

Issue 2587023002: Upstream Chrome on iOS source code [8/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/side_swipe/side_swipe_navigation_view.mm
diff --git a/ios/chrome/browser/ui/side_swipe/side_swipe_navigation_view.mm b/ios/chrome/browser/ui/side_swipe/side_swipe_navigation_view.mm
new file mode 100644
index 0000000000000000000000000000000000000000..24dbefc133527e548c280c500346c74102ad9402
--- /dev/null
+++ b/ios/chrome/browser/ui/side_swipe/side_swipe_navigation_view.mm
@@ -0,0 +1,424 @@
+// 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/side_swipe/side_swipe_navigation_view.h"
+
+#include <cmath>
+
+#include "base/logging.h"
+#include "base/mac/objc_property_releaser.h"
+#include "base/mac/scoped_nsobject.h"
+#include "base/metrics/user_metrics.h"
+#include "base/metrics/user_metrics_action.h"
+#import "ios/chrome/browser/ui/side_swipe/side_swipe_util.h"
+#import "ios/chrome/browser/ui/side_swipe_gesture_recognizer.h"
+#include "ios/chrome/browser/ui/ui_util.h"
+#import "ios/chrome/browser/ui/uikit_ui_util.h"
+#import "ios/chrome/common/material_timing.h"
+
+namespace {
+
+enum class SwipeType { CHANGE_TABS, NAVIGATION };
+
+typedef struct {
+ CGFloat min;
+ CGFloat max;
+} FloatRange;
+
+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;
+}
+
+// The portion of the screen width a swipe must travel after which a navigation
+// should be initiated.
+const CGFloat kSwipeThreshold = 0.53;
+
+// Convert the velocity (which is measured in points per second) to points per
+// |kSwipeVelocityFraction| of a second.
+const CGFloat kSwipeVelocityFraction = 0.1;
+
+// Distance after which the arrow should animate in.
+const CGFloat kArrowThreshold = 32;
+
+// Duration of the snapping animation when the selection bubble animates.
+const CGFloat kSelectionSnappingAnimationDuration = 0.2;
+
+// Size of the selection circle.
+const CGFloat kSelectionSize = 64.0;
+
+// Start scale of the selection circle.
+const CGFloat kSelectionDownScale = 0.1875;
+
+// The final scale of the selection bubble when the threshold is met.
+const CGFloat kSelectionAnimationScale = 23;
+
+// The duration of the animations played when the threshold is met.
+const CGFloat kSelectionAnimationDuration = 0.5;
+}
+
+@interface SideSwipeNavigationView () {
+ @private
+
+ // The back or forward sprite image.
+ base::scoped_nsobject<UIImageView> arrowView_;
+
+ // The selection bubble.
+ CAShapeLayer* selectionCircleLayer_;
+
+ // If |NO| this is an edge gesture and navigation isn't possible. Don't show
+ // arrows and bubbles and don't allow navigate.
+ BOOL canNavigate_;
+
+ // If |YES| arrowView_ is directionnal and must be rotated 180 degreed for the
+ // forward panes.
+ BOOL rotateForward_;
+
+ base::mac::ObjCPropertyReleaser _propertyReleaser_SideSwipeNavigationView;
+}
+// Returns a newly allocated and configured selection circle shape.
+- (CAShapeLayer*)newSelectionCircleLayer;
+// Pushes the touch towards the edge because it's difficult to touch the very
+// edge of the screen (touches tend to sit near x ~ 4).
+- (CGPoint)adjustPointToEdge:(CGPoint)point;
+@end
+
+@implementation SideSwipeNavigationView
+
+@synthesize targetView = targetView_;
+
+- (instancetype)initWithFrame:(CGRect)frame
+ withDirection:(UISwipeGestureRecognizerDirection)direction
+ canNavigate:(BOOL)canNavigate
+ image:(UIImage*)image
+ rotateForward:(BOOL)rotateForward {
+ self = [super initWithFrame:frame];
+ if (self) {
+ _propertyReleaser_SideSwipeNavigationView.Init(
+ self, [SideSwipeNavigationView class]);
+ self.backgroundColor = [UIColor colorWithWhite:90.0 / 256 alpha:1.0];
+
+ canNavigate_ = canNavigate;
+ rotateForward_ = rotateForward;
+ if (canNavigate) {
+ image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
+ const CGRect imageSize = CGRectMake(0, 0, 24, 24);
+ arrowView_.reset([[UIImageView alloc] initWithImage:image]);
+ [arrowView_ setTintColor:[UIColor whiteColor]];
+ selectionCircleLayer_ = [self newSelectionCircleLayer];
+ [arrowView_ setFrame:imageSize];
+ }
+
+ UIImage* shadowImage =
+ [UIImage imageNamed:@"side_swipe_navigation_content_shadow"];
+ CGRect borderFrame =
+ CGRectMake(0, 0, shadowImage.size.width, self.frame.size.height);
+ base::scoped_nsobject<UIImageView> border(
+ [[UIImageView alloc] initWithFrame:borderFrame]);
+ [border setImage:shadowImage];
+ [self addSubview:border];
+ if (direction == UISwipeGestureRecognizerDirectionRight) {
+ borderFrame.origin.x = frame.size.width - shadowImage.size.width;
+ [border setFrame:borderFrame];
+ [border setAutoresizingMask:UIViewAutoresizingFlexibleLeftMargin];
+ } else {
+ [border setTransform:CGAffineTransformMakeRotation(M_PI)];
+ [border setAutoresizingMask:UIViewAutoresizingFlexibleRightMargin];
+ }
+
+ [self.layer addSublayer:selectionCircleLayer_];
+ [self setClipsToBounds:YES];
+ [self addSubview:arrowView_];
+ }
+ return self;
+}
+
+- (CGPoint)adjustPointToEdge:(CGPoint)currentPoint {
+ CGFloat width = CGRectGetWidth(self.targetView.bounds);
+ CGFloat half = floor(width / 2);
+ CGFloat padding = floor(std::abs(currentPoint.x - half) / half);
+
+ // Push towards the edges.
+ if (currentPoint.x > half)
+ currentPoint.x += padding;
+ else
+ currentPoint.x -= padding;
+
+ // But don't go past the edges.
+ if (currentPoint.x < 0)
+ currentPoint.x = 0;
+ else if (currentPoint.x > width)
+ currentPoint.x = width;
+
+ return currentPoint;
+}
+
+- (void)updateFrameAndAnimateContents:(CGFloat)distance
+ forGesture:(SideSwipeGestureRecognizer*)gesture {
+ CGFloat width = CGRectGetWidth(self.targetView.bounds);
+
+ // Immediately set frame size.
+ CGRect frame = self.frame;
+ if (gesture.direction == UISwipeGestureRecognizerDirectionRight) {
+ frame.size.width = self.targetView.frame.origin.x;
+ frame.origin.x = 0;
+ } else {
+ frame.origin.x = self.targetView.frame.origin.x + width;
+ frame.size.width = width - frame.origin.x;
+ }
+ [self setFrame:frame];
+
+ // Move |selectionCircleLayer_| without animations.
+ CGRect bounds = self.bounds;
+ CGPoint center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
+ [arrowView_ setCenter:AlignPointToPixel(center)];
+ [CATransaction begin];
+ [CATransaction setDisableActions:YES];
+ [selectionCircleLayer_ setPosition:center];
+ [CATransaction commit];
+
+ CGFloat rotationStart = -M_PI_2;
+ CGFloat rotationEnd = 0;
+ if (gesture.direction == UISwipeGestureRecognizerDirectionLeft) {
+ if (rotateForward_) {
+ rotationStart = M_PI * 1.5;
+ rotationEnd = M_PI;
+ } else {
+ rotationStart = M_PI * 0.5;
+ rotationEnd = 0;
+ }
+ }
+ CGAffineTransform rotation = CGAffineTransformMakeRotation(MapValueToRange(
+ {0, kArrowThreshold}, {rotationStart, rotationEnd}, distance));
+ CGFloat scale = MapValueToRange({0, kArrowThreshold}, {0, 1}, distance);
+ [arrowView_ setTransform:CGAffineTransformScale(rotation, scale, scale)];
+
+ // Animate selection bubbles dpending on distance.
+ [UIView beginAnimations:@"transform" context:NULL];
+ [UIView setAnimationDuration:kSelectionSnappingAnimationDuration];
+ if (distance < (width * kSwipeThreshold)) {
+ // Scale selection down.
+ selectionCircleLayer_.transform =
+ CATransform3DMakeScale(kSelectionDownScale, kSelectionDownScale, 1);
+ selectionCircleLayer_.opacity = 0;
+ [arrowView_ setAlpha:MapValueToRange({0, 64}, {0, 1}, distance)];
+ [arrowView_ setTintColor:[UIColor whiteColor]];
+ } else {
+ selectionCircleLayer_.transform = CATransform3DMakeScale(1, 1, 1);
+ selectionCircleLayer_.opacity = 0.75;
+ [arrowView_ setAlpha:1];
+ [arrowView_ setTintColor:self.backgroundColor];
+ }
+ [UIView commitAnimations];
+}
+
+- (void)explodeSelection:(void (^)(void))block {
+ [CATransaction begin];
+ [CATransaction setCompletionBlock:^{
+ // Note that the animations below may complete at slightly different times
+ // resulting in frame(s) between animation completion and the transaction's
+ // completion handler that show the original state. To avoid this flicker,
+ // the animations use a fillMode forward and are not removed until the
+ // transaction completion handler is executed.
+ [selectionCircleLayer_ removeAnimationForKey:@"opacity"];
+ [selectionCircleLayer_ removeAnimationForKey:@"transform"];
+ [selectionCircleLayer_ setOpacity:0];
+ [arrowView_ setAlpha:0];
+ self.backgroundColor = [UIColor whiteColor];
+ block();
+
+ }];
+
+ CAMediaTimingFunction* timing =
+ ios::material::TimingFunction(ios::material::CurveEaseInOut);
+ CABasicAnimation* scaleAnimation =
+ [CABasicAnimation animationWithKeyPath:@"transform"];
+ scaleAnimation.fromValue =
+ [NSValue valueWithCATransform3D:CATransform3DIdentity];
+ scaleAnimation.toValue =
+ [NSValue valueWithCATransform3D:CATransform3DMakeScale(
+ kSelectionAnimationScale,
+ kSelectionAnimationScale, 1)];
+ scaleAnimation.timingFunction = timing;
+ scaleAnimation.duration = kSelectionAnimationDuration;
+ scaleAnimation.fillMode = kCAFillModeForwards;
+ scaleAnimation.removedOnCompletion = NO;
+ [selectionCircleLayer_ addAnimation:scaleAnimation forKey:@"transform"];
+
+ CABasicAnimation* opacityAnimation =
+ [CABasicAnimation animationWithKeyPath:@"opacity"];
+ opacityAnimation.fromValue = @(selectionCircleLayer_.opacity);
+ opacityAnimation.toValue = @(1);
+ opacityAnimation.timingFunction = timing;
+ opacityAnimation.duration = kSelectionAnimationDuration;
+ opacityAnimation.fillMode = kCAFillModeForwards;
+ opacityAnimation.removedOnCompletion = NO;
+ [selectionCircleLayer_ addAnimation:opacityAnimation forKey:@"opacity"];
+
+ CABasicAnimation* positionAnimation =
+ [CABasicAnimation animationWithKeyPath:@"position"];
+ positionAnimation.fromValue =
+ [NSValue valueWithCGPoint:selectionCircleLayer_.position];
+
+ CGPoint finalPosition = CGPointMake([self.targetView superview].center.x,
+ selectionCircleLayer_.position.y);
+ positionAnimation.toValue = [NSValue valueWithCGPoint:finalPosition];
+ positionAnimation.timingFunction = timing;
+ positionAnimation.duration = kSelectionAnimationDuration;
+ positionAnimation.fillMode = kCAFillModeForwards;
+ positionAnimation.removedOnCompletion = NO;
+ [selectionCircleLayer_ addAnimation:positionAnimation forKey:@"position"];
+ [CATransaction commit];
+
+ [arrowView_ setAlpha:1];
+ [arrowView_ setTintColor:self.backgroundColor];
+ [UIView animateWithDuration:kSelectionAnimationDuration
+ animations:^{
+ [arrowView_ setAlpha:0];
+ }];
+}
+
+- (void)handleHorizontalPan:(SideSwipeGestureRecognizer*)gesture
+ onOverThresholdCompletion:(void (^)(void))onOverThresholdCompletion
+ onUnderThresholdCompletion:(void (^)(void))onUnderThresholdCompletion {
+ CGPoint currentPoint = [gesture locationInView:gesture.view];
+ CGPoint velocityPoint = [gesture velocityInView:gesture.view];
+ currentPoint.x -= gesture.swipeOffset;
+
+ // Push point to edge.
+ currentPoint = [self adjustPointToEdge:currentPoint];
+
+ CGFloat distance = currentPoint.x;
+ // The snap back animation is 0.1 seconds, so convert the velocity distance
+ // to where the |x| position would in .1 seconds.
+ CGFloat velocityOffset = velocityPoint.x * kSwipeVelocityFraction;
+ CGFloat width = CGRectGetWidth(self.targetView.bounds);
+ if (gesture.direction == UISwipeGestureRecognizerDirectionLeft) {
+ distance = width - distance;
+ velocityOffset = -velocityOffset;
+ }
+
+ if (!canNavigate_) {
+ // shrink distance a bit to make the drag feel springier.
+ distance /= 3;
+ }
+
+ CGRect frame = self.targetView.frame;
+ if (gesture.direction == UISwipeGestureRecognizerDirectionLeft) {
+ frame.origin.x = -distance;
+ } else {
+ frame.origin.x = distance;
+ }
+ self.targetView.frame = frame;
+
+ [self updateFrameAndAnimateContents:distance forGesture:gesture];
+
+ if (gesture.state == UIGestureRecognizerStateEnded ||
+ gesture.state == UIGestureRecognizerStateCancelled ||
+ gesture.state == UIGestureRecognizerStateFailed) {
+ CGFloat threshold = width * kSwipeThreshold;
+ CGFloat finalDistance = distance + velocityOffset;
+ // Ensure the actual distance traveled has met the minimum arrow threshold
+ // and that the distance including expected velocity is over |threshold|.
+ if (distance > kArrowThreshold && finalDistance > threshold &&
+ canNavigate_ && gesture.state == UIGestureRecognizerStateEnded) {
+ // Speed up the animation for higher velocity swipes.
+ CGFloat animationTime = MapValueToRange(
+ {threshold, width},
+ {kSelectionAnimationDuration, kSelectionAnimationDuration / 2},
+ finalDistance);
+ [self animateTargetViewCompleted:YES
+ withDirection:gesture.direction
+ withDuration:animationTime];
+ [self explodeSelection:onOverThresholdCompletion];
+ if (IsSwipingForward(gesture.direction)) {
+ base::RecordAction(base::UserMetricsAction(
+ "MobileEdgeSwipeNavigationForwardCompleted"));
+ } else {
+ base::RecordAction(
+ base::UserMetricsAction("MobileEdgeSwipeNavigationBackCompleted"));
+ }
+ } else {
+ [self animateTargetViewCompleted:NO
+ withDirection:gesture.direction
+ withDuration:0.1];
+ onUnderThresholdCompletion();
+ if (IsSwipingForward(gesture.direction)) {
+ base::RecordAction(base::UserMetricsAction(
+ "MobileEdgeSwipeNavigationForwardCancelled"));
+ } else {
+ base::RecordAction(
+ base::UserMetricsAction("MobileEdgeSwipeNavigationBackCancelled"));
+ }
+ }
+ }
+}
+
+- (void)animateTargetViewCompleted:(BOOL)completed
+ withDirection:(UISwipeGestureRecognizerDirection)direction
+ withDuration:(CGFloat)duration {
+ void (^animationBlock)(void) = ^{
+ CGRect targetFrame = self.targetView.frame;
+ CGRect frame = self.frame;
+ CGFloat width = CGRectGetWidth(self.targetView.bounds);
+ // Animate self.targetFrame to the side if completed and to the center if
+ // not. Animate self.view to the center if completed or to the size if not.
+ if (completed) {
+ frame.origin.x = 0;
+ frame.size.width = width;
+ self.frame = frame;
+ targetFrame.origin.x =
+ direction == UISwipeGestureRecognizerDirectionRight ? width : -width;
+ self.targetView.frame = targetFrame;
+ } else {
+ targetFrame.origin.x = 0;
+ self.targetView.frame = targetFrame;
+ frame.origin.x =
+ direction == UISwipeGestureRecognizerDirectionLeft ? width : 0;
+ frame.size.width = 0;
+ self.frame = frame;
+ }
+ CGRect bounds = self.bounds;
+ CGPoint center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
+ [arrowView_ setCenter:AlignPointToPixel(center)];
+ };
+ CGFloat cleanUpDelay = completed ? kSelectionAnimationDuration - duration : 0;
+ [UIView animateWithDuration:duration
+ animations:animationBlock
+ completion:^(BOOL finished) {
+ // Give the other animations time to complete.
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
+ cleanUpDelay * NSEC_PER_SEC),
+ dispatch_get_main_queue(), ^{
+ // Reset target frame.
+ CGRect frame = self.targetView.frame;
+ frame.origin.x = 0;
+ self.targetView.frame = frame;
+ [self removeFromSuperview];
+ });
+ }];
+}
+
+- (CAShapeLayer*)newSelectionCircleLayer {
+ const CGRect bounds = CGRectMake(0, 0, kSelectionSize, kSelectionSize);
+ CAShapeLayer* selectionCircleLayer = [[CAShapeLayer alloc] init];
+ selectionCircleLayer.bounds = bounds;
+ selectionCircleLayer.backgroundColor = [[UIColor clearColor] CGColor];
+ selectionCircleLayer.fillColor = [[UIColor whiteColor] CGColor];
+ selectionCircleLayer.opacity = 0;
+ selectionCircleLayer.transform =
+ CATransform3DMakeScale(kSelectionDownScale, kSelectionDownScale, 1);
+ selectionCircleLayer.path =
+ [[UIBezierPath bezierPathWithOvalInRect:bounds] CGPath];
+
+ return selectionCircleLayer;
+}
+
+@end
« no previous file with comments | « ios/chrome/browser/ui/side_swipe/side_swipe_navigation_view.h ('k') | ios/chrome/browser/ui/side_swipe/side_swipe_util.h » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698