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

Side by Side 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 unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #import "ios/chrome/browser/ui/side_swipe/side_swipe_navigation_view.h"
6
7 #include <cmath>
8
9 #include "base/logging.h"
10 #include "base/mac/objc_property_releaser.h"
11 #include "base/mac/scoped_nsobject.h"
12 #include "base/metrics/user_metrics.h"
13 #include "base/metrics/user_metrics_action.h"
14 #import "ios/chrome/browser/ui/side_swipe/side_swipe_util.h"
15 #import "ios/chrome/browser/ui/side_swipe_gesture_recognizer.h"
16 #include "ios/chrome/browser/ui/ui_util.h"
17 #import "ios/chrome/browser/ui/uikit_ui_util.h"
18 #import "ios/chrome/common/material_timing.h"
19
20 namespace {
21
22 enum class SwipeType { CHANGE_TABS, NAVIGATION };
23
24 typedef struct {
25 CGFloat min;
26 CGFloat max;
27 } FloatRange;
28
29 CGFloat MapValueToRange(FloatRange from, FloatRange to, CGFloat value) {
30 DCHECK(from.min < from.max);
31 if (value <= from.min)
32 return to.min;
33 if (value >= from.max)
34 return to.max;
35 const CGFloat fromDst = from.max - from.min;
36 const CGFloat toDst = to.max - to.min;
37 return to.min + ((value - from.min) / fromDst) * toDst;
38 }
39
40 // The portion of the screen width a swipe must travel after which a navigation
41 // should be initiated.
42 const CGFloat kSwipeThreshold = 0.53;
43
44 // Convert the velocity (which is measured in points per second) to points per
45 // |kSwipeVelocityFraction| of a second.
46 const CGFloat kSwipeVelocityFraction = 0.1;
47
48 // Distance after which the arrow should animate in.
49 const CGFloat kArrowThreshold = 32;
50
51 // Duration of the snapping animation when the selection bubble animates.
52 const CGFloat kSelectionSnappingAnimationDuration = 0.2;
53
54 // Size of the selection circle.
55 const CGFloat kSelectionSize = 64.0;
56
57 // Start scale of the selection circle.
58 const CGFloat kSelectionDownScale = 0.1875;
59
60 // The final scale of the selection bubble when the threshold is met.
61 const CGFloat kSelectionAnimationScale = 23;
62
63 // The duration of the animations played when the threshold is met.
64 const CGFloat kSelectionAnimationDuration = 0.5;
65 }
66
67 @interface SideSwipeNavigationView () {
68 @private
69
70 // The back or forward sprite image.
71 base::scoped_nsobject<UIImageView> arrowView_;
72
73 // The selection bubble.
74 CAShapeLayer* selectionCircleLayer_;
75
76 // If |NO| this is an edge gesture and navigation isn't possible. Don't show
77 // arrows and bubbles and don't allow navigate.
78 BOOL canNavigate_;
79
80 // If |YES| arrowView_ is directionnal and must be rotated 180 degreed for the
81 // forward panes.
82 BOOL rotateForward_;
83
84 base::mac::ObjCPropertyReleaser _propertyReleaser_SideSwipeNavigationView;
85 }
86 // Returns a newly allocated and configured selection circle shape.
87 - (CAShapeLayer*)newSelectionCircleLayer;
88 // Pushes the touch towards the edge because it's difficult to touch the very
89 // edge of the screen (touches tend to sit near x ~ 4).
90 - (CGPoint)adjustPointToEdge:(CGPoint)point;
91 @end
92
93 @implementation SideSwipeNavigationView
94
95 @synthesize targetView = targetView_;
96
97 - (instancetype)initWithFrame:(CGRect)frame
98 withDirection:(UISwipeGestureRecognizerDirection)direction
99 canNavigate:(BOOL)canNavigate
100 image:(UIImage*)image
101 rotateForward:(BOOL)rotateForward {
102 self = [super initWithFrame:frame];
103 if (self) {
104 _propertyReleaser_SideSwipeNavigationView.Init(
105 self, [SideSwipeNavigationView class]);
106 self.backgroundColor = [UIColor colorWithWhite:90.0 / 256 alpha:1.0];
107
108 canNavigate_ = canNavigate;
109 rotateForward_ = rotateForward;
110 if (canNavigate) {
111 image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
112 const CGRect imageSize = CGRectMake(0, 0, 24, 24);
113 arrowView_.reset([[UIImageView alloc] initWithImage:image]);
114 [arrowView_ setTintColor:[UIColor whiteColor]];
115 selectionCircleLayer_ = [self newSelectionCircleLayer];
116 [arrowView_ setFrame:imageSize];
117 }
118
119 UIImage* shadowImage =
120 [UIImage imageNamed:@"side_swipe_navigation_content_shadow"];
121 CGRect borderFrame =
122 CGRectMake(0, 0, shadowImage.size.width, self.frame.size.height);
123 base::scoped_nsobject<UIImageView> border(
124 [[UIImageView alloc] initWithFrame:borderFrame]);
125 [border setImage:shadowImage];
126 [self addSubview:border];
127 if (direction == UISwipeGestureRecognizerDirectionRight) {
128 borderFrame.origin.x = frame.size.width - shadowImage.size.width;
129 [border setFrame:borderFrame];
130 [border setAutoresizingMask:UIViewAutoresizingFlexibleLeftMargin];
131 } else {
132 [border setTransform:CGAffineTransformMakeRotation(M_PI)];
133 [border setAutoresizingMask:UIViewAutoresizingFlexibleRightMargin];
134 }
135
136 [self.layer addSublayer:selectionCircleLayer_];
137 [self setClipsToBounds:YES];
138 [self addSubview:arrowView_];
139 }
140 return self;
141 }
142
143 - (CGPoint)adjustPointToEdge:(CGPoint)currentPoint {
144 CGFloat width = CGRectGetWidth(self.targetView.bounds);
145 CGFloat half = floor(width / 2);
146 CGFloat padding = floor(std::abs(currentPoint.x - half) / half);
147
148 // Push towards the edges.
149 if (currentPoint.x > half)
150 currentPoint.x += padding;
151 else
152 currentPoint.x -= padding;
153
154 // But don't go past the edges.
155 if (currentPoint.x < 0)
156 currentPoint.x = 0;
157 else if (currentPoint.x > width)
158 currentPoint.x = width;
159
160 return currentPoint;
161 }
162
163 - (void)updateFrameAndAnimateContents:(CGFloat)distance
164 forGesture:(SideSwipeGestureRecognizer*)gesture {
165 CGFloat width = CGRectGetWidth(self.targetView.bounds);
166
167 // Immediately set frame size.
168 CGRect frame = self.frame;
169 if (gesture.direction == UISwipeGestureRecognizerDirectionRight) {
170 frame.size.width = self.targetView.frame.origin.x;
171 frame.origin.x = 0;
172 } else {
173 frame.origin.x = self.targetView.frame.origin.x + width;
174 frame.size.width = width - frame.origin.x;
175 }
176 [self setFrame:frame];
177
178 // Move |selectionCircleLayer_| without animations.
179 CGRect bounds = self.bounds;
180 CGPoint center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
181 [arrowView_ setCenter:AlignPointToPixel(center)];
182 [CATransaction begin];
183 [CATransaction setDisableActions:YES];
184 [selectionCircleLayer_ setPosition:center];
185 [CATransaction commit];
186
187 CGFloat rotationStart = -M_PI_2;
188 CGFloat rotationEnd = 0;
189 if (gesture.direction == UISwipeGestureRecognizerDirectionLeft) {
190 if (rotateForward_) {
191 rotationStart = M_PI * 1.5;
192 rotationEnd = M_PI;
193 } else {
194 rotationStart = M_PI * 0.5;
195 rotationEnd = 0;
196 }
197 }
198 CGAffineTransform rotation = CGAffineTransformMakeRotation(MapValueToRange(
199 {0, kArrowThreshold}, {rotationStart, rotationEnd}, distance));
200 CGFloat scale = MapValueToRange({0, kArrowThreshold}, {0, 1}, distance);
201 [arrowView_ setTransform:CGAffineTransformScale(rotation, scale, scale)];
202
203 // Animate selection bubbles dpending on distance.
204 [UIView beginAnimations:@"transform" context:NULL];
205 [UIView setAnimationDuration:kSelectionSnappingAnimationDuration];
206 if (distance < (width * kSwipeThreshold)) {
207 // Scale selection down.
208 selectionCircleLayer_.transform =
209 CATransform3DMakeScale(kSelectionDownScale, kSelectionDownScale, 1);
210 selectionCircleLayer_.opacity = 0;
211 [arrowView_ setAlpha:MapValueToRange({0, 64}, {0, 1}, distance)];
212 [arrowView_ setTintColor:[UIColor whiteColor]];
213 } else {
214 selectionCircleLayer_.transform = CATransform3DMakeScale(1, 1, 1);
215 selectionCircleLayer_.opacity = 0.75;
216 [arrowView_ setAlpha:1];
217 [arrowView_ setTintColor:self.backgroundColor];
218 }
219 [UIView commitAnimations];
220 }
221
222 - (void)explodeSelection:(void (^)(void))block {
223 [CATransaction begin];
224 [CATransaction setCompletionBlock:^{
225 // Note that the animations below may complete at slightly different times
226 // resulting in frame(s) between animation completion and the transaction's
227 // completion handler that show the original state. To avoid this flicker,
228 // the animations use a fillMode forward and are not removed until the
229 // transaction completion handler is executed.
230 [selectionCircleLayer_ removeAnimationForKey:@"opacity"];
231 [selectionCircleLayer_ removeAnimationForKey:@"transform"];
232 [selectionCircleLayer_ setOpacity:0];
233 [arrowView_ setAlpha:0];
234 self.backgroundColor = [UIColor whiteColor];
235 block();
236
237 }];
238
239 CAMediaTimingFunction* timing =
240 ios::material::TimingFunction(ios::material::CurveEaseInOut);
241 CABasicAnimation* scaleAnimation =
242 [CABasicAnimation animationWithKeyPath:@"transform"];
243 scaleAnimation.fromValue =
244 [NSValue valueWithCATransform3D:CATransform3DIdentity];
245 scaleAnimation.toValue =
246 [NSValue valueWithCATransform3D:CATransform3DMakeScale(
247 kSelectionAnimationScale,
248 kSelectionAnimationScale, 1)];
249 scaleAnimation.timingFunction = timing;
250 scaleAnimation.duration = kSelectionAnimationDuration;
251 scaleAnimation.fillMode = kCAFillModeForwards;
252 scaleAnimation.removedOnCompletion = NO;
253 [selectionCircleLayer_ addAnimation:scaleAnimation forKey:@"transform"];
254
255 CABasicAnimation* opacityAnimation =
256 [CABasicAnimation animationWithKeyPath:@"opacity"];
257 opacityAnimation.fromValue = @(selectionCircleLayer_.opacity);
258 opacityAnimation.toValue = @(1);
259 opacityAnimation.timingFunction = timing;
260 opacityAnimation.duration = kSelectionAnimationDuration;
261 opacityAnimation.fillMode = kCAFillModeForwards;
262 opacityAnimation.removedOnCompletion = NO;
263 [selectionCircleLayer_ addAnimation:opacityAnimation forKey:@"opacity"];
264
265 CABasicAnimation* positionAnimation =
266 [CABasicAnimation animationWithKeyPath:@"position"];
267 positionAnimation.fromValue =
268 [NSValue valueWithCGPoint:selectionCircleLayer_.position];
269
270 CGPoint finalPosition = CGPointMake([self.targetView superview].center.x,
271 selectionCircleLayer_.position.y);
272 positionAnimation.toValue = [NSValue valueWithCGPoint:finalPosition];
273 positionAnimation.timingFunction = timing;
274 positionAnimation.duration = kSelectionAnimationDuration;
275 positionAnimation.fillMode = kCAFillModeForwards;
276 positionAnimation.removedOnCompletion = NO;
277 [selectionCircleLayer_ addAnimation:positionAnimation forKey:@"position"];
278 [CATransaction commit];
279
280 [arrowView_ setAlpha:1];
281 [arrowView_ setTintColor:self.backgroundColor];
282 [UIView animateWithDuration:kSelectionAnimationDuration
283 animations:^{
284 [arrowView_ setAlpha:0];
285 }];
286 }
287
288 - (void)handleHorizontalPan:(SideSwipeGestureRecognizer*)gesture
289 onOverThresholdCompletion:(void (^)(void))onOverThresholdCompletion
290 onUnderThresholdCompletion:(void (^)(void))onUnderThresholdCompletion {
291 CGPoint currentPoint = [gesture locationInView:gesture.view];
292 CGPoint velocityPoint = [gesture velocityInView:gesture.view];
293 currentPoint.x -= gesture.swipeOffset;
294
295 // Push point to edge.
296 currentPoint = [self adjustPointToEdge:currentPoint];
297
298 CGFloat distance = currentPoint.x;
299 // The snap back animation is 0.1 seconds, so convert the velocity distance
300 // to where the |x| position would in .1 seconds.
301 CGFloat velocityOffset = velocityPoint.x * kSwipeVelocityFraction;
302 CGFloat width = CGRectGetWidth(self.targetView.bounds);
303 if (gesture.direction == UISwipeGestureRecognizerDirectionLeft) {
304 distance = width - distance;
305 velocityOffset = -velocityOffset;
306 }
307
308 if (!canNavigate_) {
309 // shrink distance a bit to make the drag feel springier.
310 distance /= 3;
311 }
312
313 CGRect frame = self.targetView.frame;
314 if (gesture.direction == UISwipeGestureRecognizerDirectionLeft) {
315 frame.origin.x = -distance;
316 } else {
317 frame.origin.x = distance;
318 }
319 self.targetView.frame = frame;
320
321 [self updateFrameAndAnimateContents:distance forGesture:gesture];
322
323 if (gesture.state == UIGestureRecognizerStateEnded ||
324 gesture.state == UIGestureRecognizerStateCancelled ||
325 gesture.state == UIGestureRecognizerStateFailed) {
326 CGFloat threshold = width * kSwipeThreshold;
327 CGFloat finalDistance = distance + velocityOffset;
328 // Ensure the actual distance traveled has met the minimum arrow threshold
329 // and that the distance including expected velocity is over |threshold|.
330 if (distance > kArrowThreshold && finalDistance > threshold &&
331 canNavigate_ && gesture.state == UIGestureRecognizerStateEnded) {
332 // Speed up the animation for higher velocity swipes.
333 CGFloat animationTime = MapValueToRange(
334 {threshold, width},
335 {kSelectionAnimationDuration, kSelectionAnimationDuration / 2},
336 finalDistance);
337 [self animateTargetViewCompleted:YES
338 withDirection:gesture.direction
339 withDuration:animationTime];
340 [self explodeSelection:onOverThresholdCompletion];
341 if (IsSwipingForward(gesture.direction)) {
342 base::RecordAction(base::UserMetricsAction(
343 "MobileEdgeSwipeNavigationForwardCompleted"));
344 } else {
345 base::RecordAction(
346 base::UserMetricsAction("MobileEdgeSwipeNavigationBackCompleted"));
347 }
348 } else {
349 [self animateTargetViewCompleted:NO
350 withDirection:gesture.direction
351 withDuration:0.1];
352 onUnderThresholdCompletion();
353 if (IsSwipingForward(gesture.direction)) {
354 base::RecordAction(base::UserMetricsAction(
355 "MobileEdgeSwipeNavigationForwardCancelled"));
356 } else {
357 base::RecordAction(
358 base::UserMetricsAction("MobileEdgeSwipeNavigationBackCancelled"));
359 }
360 }
361 }
362 }
363
364 - (void)animateTargetViewCompleted:(BOOL)completed
365 withDirection:(UISwipeGestureRecognizerDirection)direction
366 withDuration:(CGFloat)duration {
367 void (^animationBlock)(void) = ^{
368 CGRect targetFrame = self.targetView.frame;
369 CGRect frame = self.frame;
370 CGFloat width = CGRectGetWidth(self.targetView.bounds);
371 // Animate self.targetFrame to the side if completed and to the center if
372 // not. Animate self.view to the center if completed or to the size if not.
373 if (completed) {
374 frame.origin.x = 0;
375 frame.size.width = width;
376 self.frame = frame;
377 targetFrame.origin.x =
378 direction == UISwipeGestureRecognizerDirectionRight ? width : -width;
379 self.targetView.frame = targetFrame;
380 } else {
381 targetFrame.origin.x = 0;
382 self.targetView.frame = targetFrame;
383 frame.origin.x =
384 direction == UISwipeGestureRecognizerDirectionLeft ? width : 0;
385 frame.size.width = 0;
386 self.frame = frame;
387 }
388 CGRect bounds = self.bounds;
389 CGPoint center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
390 [arrowView_ setCenter:AlignPointToPixel(center)];
391 };
392 CGFloat cleanUpDelay = completed ? kSelectionAnimationDuration - duration : 0;
393 [UIView animateWithDuration:duration
394 animations:animationBlock
395 completion:^(BOOL finished) {
396 // Give the other animations time to complete.
397 dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
398 cleanUpDelay * NSEC_PER_SEC),
399 dispatch_get_main_queue(), ^{
400 // Reset target frame.
401 CGRect frame = self.targetView.frame;
402 frame.origin.x = 0;
403 self.targetView.frame = frame;
404 [self removeFromSuperview];
405 });
406 }];
407 }
408
409 - (CAShapeLayer*)newSelectionCircleLayer {
410 const CGRect bounds = CGRectMake(0, 0, kSelectionSize, kSelectionSize);
411 CAShapeLayer* selectionCircleLayer = [[CAShapeLayer alloc] init];
412 selectionCircleLayer.bounds = bounds;
413 selectionCircleLayer.backgroundColor = [[UIColor clearColor] CGColor];
414 selectionCircleLayer.fillColor = [[UIColor whiteColor] CGColor];
415 selectionCircleLayer.opacity = 0;
416 selectionCircleLayer.transform =
417 CATransform3DMakeScale(kSelectionDownScale, kSelectionDownScale, 1);
418 selectionCircleLayer.path =
419 [[UIBezierPath bezierPathWithOvalInRect:bounds] CGPath];
420
421 return selectionCircleLayer;
422 }
423
424 @end
OLDNEW
« 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